How Discord Implemented App-Wide Keyboard Navigation

When working on creating a complete keyboard navigation experience for Discord, using styling with :focus and outline, the folks at Discord ran into issues where the outline would not match the shape of actual element being rendered. Thinks like border-radius, overflow: hidden; on the container, padding got in the way. So they set out to find a solution.

After a lot of trial and error, we landed on a system which is built on two components: FocusRing for declaring where a ring should be placed, and FocusRingScope for declaring a root position for rings to be rendered.

Here’s an example showing how the FocusRing works:

function SearchBar() {
  const containerRef = React.useRef<HTMLDivElement>(null);
  const inputRef = React.useRef<HTMLInputElement>(null);
  return (
    <FocusRing focusTarget={inputRef} ringTarget={containerRef}>
      <div className={styles.container} ref={containerRef}>
        <input type="text" ref={inputRef} placeholder="Search" />
        <div className={styles.icon}>
          <ClearIcon />
        </div>
      </div>
    </FocusRing>
  );
}

The FocusRing will capture focussing of the contained input, but will render the ring around the entire div. To have a FocusRing behave like :focus-within and respond to any descendant being focussed, you can set the within prop.

The package can be installed using NPM:

npm i react-focus-rings

How Discord Implemented App-Wide Keyboard Navigation →
Browser Focus Ring Problems →
react-focus-rings (GitHub) →

Related: Not entirely coincidental the aforementioned React Spectrum by Adobe also comes with a FocusRing component.

Deep Dive into Page Lifecycle API

As the name suggests, the Page Lifecycle API exposes the web page lifecycle hooks to JavaScript. However, it isn’t an entirely new concept. Page Visibility API existed for some time, revealing some of the page visibility events to JavaScript.
However, if you happen to choose between these two, it’s worth mentioning some of the limitations of the Page Visibility API.

  • It only provides visible and hidden states of a web page.
  • It can not capture pages discarded by the operating system (Android, IOS, and the latest Windows systems can terminated background processes to preserve system resources).

Let’s take a look at the page lifecycle states exposed by the Page Lifecycle API.

To implement the Page Lifecycle API and help overcome browser inconsistencies, there’s PageLifecycle.js that will come in handy:

import lifecycle from '/path/to/page-lifecycle.mjs';

lifecycle.addEventListener('statechange', function(event) {
  console.log(event.oldState, event.newState);
});

Deep Dive into Page Lifecycle API →
The Page Lifecycle API →
PageLifecycle.js →

Avoiding tab styles for navigation

Adam Silver, who works at/with the fine folks at GOV.UK:

Tabs should only look like tabs if they behave like tabs otherwise it can be in disorienting and confusing for users.

Shown above is the old layout that featured the tabs (which are actually links, here). The new version still has the links in place at the same position, but they’ve styled them differently so that the look less like tabs.

Avoiding tab styles for navigation →

Using JavaScript’s closest() Method to Capture a “Click outside” an Element

In Practical Use Cases for JavaScript’s closest() Method, Andreas Remdt talks about some nice use cases that use Element.closest().

I especially like this example with a menu. Click on one of the links and it will show the menu which has the class menu-dropdown. Clicking outside said menu will close it. It’s that latter one that leverages Element.closest().

var menu = document.querySelector(".menu-dropdown");

function handleClick(evt) {
  // Only if a click on a dropdown trigger happens, either close or open it.
  …
  
  // If a click happens somewhere outside .menu-dropdown, close it.
  if (!evt.target.closest(".menu-dropdown")) {
    menu.classList.add("is-hidden");
  }
}

window.addEventListener("click", handleClick);

Here’s a pen with the result:

🔥 If you haven’t checked out Hakim El Hattab’s dynamically drawn hit areas for menus, as talked about in Building Better Interfaces, be sure to do so, as they’re amazing:

React Navigation 5.0 alpha – Rethinking Navigation as a Component-first API

Just announced at React Native EU is an alpha release of React Navigation 5.0, a navigator for use with React Native.

An exploration of a component-first API for React Navigation for building more dynamic navigation solutions.

  • Should play well with static type system
  • Navigation state should be contained in root component (helpful for stuff such as deep linking)
  • Component-first API

Instead of using constructs where you had to call the createStackNavigator function with an object …

const AppNavigator = createStackNavigator({
  Home: {
    screen: HomeScreen,
  },
});

… there’s now a component that provides you with it:

const Stack = createStackNavigator();

function App() {
  return (
    <NavigationNativeContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
      </Stack.Navigator>
    </NavigationNativeContainer>
  );
}

React Navigation @next Documentation →
React Navigation @next Source (GitHub) →

pagemap, a mini map navigation for web pages

Many text editors nowadays sport a minimap on the right hand side of the screen. Pagemap is like that, but for webpages:

To use it, position a canvas element fixed on your screen, and tell pagemap which elements to include in the map:

<canvas id="map"></canvas>
#map {
    position: fixed;
    top: 0;
    right: 0;
    width: 200px;
    height: 100%;
    z-index: 100;
}
pagemap(document.querySelector('#map'), {
    viewport: null,
    styles: {
        'header,footer,section,article': rgba(0,0,0,0.08),
        'h1,a': rgba(0,0,0,0.10),
        'h2,h3,h4': rgba(0,0,0,0.08)
    },
    back: rgba(0,0,0,0.02),
    view: rgba(0,0,0,0.05),
    drag: rgba(0,0,0,0.10),
    interval: null
});

Installation per npm/yarn

$ yarn add pagemap

pagemap – minimap for web pages →
pagemap source (GitHub) →

Fixing common performance problems in React Navigation

If you’re using React Navigation in your app(s) you might have noticed these two issues the folks over at November Five have written about:

On a few screens – specifically those with lots of components – we started noticing a few things…

Right off the bat, there is a substantial delay between the user pressing a button and the swipe-in animation of a new screen. When a new screen is pushed, React Navigation will initially render it off-screen and animate it into place afterwards. This means that when a complex screen with lots of components that easily takes a few hundred milliseconds to render is pushed, it feels less snappy than a natively written application. It also causes some nasty side effects: for instance, if you tap a button quickly, you can trigger the same route from being pushed multiple times.

Another problem is that business logic can be executed while the swipe-in animation is doing its thing. This can make for a janky animation.

Of course the post also contains fixes for these problems 😉

Fixing common performance problems in React Navigation →

Meny — An experimental CSS 3D fold-in menu

A UI experiment with a 3D fold-in menu. Move your mouse to the left edge of this page — or swipe in from the left edge if you’re on a touch device — to expand the menu.

A refreshing alternative to the already overly common left nav flyout which Facebook introduced in their mobile app.

Meny Demo →
Meny Source (GitHub) →