惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The Register - Security
The Register - Security
美团技术团队
Recent Announcements
Recent Announcements
MongoDB | Blog
MongoDB | Blog
Jina AI
Jina AI
C
Check Point Blog
aimingoo的专栏
aimingoo的专栏
I
InfoQ
S
Securelist
T
Tor Project blog
GbyAI
GbyAI
L
LINUX DO - 热门话题
V
Visual Studio Blog
AWS News Blog
AWS News Blog
The Cloudflare Blog
腾讯CDC
K
Kaspersky official blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Recorded Future
Recorded Future
李成银的技术随笔
W
WeLiveSecurity
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
M
Microsoft Research Blog - Microsoft Research
G
Google Developers Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Schneier on Security
Schneier on Security
B
Blog
IT之家
IT之家
爱范儿
爱范儿
H
Help Net Security
Simon Willison's Weblog
Simon Willison's Weblog
NISL@THU
NISL@THU
J
Java Code Geeks
博客园 - 聂微东
T
The Exploit Database - CXSecurity.com
Cyberwarzone
Cyberwarzone
博客园 - 叶小钗
MyScale Blog
MyScale Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Project Zero
Project Zero
F
Future of Privacy Forum
D
Darknet – Hacking Tools, Hacker News & Cyber Security
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Hacker News: Ask HN
Hacker News: Ask HN
D
Docker
Apple Machine Learning Research
Apple Machine Learning Research
B
Blog RSS Feed
V
Vulnerabilities – Threatpost

DEV Community

9 SaaS development companies worth knowing (a technical look) Material Nova — The Best VS Code Theme of 2026 Inference Routing Is Becoming an Infrastructure Placement Problem I just build a League MBTI Analytics Why I Built My Own Site with Astro, Not WordPress when I use WordPress for a Living Hello! I'm a balloon artist who started 3D modeling 7 Next.js 16 Caching Bugs That Compile Fine and Break Silently in Production I got tired of writing READMEs so I built a tool that generates them from your GitHub URL FrontGate: a Lightweight Package Proxy for Supply Chain Security Why Your Expense Tracking Architecture Keeps Breaking Stop your AI trading agent from hallucinating technical analysis Breaking the Monorepo Barrier in a Crypto Store for Digital Products Imposter Syndrome Is Something We All Struggle With at Some Point in Our Careers Moving Beyond the Black Box: How I Built a Real-Time Voice Fitness Coach using Next.js 15, Convex, & Vapi.ai How to Recover Kafka DLQ Messages After a Schema Change Broke Your Consumer From Spec-Driven Development to Attractor-Guided Engineering Githubster free tool to track your GitHub followers and unfollowers Why Bitcoin Core RPC is Too Slow for High-Frequency Trading (And How to Fix It) Why Reading Food Labels Shouldn't Feel Like Decoding a Chemistry Exam I built a "brain" for AI coding agents — it never forgets and never stops How to Build a Local LLM Agent to Automate Work List Generation from Monthly Reports (With Jira Integration) Controlling Employee AI Usage on Managed Devices: Browser Controls, Cloudflare AI Gateway, and AWS Bedrock When Global Payment Gateways Fail, Local Solutions Shine LeetCode Solution: 13. Roman to Integer End-to-End Observability for vLLM and TGI: from DCGM to Tokens LeetCode Solution: 12. Integer to Roman 🚀 A Beginner’s First Look at Project IDX: Secure Coding from Day One Team Topologies for DevOps: A Practical Implementation Guide Seven Contradictions Shaped an Architecture. Telemedicine in Venezuela: A Technical Guide for Clinics in 2026 SSO, SAML, OIDC, and SCIM: What Actually Happens When You Click "Sign in with Google" Mastering Next.js 16 Server Actions & Forms: The Future of Full-Stack React | Muhammad Arslan Enterprise Laravel API Development: Best Practices for Performance, Security, and Scale | Muhammad Arslan How I Turned an Image Into a 3D Model in Minutes With AI Why Pure Rust WASM Is Harder Than It Looks Platform Stores Are a Dead End for Crypto Payments The VLA Testing Pipeline in Mano-AFK: When AI Agents QA Their Own Work LeetCode Solution: 10. Regular Expression Matching IPv4 Geolocation and Leasing: A Practical Guide for Network Operators Reconciling the Inefficiencies of Global Crypto Payments Platforms I Exported HT-Demucs FT to ONNX in 2026 (4 Blockers Everyone Else Gave Up On) 🤖 The Hacker in the Machine: Using AI Agents to Build Interactive Security Games Savings Plan Amortized Cost in AWS Cost Explorer: What It Is and How to Use It How to Tailor Your Resume to a Job Description in 5 Minutes (A Method That Actually Works) Flutter vs React Native in 2026: I Built the Same App in Both JWT vs Session Tokens in Spring Boot: A Senior Dev's Decision Guide How to Choose an AI Gateway in 2026 How to Teach Source Evaluation When Your Students Use ChatGPT Why Passwordless B2C Rollouts Stall at 5% (and How to Reach 60%) Rmux Review: Rust Terminal Multiplexer Built for AI Agents I realized I was only using half of what Claude Code has to offer DevOps & Deployment Essentials: Your Practical CI/CD Guide How next-generation captchas work and why it matters for automation Chat is Dead: How JSON Prompting Cut My AI Costs by 73% What if Everybody Were Suddenly... Better? OCI Web Application Firewall (WAF) Deep Dive: Architecture, Traffic Inspection, Threat Protection, and Enterprise Security Design Selling Digital Products in a Country PayPal Refuses to Touch PostgreSQL backup tool Databasus released backup verification in real database Docker containers We Connected an LLM to a 12-Year-Old Codebase. Here's What Broke. The Fallacy of Digital Platforms: Why Stripe Isn't Always King Sizce Google'ın 26 Mayıs tarihinde arama bölümünü tamamen yapay zekaya devredecek olması açık webin devamı için nasıl sonuçlanır? When Should You Use GraphRAG Instead of RAG? Big Data Is Not Just About “Huge Data” The Prefix Bubble MPP TestKit VSCode Extension - Inline HTTP 402 Payment Flow Hints The README Was a Protocol. The Entrypoint Was Still Optional. After AI Healthcare, Medical World Models May Be the Next Life-Science AI Platform Your AI Agent Doesn't Need an API Key: Entra Agent ID and Anthropic's Workload Identity Federation ECDSA - The Math That Only Goes One Way S3 Files Killed My Least Favorite Lambda Pattern BNB RPC Endpoints for Production Apps and Backend Workloads I Used to Get Excited About New Tools Now I Feel Tired. Google I/O 2026 — What I Hoped to See Beyond the Model Announcements Most 'AI agents' are just scripts with a marketing budget 🚀 Replicating the evasive VoidLink: My Journey Building Cortex C2 # new stuff dropped in duckkit 🦆 Paying the bills in a restricted country with cryptocurrency: the lie that almost killed our digital product Building Global Economies Through Better APIs: Lessons from PayPal vs Crypto for Crypto Payments in Developing Countries Verified or Not? Ep. 2 — Snyk's Own Test App Scanned With 9 Engines 17 SessionAuth Tools in OpenClaw: Integrate Any AI Framework with Wallet Infrastructure WebMCP and the Citation Paradox — What Agent-Ready Websites Actually Mean for GEO What Gemma 4 Doesn't Know About Cameroon — and What That Taught Me About Building AI for the Real World AI Can Generate Code — And Interactive Coding Playgrounds Are Becoming Essential Modern Web Guidance: Teaching AI Agents to Stop Coding Like It's 2019 The Discipline We Forgot We Had I Built a 3-Agent AI Research Crew in 250 Lines of Python (LangGraph + Free Gemini) PostgreSQL MCP: Let Claude query your databases in plain English Building digital products and Android apps under IteraTrail Fuel Price API for Fleet Cost Planning Linux File System Explained Simply Building a shot-detection worker for an upload pipeline with PySceneDetect 0.7 Wiring VMAF (and PSNR) into your encoder CI with FFmpeg 8.1 and ffmpeg-quality-metrics Bikin Chatbot Sendiri yang Bisa Jawab Pertanyaan dari Dokumen kamu Learning Arabic: Where to Start Shipping WebVTT subtitles in HLS that actually stay in sync (a hands-on guide for 2026) Understanding AI Code Fast: A 60-Second Habit for Institutional Memory Building a Real-Time Camera Classifier Chasing Tokens: The Developer Grind Nobody Warned You About A 10th Grader’s Journey: Why Cyber Security Starts with Your Very First Loop Why Most Developer Portfolios Fail to Show Engineering Maturity
Single List Keyboard Handling
ShaynaProduc · 2026-05-21 · via DEV Community

Prologue

A while ago, I decided to develop a fully accessible main navigation component in React and write a series of articles documenting the steps it took to create a non-trivial accessible component.

In my last article, the focus was actually on creating the components and adding a transformation utility to convert JavaScript objects into actual navigation. The base navigation component at this stage supports screen reader functionality through structured HTML and WAI-ARIA attributes, the ability to open and close a list through pointers and the Enter and Space keys.

This article, along with its accompanying release, focuses on basic keyboard handling for screen/keyboard users in a single list.


Note: This article is one of a series demonstrating how to build a React navigational component from scratch while considering accessibility through the process. The articles are accompanied by a GitHub repository with releases tied to one or more articles; each building on the previous, until a fully implemented navigation component is complete.

Each release and its associated tag contain fully runnable code for the article. The code discussed in this article is available in the release. and may be downloaded at release 0.4.0.

Examples have been updated in this release, enabling keyboard handling for a single list. Examples include a vertically aligned single list and horizontally aligned components with links and buttons for verifying operability.

While code examples are written in JavaScript for brevity, all actual code is written in Typescript and targets React 19.x. Examples use Next.js 16.x, which is not required to run the navigation component.

You can view the requirements for the Single List Keyboard Release along with previous requirements.

Follow along either by downloading the release and running the examples while examining the codebase, or by activating the link accompanying each code snippet to view the full file on GitHub.


This article is fairly long. It contains code and explanations. The code for this release is displayed, and the corresponding GitHub source is linked. Use these anchor links to move to the items of interest.

Content Links

Introduction

This article is the first of three detailing keyboard handling in a universally accessible navigation component; It is also the simplest.

In my experience, many developers seem to expect keyboard handling to be a browser feature, and it's been rare on the contracts I've worked on to see code that handles much custom keyboard input. Keyboard handling works well with native browser elements, but not as well with custom widgets, where developers need to handle code navigation requirements.

While most developers seem to understand that a keyboard is necessary for screen reader users, the fact that many people use a keyboard alongside a screen to navigate isn't always obvious. Users of screen readers have a variety of key combinations they may use to navigate through structures such as headings, links, form elements and tables. Users who navigate the screen with a pointer can simply move their cursor to a specific element and click. Unfortunately, the keyboard flexibility is limited for screen/keyboard users, who are the only ones who must navigate a page linearly. Navigation is not as easy for someone who can see a screen and depends on a keyboard for both input and navigation.

Those who use a screen and keyboard have expectations that developers should not only be aware of, but should strive to match. For those developers who work with a screen, testing how focus moves within a component is actually easy; just use the keyboard. Developers should always ensure a component is fully navigable with the Tab/Shift+Tab key combinations, which are required for both screen and screen reader users, and that any custom keyboard handling for screen users is implemented and works correctly.

To do so, developers need to receive acceptance criteria that detail what happens when a key is used.

When developers don't receive information about keyboard handling requirements when implementing a custom widget, efforts to bring the code into compliance with accessibility standards can run into snags. Keyboard handling can be compromised, and what actually gets implemented might be more focused on the letter of the law than the spirit, as is demonstrated by the following video.

Since the focus of this navigation component is accessibility, the acceptance criteria must specify what happens on each key press.

Return to Content Links

Acceptance Criteria

Single List Keyboard Handling

  • AC 1 - Pressing the HOME Key should take a user to the first item in the current List.
  • AC 2 - Pressing the END Key should take a user to the last item in the current list.
  • AC 3 - Pressing the LEFT-ARROW key should take a user to the previous item in the current list.
  • AC 4 - If focus is already on the first item in the current list, pressing the LEFT-ARROW key should take the user to the last item in the current list.
  • AC 5 - Pressing the RIGHT-ARROW Key should take a user to the next item in the current list.
  • AC 6 - If focus is already on the last item in the current list, pressing the RIGHT-ARROW Key should take a user to the first item in the current list.

The keyboard support section in the Disclosure Navigation pattern in the APG requires interpretation. The guide references moving buttons to buttons and links to links. Both are focusable elements, and depending on where each lies, navigation can move from a button to a link, or from a link to a button, within a single list.

Hand-eye coordination is key in this instance. We expect the cursor to move in the direction indicated by the arrow keys. When the cursor moves to an unexpected location, it's jarring; if it disappears entirely, the site can appear broken.

Return to Content Links

Holding Data in a Provider

Navigation in a sublist requires a focusable element, in this case either a button or a link, to be able to determine its sibling in a list and move according to the acceptance criteria.

Each link and button in a list is rendered in its own component, with no real way to know about, much less access, the others.

Data for each focusable item in a particular list can be stored in a context provider, along with functions to register and retrieve the collective data. Since each focusable element is part of a list, the best data structure to hold the information is an array.

NavigationListProvider

Context providers hold data along with functions for retrieving and setting it. Each list works with its own copy of the provider, and the data in its array consists of each individual link or button element within the list items it contains. Each focusable element, whether a button or a link, can only focus on siblings contained in the list it resides in.

export function NavigationListProvider({ children }) {
  const [state, dispatch] = useReducer(navigationListReducer, {
    items: [],
  });

  const getCurrentListItems = useCallback(() => {
      return state.items;
    }, [state.items]);

  const registerItemInCurrentList =  useCallback((focusableEl) => {
      dispatch({ type: "REGISTER_ITEM", item: focusableEl });
    }, []);

  return (
    <NavigationListContext.Provider
      value={{
        getCurrentListItems,
        registerItemInCurrentList,
      }}
    >
      {children}
    </NavigationListContext.Provider>
  );
}
NavigationListProvider.context = NavigationListContext;

Enter fullscreen mode Exit fullscreen mode

GitHub (release 0.4.0) - NavigationListProvider.tsx

Given the nature of the data, useReducer is a better solution than useState. A reducer still uses the state object but can manage more complex logic. Since state updates to the NavigationListProvider affect multiple components and changes might depend on others, it makes sense to use the provided reducer hook rather than the state hook.

navigationListReducer

export function navigationListReducer(
  state,
  action,
) {
  switch (action.type) {
    case "REGISTER_ITEM": {
      if (state.items.includes(action.item)) return state;
      return { ...state, items: [...state.items, action.item] };
    }
    default: {
      return state;
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

GitHub (release 0.4.0) - navigationListReducer.ts

Like most reducers, actions are held in a switch, even though there is only one action type, REGISTER_ITEM. When called, a focusable element (action.item) is passed through and checked to determine if it already exists in the array. If not, it is added in.

A context provider is not exposed to public components; instead, a hook is used to execute functionality that can be passed through the registration function and to add additional functionality when necessary.

useNavigationList

export function useNavigationList() {
  const navigationListContextObj = use(NavigationListContext);
  const { registerItemInCurrentList } =
    returnTrueElementOrUndefined(
      !!navigationListContextObj,
      navigationListContextObj,
    );

  return {
    registerItemInCurrentList,
  };
}

Enter fullscreen mode Exit fullscreen mode

A common pattern for a hook associated with a context provider is to load the provider's public functions and either use them in the hook or pass them through to another component. For the moment, the registration function is just passed through, so it may be imported into the navigation components.

Return to Content Links

Registering a Focusable Element

In order for the array to be used with keyboard handling, the focusable elements must be registered (added) into the data array. The first step is to wrap each list within its own context provider.

NavigationList

export default function NavigationList({...) {
  const listProps= {...};

  return (
    <NavigationListProvider>
      <List key={`list-$id`} {...listProps}>
        {children}
      </List>
    </NavigationListProvider>
  );
}

Enter fullscreen mode Exit fullscreen mode

GitHub (release 0.4.0) - NavigationList.tsx

With the NavigationListProvider now wrapping the List component, each list item can access any other list item stored in its copy of the data array. It's important to understand that each list maintains its own array of items and cannot access items across lists.

NavigationItem

export default function NavigationItem({...}) {
  const {
    registerItemInCurrentList,
  } = useNavigationList();

  const currentPath = ...;

  const linkRef = useRef(null);

  useEffect(() => {
    if (linkRef.current !== null) {
      registerItemInCurrentList(linkRef.current);
    }
  }, [ linkRef, registerItemInCurrentList]);


  const listItemProps = { ... };

  const linkProps = {
    ... ,
    ref: linkRef,
    ...rest,
  };

  return ( ... );
}

Enter fullscreen mode Exit fullscreen mode

GitHub (release 0.4.0) - NavigationItem.tsx

Code that has already been discussed in previous releases is represented by ellipses; only new code is given.

Focusable elements are stored in ref objects, but reading the element through ref.current can lead to unreliable code during renders. Since the elements held in the ref object are just pointers to the DOM elements, it's safe to extract them and store them separately as read-only.

References to elements are not populated right away; the focusable element, in this case a link, needs to be rendered before the information is available, so the useEffect() that registers the link into the currentlistitems array only runs when the ref. current is not null.

SubNavigation

export default function SubNavigation({...}) {
  const {
    registerItemInCurrentList,
  } = useNavigationList();

  const buttonRef = useRef(null);

  const [isSubListOpen, setIsSubListOpen] = useState(false);

  useEffect(() => {
    if (buttonRef.current !== null) {
        registerItemInCurrentList(buttonRef.current);
    }
  }, [buttonRef, registerItemInCurrentList]);

  const handlePress = () => {
    setIsSubListOpen(!isSubListOpen);
  };
    ...
    ref: buttonRef,
  };
  ...
}

Enter fullscreen mode Exit fullscreen mode

GitHub (release 0.4.0) - SubNavigation.tsx

The code to register a button is almost identical to the code registering a link. The only difference is that the reference being passed is a button element.

Return to Content Links

Keyboard Handler

With the data structure set up and items registered, it's finally time to shift the focus to handling the appropriate keys, and attention turns back to the acceptance criteria.

  • AC 1 - Pressing the HOME Key should take a user to the first item in the current List.
  • AC 2 - Pressing the END Key should take a user to the last item in the current list.
  • AC 3 - Pressing the LEFT-ARROW key should take a user to the previous item in the current list.
  • AC 4 - If focus is already on the first item in the current list, pressing the LEFT-ARROW key should take the user to the last item in the current list.
  • AC 5 - Pressing the RIGHT-ARROW Key should take a user to the next item in the current list.
  • AC 6 - If focus is already on the last item in the current list, pressing the RIGHT-ARROW Key should take a user to the first item in the current list.

The only keys being handled now are home, end, and the left and right arrows. If the focus is on the first element in the list and the left arrow key is pressed, then focus should jump to the last element in the list, and the opposite should happen if the focus is on the last element and the right key is pressed. Focus should move to the first element.

useNavigationList

Since the functionality needs to access data stored in the context provider, the functions are created in the hook.

 export function useNavigationList() {
  const navigationListContextObj = use(NavigationListContext);
  const { getCurrentListItems, registerItemInCurrentList } =
    returnTrueElementOrUndefined(
      !!navigationListContextObj,
      navigationListContextObj,
    );

  const currentListItems = getCurrentListItems();

  const _getCurrentIndex = useCallback(
      (currentlyFocusedEl) => {
        return currentListItems.indexOf(currentlyFocusedEl);
      },
      [currentListItems],
    );

    const shiftFocus= ( focusableEl) => {
      focusableEl.focus({ preventScroll: true });
    };

    const setFirstFocus = () => {
      shiftFocus(currentListItems[0]);
    };

    const setLastFocus = () => {
      shiftFocus(currentListItems[currentListItems.length - 1]);
    };

    const setNextFocus = ( currentlyFocusedEl) => {
      const newIndex = _getCurrentIndex(currentlyFocusedEl) + 1;
      if (newIndex >= currentListItems.length) {
        setFirstFocus();
      } else {
        shiftFocus(currentListItems[newIndex]);
      }
    };

    const setPreviousFocus = ( currentlyFocusedEl) => {
      const newIndex = _getCurrentIndex(currentlyFocusedEl) - 1;
      if (newIndex < 0) {
        setLastFocus();
      } else {
        shiftFocus(currentListItems[newIndex]);
      }
    };

    return {
      registerItemInCurrentList,
      setFirstFocus,
      setLastFocus,
      setNextFocus,
      setPreviousFocus,
      shiftFocus,
    };
} 

Enter fullscreen mode Exit fullscreen mode

GitHub (release 0.4.0) - useNavigationList.tsx

A reminder that the code discussed earlier is represented by ellipses; only the new code is shown.

It's useful to know when a private or public function is used, and I do so by prefacing private functions with an underscore.

To provide a fresh copy of the data to the hook, don't call the data directly; it won't refresh. Instead, a getter is retrieved from the context provider, getCurrentListItems(). You'll note an alias for currentListItems that calls the getter each time. When used this way, the code always returns an updated copy of the array, making it more natural to work with.

A custom function, shiftFocus(), is used instead of implementing focus within each publicly called function to standardize how scrolling is handled. In these cases, scrolling should be prevented when navigation is displayed in a horizontal layout to prevent extraneous shifts when sublists open and close.

The rest of the code is self-explanatory; focus is shifted depending on the key pressed: Home and End, sends focus to the first or last children in the list, while the right or left arrow key sets focus depending on where the currently focused element is; If the focus is at the end of the list when setNextFocus is called, focus then moves to the first element and if the focus is at the beginning of the list when setPreviousFocus is called, focus moves to the end. Otherwise, focus shifts to the respective sibling on either side of the current element.

With the focus functions created, it's time to use them.

NavigationItem

export default function NavigationItem({ ... }) {
  const {
    registerItemInCurrentList,
    setFirstFocus,
    setLastFocus,
    setNextFocus,
    setPreviousFocus,
  } = useNavigationList();
  const currentPath = ...;
  

 const handleKeyDown = (e) => {
      const linkEl = linkRef.current;

      switch (e.key) {
        case Keys.HOME:
        case Keys.END:
        case Keys.LEFT:
        case Keys.RIGHT:
          e.preventDefault();
          e.stopPropagation();
          break;
      }

      handleCommonKeyDown(
        e,
        linkEl,
        setFirstFocus,
        setLastFocus,
        setNextFocus,
        setPreviousFocus,
      );
    };
  

  const linkProps = {
    ...
    onKeyDown: handleKeyDown,
    ref: linkRef,
    ...rest,
  };

  return (..);
}

Enter fullscreen mode Exit fullscreen mode

The setters from the hook are retrieved, and a handleKeyboard function is created. Rather than setting preventDefault and stopPropagation in each case statement, I use a switch that sets them on all the keys at once. And because both links and buttons handle these keys in the same way, a common keyboard handler is created and called rather than duplicating the code across components.

When calling a function outside the render, everything necessary must be passed in, including all the hook calls.

handleCommonKeyDown

export const handleCommonKeyDown = (
  e,
  currentlyFocusedEl,
  setFirstFocus,
  setLastFocus,
  setNextFocus,
  setPreviousFocus
) => {
  switch (e.key) {
    case Keys.HOME:
      setFirstFocus();
      break;
    case Keys.END:
      setLastFocus();
      break;
    case Keys.LEFT:
      setPreviousFocus(currentlyFocusedEl);
      break;
    case Keys.RIGHT:
      setNextFocus(currentlyFocusedEl);
      break;
  }
};

Enter fullscreen mode Exit fullscreen mode

GitHub (release 0.4.0) - handleCommonKeyDown.tsx

The common key handler is quite straightforward; each specific key triggers a function in the hook to decide the next item to shift to.

SubNavigation

export function SubNavigation({...}) {
    const {
        currentListItems,
        parentRef,
        registerListItem,
        setFirstFocus,
        setLastFocus,
        setNextFocus,
        setPreviousFocus
    } = useNavigationList();
   const {
        registerListItem, 
        setFirstFocus, 
        setLastFocus, 
        setNextFocus, 
        setPreviousFocus} = useNavigationList();

   const buttonRef = useRef(null);
    ...

  useEffect(() => {
    const currentButtonEl = buttonRef.current;
    if (currentButtonEl !== null) {
       registerItemInCurrentList(currentButtonEl);
    }
  } , [buttonEl, buttonRef, isSubListOpen, registerItemInCurrentList]);

    const handleKeyDown = (e) => {
        const buttonEl = buttonRef.current;

        switch (e.key) {
            case Keys.HOME:
            case Keys.END:
            case Keys.LEFT:
            case Keys.RIGHT:
                e.preventDefault();
                break;
        }

        handleCommonKeyDown(
          e, 
          buttonEl, 
          setFirstFocus, 
          setLastFocus, 
          setNextFocus, 
          setPreviousFocus
       );

    ...

    const buttonProps: ButtonProps = {
        ...
        onKeyDown: handleKeyDown,
        onPress: handlePress,
        ref: buttonRef,
        ...
    };

    ...
    return ( ...)
}

Enter fullscreen mode Exit fullscreen mode

GitHub (release 0.4.0) - SubNavigation.tsx

Notice how stopPropagation() is not included in the top switch. This is because the button component I use from react-aria-components already calls stopPropagation() itself, so I don't need to set it. This may differ from the button component you are working with. Otherwise, the code for SubNavigation is similar to that already seen in NavigationItem.

Return to Content Links

Summary

Handling link references to multiple components becomes straightforward when a provider holds the data and the functions that call and manipulate it. A hook passes through necessary provider functions as well as its own to provide keyboard handling within a single list.

With all the code added to support single-list keyboard handling, the question arises: Does it work?

Part of the issue with demonstrating keyboard handling is the inability to show which key is being used as focus moves. It's one of the reasons keyboard accessibility is so hard to demonstrate during sprint demos. I'll be providing videos I make that detail what I am doing and which keys I'm pressing as I guide you through the enhancements.

I'll be back with another article soon, focusing on CSS design and applying a layout to the default horizontal navigation.