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

推荐订阅源

T
Threatpost
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
Visual Studio Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Forbes - Security
Forbes - Security
人人都是产品经理
人人都是产品经理
N
Netflix TechBlog - Medium
Recent Commits to openclaw:main
Recent Commits to openclaw:main
WordPress大学
WordPress大学
Webroot Blog
Webroot Blog
Jina AI
Jina AI
H
Hacker News: Front Page
Attack and Defense Labs
Attack and Defense Labs
T
Troy Hunt's Blog
TaoSecurity Blog
TaoSecurity Blog
AI
AI
Hacker News - Newest:
Hacker News - Newest: "LLM"
Google Online Security Blog
Google Online Security Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Help Net Security
Help Net Security
美团技术团队
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 叶小钗
P
Privacy International News Feed
A
Arctic Wolf
IT之家
IT之家
云风的 BLOG
云风的 BLOG
S
Security Affairs
Simon Willison's Weblog
Simon Willison's Weblog
The Cloudflare Blog
博客园 - 司徒正美
Vercel News
Vercel News
C
Cyber Attacks, Cyber Crime and Cyber Security
SecWiki News
SecWiki News
K
Kaspersky official blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
N
News and Events Feed by Topic
S
Schneier on Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
有赞技术团队
有赞技术团队
PCI Perspectives
PCI Perspectives
F
Fortinet All Blogs
T
Tenable Blog
Spread Privacy
Spread Privacy
T
The Blog of Author Tim Ferriss
S
Securelist
L
LangChain Blog
Latest news
Latest news
Cloudbric
Cloudbric
博客园 - 三生石上(FineUI控件)

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Setting Up a Controlled Component
ShaynaProductions · 2026-06-25 · 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 development article, I finally completed the requirements for delivering an uncontrolled navigation component with a horizontal layout; with full keyboard functionality, along with adding the niceties of closing sublists when lists are closed and making sure any open sublist on the top row closes when focus shifts away from it.

Rather than write an entirely new component for a mobile version, I'm going to modify the existing code. This first article outlines the steps necessary to set up and work with a controlled component.
-—

Note: This article is one of a series demonstrating building 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 builds on the previous one 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 1.0.0. Links in the article will take you to the proper file in the tagged GitHub Repository.

Because the code for this release is scattered across components, line numbers are added to make it easier to locate in the linked GitHub file. Line numbers are also provided for those who would like to follow along with a downloaded copy.

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

You can view the requirements for the Controlled Components Release along with previous requirements.


Content Links

  • Introduction
  • Acceptance Criteria
  • Controlled Component
  • Mobile Navigation
  • Data Setup
  • Close on Escape
  • Passthrough When Closed
  • Component Closing
  • Summary

Introduction

Everything up to this point has been built with the default requirement of an uncontrolled component. As a desktop horizontally aligned navigation component, there hasn't been a need to control the component's visibility. If you've been following these articles from the start, you might recall that one of the requirements was for the ability to use the same component as a mobile version, which means a small icon button, typically shown with three horizontal lines, colloquially referred to as a "hamburger menu," which, when triggered, opens a vertically aligned navigation component.

The decision to expand the navigation component to work in a more constrained screen environment means less work, since much of the code already written would have to be replicated.

When adding more or changing existing functionality, accessibility still needs to be considered. What works in one situation may need to be refactored for another. And that's the case when considering how to add controlled-component functionality to an uncontrolled component.

The remaining issues are documented in this video.

Return to Content Links

Acceptance Criteria

Controlled Components
AC 1 - When the controlling element is activated, and the navigation component is closed, the navigation component should open and set focus on the first child in the first row.
AC 2 - When Escape is pressed anywhere in a controlled navigation component, all open sublists close and focus shifts to the controlling element.
AC 3 - When tabbing past the controlling element when navigation is closed, focus skips over the navigation component to the next focusable element.
AC 4 - When SHIFT+Tab is applied to a focusable element just past the closed navigation component, focus skips the component and lands on the controlling element.
AC 5 - When tabbing out of a controlled component, the component should close.

Rather than staying on the icon button once navigation opens, focus should shift to the first focusable element in the navigation component. When the component is closed by pressing Escape, focus should shift back to the icon button.

The difficulty, once again, stems from the architectural decision to place all navigation links within the DOM to facilitate screen reader access via the various elements list (or rotor for VoiceOver). When the navigation component is closed, pressing the Tab key should move focus from the icon button to the first focusable element on the other side of the component. Conversely, if Shift+Tab is used, focus should move from the focusable element on the other side, back to the icon button.

The last acceptance criteria requires the component to close when an outside event is consumed. Since the navigation component also provides an OutsideEventListener, the duplication must be handled so that only one listening component is active.

Return to Content Links

Controlled Component

A component is considered controlled when its states and behaviors are managed by the parent component rather than relying on its own internal code. In this case, a MobileNavigation component toggles its open state when the icon button is activated.

Interaction between the controlling element and the navigation component requires handling focus and determining whether the navigation component is open or closed.

Return to Content Links

<MobileNavigation />

AC 1

When the controlling element is activated, and the navigation component is closed, the navigation component should open and set focus on the first child in the first row.

export function MobileNavigation({
  children,
  cx,
  label,
  skipName = "skip",
  ...rest
}) {
  const [open, setOpen] = useState(false);
  const buttonRef = useRef(null);
  const closeNavigation = () => {
    setOpen(false);
  };

  const handleFocus = () => {
    if (open) {
      closeNavigation();
    }
  };

  const handleKeyDown = (e) => {
    switch (e.key) {
      case Keys.TAB:
        /* istanbul ignore else */
        if (!open) {
          const nextEl = getFocusableElementFromDOM(buttonRef.current, "next");
          nextEl.setAttribute(`data-${skipName}`, "true");
        }
        break;
    }
  };

  const handlePress = (e) => {
    if (!open) {
      const nextEl = getFocusableElementFromDOM(buttonRef.current, "next");
      nextEl.focus({ preventScroll: true });
    }
    setOpen(!open);
  };
  const buttonProps = {
    "aria-expanded": open,
    "aria-controls": "mobile-menu",
    "aria-label": "Navigation Menu",
    id: "mobile",
    isOpen: open,
    onFocus: handleFocus,
    onKeyDown: handleKeyDown,
    onPress: handlePress,
    ref: buttonRef,
    style: { "--component-item-size": 24 } as CSSProperties,
  };

  const iconProps: IconProps = {
    IconComponent: MenuIcon,
    isSilent: true,
  };

  const navigationProps = {
    ...rest,
    cx: cx,
    controllingRef: buttonRef,
    id: "mobile-menu",
    isOpen: open,
    label: label,
    orientation: "vertical" as Orientation,
  };
  return (
    <Box cx="mobile-navigation">
      <Button {...buttonProps}>
        <Icon {...iconProps} />
      </Button>
      <Navigation {...navigationProps}>{children}</Navigation>
    </Box>
  );
}

GitHub (release 1.0.0) - MobileNavigation.tsx

The MobileNavigation component contains and handles the icon button along with the navigation component, setting up event handlers for onFocus (closing the navigation component if it is set to open) and onPress, which, at the moment, simply toggles the open state.

Notice that the button and open state are passed into the Navigation component, along with a new prop, skipName, that creates a unified data attribute on a focusable element to handle focus when the navigation component should be skipped entirely.

In this case, when the navigation component is closed, and a keyboard user presses the Tab key on the icon button, focus should skip the actual component and move to the element on the other side. The data name could be hardcoded, and indeed, there is a default, but it's always a good idea when setting up an interaction between components to allow the data name to be agreed upon by both parties.

When the mobile navigation component sets the open state to true, focus should shift to the first focusable element within the navigation component. Pushing focus into the component helps both screen/keyboard and screenreader/keyboard users.

Return to Content Links

Data Setup

A controlled navigation component needs to keep track of more information than an uncontrolled component. The controllingRef needs to be added to the context provider and sent to the NavigationWrapper, along with the open state.

export default function Navigation({
  children,
  controllingRef,
  cx,
  isOpen = true,
  label,
  orientation = "horizontal",
  SkipName,
  ...rest
}: NavigationProps) {
    const parentRef = useRef(null);
    const uncontrolledRef = useRef(null);
    const mergedRef = useMergedRef(uncontrolledRef, controllingRef,);

    const navigationContextProps: NavigationContextStoredValueProps = {
        data: {
            controllingRef: mergedRef, isSubListOpen: isOpen, storedParentEl: null, storedList: [],
        }, config: {skipName: skipName || "skip"},
    };
}

GitHub (release 1.0.0) - Navigation.tsx

While the controllingRef is managed in the data object, a new object is also being passed to the context provider, config. Any elements in the config object should be considered stable and non-changeable.

  const navigationListProps: NavigationListProps = {
    ...rest,
    isOpen,
    orientation,
    parentRef,
  };

  const navigationWrapperProps = {
    cx,
    controllingRef,
    isOpen,
    label: label,
  };

  return (
    <NavigationProvider value={navigationContextProps}>
      <NavigationWrapper {...navigationWrapperProps}>
        <NavigationList {...navigationListProps}>{children}</NavigationList>
      </NavigationWrapper>
    </NavigationProvider>
  );

GitHub (release 1.0.0) - Navigation.tsx

The controlling reference needs to be stored and retrieved to shift focus back to the button when the Escape key is pressed. The controlling reference is not the parentRef; for a top-row navigation object, the parentRef will always return as null and be passed through to the top-level list. The controlling reference is not only passed to the data object but also to the NavigationWrapper.

NavigationProvider.tsx

Before anything, the controllingRef needs to be set up in the reducer. The config object is simply destructured and is ready to return information.

export function NavigationProvider({ children, value }) {
  const { data, config } = value;
  ...

  const [state, dispatch] = useReducer(navigationReducer, {
    navigationArray: [navigationObject],
    isComponentActive: false,
    controllingRef: data.controllingRef,
  });
  ...
  const getControllingElement = useCallback(() => {
    return returnElementFromRefObject(state.controllingRef);
  }, [state.controllingRef]);

const isComponentControlled = useCallback(() => {
  return getControllingElement() !== null;
},[getControllingElement]);

const getSkipName = useCallback(() => {
  return config.skipName;
}, [config.skipName]);

 ...
  return (
    <NavigationContext.Provider
      value={{
        getControllingElement,
        ...,
        isComponentControlled,
        ...
      }}
    >
      {children}
    </NavigationContext.Provider>
  );
}

GitHub (release 1.0.0) - NavigationProvider.tsx

Three functions are added to the context provider: getControllingElement, which returns the actual element stored; isComponentControlled, which returns false if the controlling element is null; and getSkipName, which returns the value from the config object.

The controllingRef is added to the reducer. Since the controlling reference will never change once the mobile navigation button renders, there's no need to add extra code to the reducer.

With the data set up, it's time to implement the functionality.

Return to Content Links

Close on Escape

AC 2

When Escape is pressed anywhere in a controlled navigation component, all open sublists close and focus shifts to the controlling element.

The next criterion to implement is also the easiest; all that's necessary is to modify closeComponentWithFocus inside the useNavigation hook.

const closeComponentWithFocus = (focusedEl) => {
  closeComponent();

  const controllingElement = getControllingElement();
  return controllingElement !== null
    ? controllingElement
    : _getTopRowElement(focusedEl);
};

GitHub (release 1.0.0) - useNavigation.tsx closeComponentWithFocus() - Line 310

When Escape is pressed, and the component is controlled, focus should shift to the controlling element rather than the top-level parent of the currently focused item.

Return to Content Links

Passthrough When Closed

AC 3

When tabbing past the controlling element after navigation is closed, focus skips the navigation component and moves to the next focusable element.

As a result of the architectural decision to keep all links in the DOM, tabbing on the mobile button should pass through the component to the focusable element on the other side. Realistically, the mobile navigation has no idea how to do that; it has no way of reaching into the navigation component to find the last element and shift focus beyond the navigation component when tabbing off its icon button.

const handleKeyDown = (e) => {
  switch (e.key) {
    case Keys.TAB:
      if (!open) {
        const nextEl = getFocusableElementFromDOM(buttonRef.current, "next");
        nextEl.setAttribute(`data-${skipName}`, "true");
      }
      break;
  }
};

GitHub (release 1.0.0) - MobileNavigation.tsx - handleKeyDown Line 35.

The challenge is how to communicate to the navigation component that, when it receives focus, it should immediately shift outside the component. The solution is to set a data attribute on the first focusable element in the component that instructs the element to shift. The skipName is the same one passed through to the navigation component.

const _handlePassthroughNavigation = (focusedEl) => {
  const shouldSkip = focusedEl.getAttribute(`data-${getSkipName()}`);

  let returnEl;
  if (shouldSkip) {
    const lastElement = _getLastElementInComponent();
    focusedEl.removeAttribute(`data-${getSkipName()}`);
    returnEl = getFocusableElementFromDOM(lastElement, "next");
  }

  return returnEl;
};

const _handleTopRowItemFocus = (focusedEl) => {
  let returnEl: FocusableElementType | undefined;
  if (isComponentActive()) {
    _closeOpenSiblings(focusedEl);
  }

  if (isComponentControlled() && _isFirstElementInComponent(focusedEl)) {
    returnEl = _handlePassthroughNavigation(focusedEl);
  }
  if (!isComponentActive()) {
    setIsComponentActive(true);
  }

  return returnEl;
};

GitHub (release 1.0.0) - useNavigation.tsx- _handlePassthroughNavigation - Line 218

If a skip data attribute exists on the focused element, the last component element is found and returned to shift outside the component. The call to the _handlePassthroughNavigation function is made only if the component is controlled, and focus is currently on the first element in the top row.

The issue with Tabbing through a closed navigation component is now resolved.

AC 4

When SHIFT+Tab is applied to a focusable element just past the closed navigation component, focus skips the component and lands on the controlling element.

Just as a check was made when the currently focused element was the first focusable element in a component, shifting focus to the mobile button needs to happen when focus is on the last focusable element in the component.

const _handleLastChildFocus = (focusedEl) => {
  const { isSubListOpen } = _getNavigationObjectByListElement(focusedEl);
  if (!isSubListOpen) {
    setIsComponentActive(false);
    if (isComponentControlled()) {
      return getControllingElement();
    } else {
      return _getLastElementInTopRow(focusedEl);
    }
  }
};

GitHub (release 1.0.0) - useNavigation.tsx - _handleLastChildFocus - Line 280

Code has been added to check if the component is controlled only when the sublist the focusable element is a part of is not open. If this is the case, the controlling element is returned rather than the last element in the top row.

Return to Content Links

Component Closing

AC 5

When tabbing out of a controlled component, the component should close.

When the focus shifts outside of the mobile navigation component, the component should become invisible on the screen. This requires an OutsideEventListener in the Mobile component to capture clicks and blurs outside the component and close it. But the uncontrolled component also has the same listener, and having two of them can create issues.

if (isComponentControlled()) {
  return <nav {...navigationProps}>{children}</nav>;
} else {
  return (
    <OutsideEventListener {...outsideElementListenerProps}>
      <nav {...navigationProps}>{children}</nav>
    </OutsideEventListener>
  );
}

GitHub (release 1.0.0) - NavigationWrapper.tsx - Line 49

The solution, of course, is only to load the listener in the NavigationWrapper when the component is uncontrolled.

export function MobileNavigation({ ... }){
  ...
  const outsideEventProps = {
    onOutsideEvent: returnTrueElementOrUndefined(open, closeNavigation),
  };

  ...
  return (
    <OutsideEventListener {...outsideEventProps}>
      <Box cx="mobile-navigation">
        <Button {...buttonProps}>
          <Icon {...iconProps} />
        </Button>
        <Navigation {...navigationProps}>{children}</Navigation>
      </Box>
    </OutsideEventListener>
  );
}

GitHub (release 1.0.0) - MobileNavigation.tsx - Line 74

Adding the OutsideEventListener and requiring the close function to be called only when the navigation component is open solves the last issue when a component is controlled.

And with that, the last of the issues surrounding a controlled component are solved.

Return to Content Links

Summary

Adding functionality to allow a component to be controlled requires a new look at keyboard functionality and confirming keyboard focus works both when the controlled component is on the screen and when it is not. It means making sure there are no conflicts with competing utilities when the component is controlled versus when it is not.

The next article will address the remaining issues with the vertical layout, and the navigation component will be ready for use in production.