Skip to content

Conversation

@404Dealer
Copy link
Contributor

@404Dealer 404Dealer commented Oct 17, 2025

Problem:
The A11y panel UI state (selected tab and highlight toggle) is lost when navigating Stories → Docs → Stories. See #32634.

Solution:
Persist the A11y panel UI state with useAddonState so preferences survive navigation between Stories and Docs.

  • Persisted UI state: selected tab and highlighted are stored under a dedicated key across view-mode changes.
  • State initialization: on initial mount, the a11ySelection query param overrides any persisted state for deep-linking. The param is cleared after it’s read.
  • Minimal impact: only UI state handling changed; no changes to A11y checks or core logic.

Implementation details:

  • New key:
    • UI_STATE_ID = ${ADDON_ID}/ui
  • State management:
    const [uiState, setUiState] = useAddonState(UI_STATE_ID, {
      highlighted: false,
      tab: RuleType.VIOLATION,
    });
    
    const [tab, setTabState] = useState<RuleType>(() => {
      const [type] = a11ySelection?.split('.') ?? [];
      return type && Object.values(RuleType).includes(type as RuleType)
        ? (type as RuleType)
        : uiState.tab;
    });
    
    const [highlighted, setHighlighted] = useState<boolean>(() =>
      a11ySelection ? true : uiState.highlighted
    );
    // a11ySelection precedence is applied once on mount; effect depends only on a11ySelection

Files touched:

  • code/addons/a11y/src/constants.ts
  • code/addons/a11y/src/components/A11yContext.tsx
  • code/addons/a11y/src/components/A11yContext.test.tsx

Test coverage:

  • Unit tests:
    • Highlight toggle persists over re-mount
    • Tab persists over re-mount
    • a11ySelection overrides persisted state on initial mount

Manual testing:

  • yarn start → http://localhost:6006
  • Enable highlights, switch tabs, go to Docs, return — state persists

Risks/considerations:

  • Unique useAddonState key avoids collisions
  • a11ySelection takes precedence once and is cleared
  • Only small UI booleans/enums are persisted

Fixes:

Summary by CodeRabbit

  • New Features
    • Persisted UI state now stored under a dedicated UI namespace; new Status type added to the public API for addon lifecycle.
  • Refactor
    • Internal state consolidated into a single composite addon state (ui, results, error, status); tab and highlights driven from ui and exposed via wrapper APIs.
  • Tests
    • Tests updated to reflect the composite state shape and adjusted persistence/remount behavior.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 17, 2025

📝 Walkthrough

Walkthrough

Consolidates addon state into a persisted composite object { ui, results, error, status }, introduces UI_STATE_ID constant and exported Status type, moves tab/highlight into ui, refactors A11yContext and manager to use the composite state, and updates tests/mocks to the new shape.

Changes

Cohort / File(s) Summary
Constants
code/addons/a11y/src/constants.ts
Added UI_STATE_ID export set to ${ADDON_ID}/ui.
Types
code/addons/a11y/src/types.ts
Added exported Status union type: `'initial'
Core context
code/addons/a11y/src/components/A11yContext.tsx
Replaced separate state fields with a composite persisted state { ui, results, error, status }; ui holds highlighted and tab (RuleType). Exposed tab/setTab and status/setStatus as wrappers over the composite setState. Init and persist UI from a11ySelection on mount.
Context tests
code/addons/a11y/src/components/A11yContext.test.tsx
Added imports (UI_STATE_ID, RuleType) and adapted tests to the composite state shape (mocked addon state now includes results and ui where appropriate).
Manager
code/addons/a11y/src/manager.tsx
useAddonState default/typing updated to composite shape; derives violationsNb and incompleteNb from results; imports RuleType and Status; default ui.tab set to RuleType.VIOLATION.
Manager tests
code/addons/a11y/src/manager.test.tsx
Mocks updated: useAddonState returns structured object with results (e.g., { results: { violations: [...], incomplete: [...] } }) instead of top-level arrays/properties.
Manifest
package.json
Listed in manifest summary; no behavioral changes noted.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant A11yContext
    participant Store as AddonStateStore

    User->>A11yContext: Mount
    A11yContext->>Store: useAddonState(UI_STATE_ID) — read persisted { ui?, results?, error?, status? }
    Store-->>A11yContext: persisted state

    alt a11ySelection present
        A11yContext->>A11yContext: initialize ui.tab / ui.highlighted from a11ySelection
        A11yContext->>Store: persist composite state (UI_STATE_ID)
        Store-->>A11yContext: confirmation
    else
        A11yContext->>A11yContext: use persisted or default ui
    end

    User->>A11yContext: Toggle highlight / Switch tab / Update status
    A11yContext->>A11yContext: update composite state via setState wrapper
    A11yContext->>Store: persist composite state
    Store-->>A11yContext: confirmation
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Potential focus areas:

  • code/addons/a11y/src/components/A11yContext.tsx — composite state merging, effect dependencies, and wrapper APIs (setTab, setStatus).
  • code/addons/a11y/src/manager.tsx — new state typing and deriving counts from results.
  • code/addons/a11y/src/types.ts — Status integration across imports/usages.
  • Tests (A11yContext.test.tsx, manager.test.tsx) — mocks and assertions adjusted to the composite state shape and persistence key.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4147d30 and 83f36d8.

📒 Files selected for processing (2)
  • code/addons/a11y/src/components/A11yContext.tsx (13 hunks)
  • code/addons/a11y/src/manager.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • code/addons/a11y/src/components/A11yContext.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,jsx,json,html,ts,tsx,mjs}: Run Prettier formatting on changed files before committing
Run ESLint on changed files and fix all errors/warnings before committing (use yarn lint:js:cmd <file>)

Files:

  • code/addons/a11y/src/manager.tsx
**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Export functions from modules when they need to be unit-tested

Files:

  • code/addons/a11y/src/manager.tsx
code/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

In application code, use Storybook loggers instead of console.* (client code: storybook/internal/client-logger; server code: storybook/internal/node-logger)

Files:

  • code/addons/a11y/src/manager.tsx
{code/**,scripts/**}/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/addons/a11y/src/manager.tsx
🧬 Code graph analysis (1)
code/addons/a11y/src/manager.tsx (2)
code/core/src/manager-api/root.tsx (2)
  • useStorybookApi (294-297)
  • useAddonState (455-457)
code/addons/a11y/src/types.ts (4)
  • RuleType (33-37)
  • RuleType (39-39)
  • EnhancedResults (49-53)
  • Status (60-67)
🔇 Additional comments (2)
code/addons/a11y/src/manager.tsx (2)

11-12: LGTM! Type imports aligned with new state shape.

The Status and RuleType imports are necessary for the composite addon state structure and are used correctly in the type annotations and default state initialization.


31-32: Good use of nullish coalescing and optional chaining.

The code correctly uses ?? instead of || and applies optional chaining to safely access nested properties. This will work correctly once the unsafe destructuring on line 17 is fixed.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
code/addons/a11y/src/components/A11yContext.test.tsx (3)

16-21: Align mocks with repo test guidelines

Use spy: true and move ad‑hoc mock impls into beforeEach to reduce drift and keep tests consistent.

As per coding guidelines, update the module mock:

-vi.mock('storybook/manager-api');
+vi.mock('storybook/manager-api', { spy: true });

And hoist custom mock implementations currently defined inside individual tests (e.g., useAddonState store-backed impl) into a helper or the test's beforeEach when possible. Based on guidelines.


338-380: Highlight state persistence test reads well; consider DRY helper

Test is correct and representative. To avoid duplication, extract the store‑backed useAddonState mock into a small helper used by persistence tests.


426-473: Deep‑link precedence test: add assertion that persistence updated

Great verification of precedence and query param clearing. Also assert that the persisted store reflects the override.

Apply:

   expect(getByTestId('hl').textContent).toBe('true');
   expect(getByTestId('tab').textContent).toBe('violations');
   expect(setQueryParams).toHaveBeenCalledWith({ a11ySelection: '' });
+  // Persisted UI state should also reflect the override
+  expect(store.get(UI_STATE_ID)).toEqual({ highlighted: true, tab: RuleType.VIOLATION });
code/addons/a11y/src/components/A11yContext.tsx (1)

325-339: Avoid double writes; make deep‑link persistence atomic

Current effect sets highlighted, calls setTab (which also persists), and then calls setUiState again. Combine into a single UI state write and set local tab only once. Behavior unchanged; fewer rerenders.

Apply:

-  useEffect(() => {
-    if (!a11ySelection) {
-      return;
-    }
-    setHighlighted(true);
-    const [type] = a11ySelection.split('.') ?? [];
-    if (type && Object.values(RuleType).includes(type as RuleType)) {
-      setTab(type as RuleType);
-    }
-    setUiState((prev) => ({ ...prev, highlighted: true }));
-    // We intentionally do not include setHighlighted/setTab/setUiState in deps to avoid loops
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [a11ySelection]);
+  useEffect(() => {
+    if (!a11ySelection) return;
+    setHighlighted(true);
+    const [type] = a11ySelection.split('.') ?? [];
+    const validType = type && Object.values(RuleType).includes(type as RuleType)
+      ? (type as RuleType)
+      : undefined;
+    if (validType) setTabState(validType);
+    setUiState((prev) => ({ ...prev, highlighted: true, tab: validType ?? prev.tab }));
+    // Intentionally omit setters to avoid loops
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [a11ySelection]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f47e861 and 9ec584f.

📒 Files selected for processing (3)
  • code/addons/a11y/src/components/A11yContext.test.tsx (2 hunks)
  • code/addons/a11y/src/components/A11yContext.tsx (4 hunks)
  • code/addons/a11y/src/constants.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,jsx,json,html,ts,tsx,mjs}: Run Prettier formatting on changed files before committing
Run ESLint on changed files and fix all errors/warnings before committing (use yarn lint:js:cmd <file>)

Files:

  • code/addons/a11y/src/constants.ts
  • code/addons/a11y/src/components/A11yContext.tsx
  • code/addons/a11y/src/components/A11yContext.test.tsx
**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Export functions from modules when they need to be unit-tested

Files:

  • code/addons/a11y/src/constants.ts
  • code/addons/a11y/src/components/A11yContext.tsx
  • code/addons/a11y/src/components/A11yContext.test.tsx
code/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

In application code, use Storybook loggers instead of console.* (client code: storybook/internal/client-logger; server code: storybook/internal/node-logger)

Files:

  • code/addons/a11y/src/constants.ts
  • code/addons/a11y/src/components/A11yContext.tsx
  • code/addons/a11y/src/components/A11yContext.test.tsx
{code/**,scripts/**}/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/addons/a11y/src/constants.ts
  • code/addons/a11y/src/components/A11yContext.tsx
  • code/addons/a11y/src/components/A11yContext.test.tsx
code/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

code/**/*.{test,spec}.{ts,tsx}: Place all test files under the code/ directory
Name test files as *.test.ts, *.test.tsx, *.spec.ts, or *.spec.tsx

Files:

  • code/addons/a11y/src/components/A11yContext.test.tsx
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/spy-mocking.mdc)

**/*.test.{ts,tsx,js,jsx}: Use vi.mock() with the spy: true option for all package and file mocks in Vitest tests
Place all mocks at the top of the test file before any test cases
Use vi.mocked() to type and access mocked functions
Implement mock behaviors in beforeEach blocks
Mock all required dependencies that the test subject uses
Mock implementations should be placed in beforeEach blocks
Each mock implementation should return a Promise for async functions
Mock implementations should match the expected return type of the original function
Use vi.mocked() to access and implement mock behaviors
Mock all required properties and methods that the test subject uses
Avoid direct function mocking without vi.mocked()
Avoid mock implementations outside of beforeEach blocks
Avoid mocking without the spy: true option
Avoid inline mock implementations within test cases
Avoid mocking only a subset of required dependencies
Mock at the highest level of abstraction needed
Keep mock implementations simple and focused
Use type-safe mocking with vi.mocked()
Document complex mock behaviors
Group related mocks together

Files:

  • code/addons/a11y/src/components/A11yContext.test.tsx
**/*.@(test|spec).{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.@(test|spec).{ts,tsx,js,jsx}: Unit tests should import and execute the functions under test rather than only asserting on syntax patterns
Mock external dependencies in tests using vi.mock() (e.g., filesystem, loggers)

Files:

  • code/addons/a11y/src/components/A11yContext.test.tsx
🧬 Code graph analysis (2)
code/addons/a11y/src/components/A11yContext.tsx (2)
code/core/src/manager-api/root.tsx (1)
  • useAddonState (455-457)
code/addons/a11y/src/constants.ts (1)
  • UI_STATE_ID (4-4)
code/addons/a11y/src/components/A11yContext.test.tsx (2)
code/addons/a11y/src/components/A11yContext.tsx (2)
  • useA11yContext (488-488)
  • A11yContextProvider (97-486)
code/addons/a11y/src/constants.ts (1)
  • UI_STATE_ID (4-4)
🔇 Additional comments (3)
code/addons/a11y/src/constants.ts (1)

4-4: LGTM: dedicated addon state key

Clear, unique namespace for UI persistence. No concerns.

code/addons/a11y/src/components/A11yContext.test.tsx (1)

382-424: Tab persistence over remounts is verified correctly

Solid coverage; no issues found.

code/addons/a11y/src/components/A11yContext.tsx (1)

116-122: Good: unified, persisted UI state for tab/highlight

Using useAddonState with a typed shape and persisting in both handlers ensures state survives remounts. Nicely done.

Also applies to: 153-166

@vanessayuenn vanessayuenn changed the title fix(a11y): persist tab/highlight across docs navigation (#32634) A11y: Persist tab/highlight across docs navigation (#32634) Oct 20, 2025
@vanessayuenn vanessayuenn changed the title A11y: Persist tab/highlight across docs navigation (#32634) A11y: Persist tab/highlight across docs navigation Oct 20, 2025
@404Dealer
Copy link
Contributor Author

After merging the latest 'next', I noticed ci/circleci: e2e-ui and normal checks are failing, though my previous commit passed. I'm happy to debug and resolve this if these failures relate to my code—just let me know if you'd like me to proceed, or if the main branch is in flux and I should wait. If needed, I'm also happy to post more details on the errors to help clarify whether it's my changes or the branch state.

@404Dealer
Copy link
Contributor Author

404Dealer commented Oct 23, 2025

Hey @yannbf! Friendly ping here. This fixes #32634, the A11y panel now keeps its selected tab and highlight across Stories/Docs.

Added tests and kept it scoped to UI state. First time contributing to open source, so happy to tweak anything and I welcome any feedback. Thanks!

@yannbf yannbf assigned ndelangen and unassigned yannbf Oct 27, 2025
@nx-cloud
Copy link

nx-cloud bot commented Oct 27, 2025

View your CI Pipeline Execution ↗ for commit 83f36d8

Command Status Duration Result
nx run-many -t build --parallel=3 ✅ Succeeded 46s View ↗

☁️ Nx Cloud last updated this comment at 2025-10-27 12:52:06 UTC

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
code/addons/a11y/src/components/A11yContext.tsx (1)

355-377: Optional chaining gaps can crash when results are undefined

results?.[ui.tab as RuleType].map/flatMap(...) accesses .map/.flatMap on possibly undefined, causing a runtime TypeError. Use optional chaining on the array, too.

Apply this diff:

-        menu: results?.[ui.tab as RuleType].map<HighlightMenuItem[]>((result) => {
+        menu: results?.[ui.tab as RuleType]?.map<HighlightMenuItem[]>((result) => {
           const selectors = result.nodes
             .flatMap((n) => n.target)
             .map(String)
             .filter((e) => selected.includes(e));
           return [
             {
-              id: `${ui.tab}.${result.id}:info`,
+              id: `${ui.tab}.${result.id}:info`,
               title: getTitleForAxeResult(result),
               description: getFriendlySummaryForAxeResult(result),
               selectors,
             },
             {
-              id: `${ui.tab}.${result.id}`,
+              id: `${ui.tab}.${result.id}`,
               iconLeft: 'info',
               iconRight: 'shareAlt',
               title: 'Learn how to resolve this violation',
               clickEvent: EVENTS.SELECT,
               selectors,
             },
           ];
         }),
       });
     }
-    const others = results?.[ui.tab as RuleType]
-      .flatMap((r) => r.nodes.flatMap((n) => n.target).map(String))
+    const others = results?.[ui.tab as RuleType]
+      ?.flatMap((r) => r.nodes.flatMap((n) => n.target).map(String))
       .filter((e) => ![...unhighlightedSelectors, ...selected].includes(e));
     if (others?.length) {
       emit(HIGHLIGHT, {
         id: `${ADDON_ID}/others`,
         selectors: others,
         styles: {
-          outline: `1px solid color-mix(in srgb, ${colorsByType[ui.tab]}, transparent 30%)`,
-          backgroundColor: `color-mix(in srgb, ${colorsByType[ui.tab]}, transparent 60%)`,
+          outline: `1px solid color-mix(in srgb, ${colorsByType[ui.tab]}, transparent 30%)`,
+          backgroundColor: `color-mix(in srgb, ${colorsByType[ui.tab]}, transparent 60%)`,
         },
         hoverStyles: {
           outlineWidth: '2px',
         },
         focusStyles: {
           backgroundColor: 'transparent',
         },
-        menu: results?.[ui.tab as RuleType].map<HighlightMenuItem[]>((result) => {
+        menu: results?.[ui.tab as RuleType]?.map<HighlightMenuItem[]>((result) => {

Also applies to: 380-390, 397-419

🧹 Nitpick comments (3)
code/addons/a11y/src/components/A11yContext.tsx (3)

99-105: Avoid side‑effects inside useMemo and tighten deep‑link parsing

  • api.setQueryParams inside useMemo is a side‑effect. Move to useEffect for safety (React strict mode double renders can otherwise bite).
  • The regex uses unescaped dots; it matches too broadly and may accept invalid values.

Apply this diff:

-  const a11ySelection = useMemo(() => {
-    const value = api.getQueryParam('a11ySelection');
-    if (value) {
-      api.setQueryParams({ a11ySelection: '' });
-    }
-    return value;
-  }, [api]);
+  const a11ySelection = useMemo(() => api.getQueryParam('a11ySelection'), [api]);
+  useEffect(() => {
+    if (a11ySelection) {
+      // Clear the param after reading; if supported, removing it entirely is preferable.
+      api.setQueryParams({ a11ySelection: '' });
+    }
+  }, [api, a11ySelection]);

And for the validator:

-  if (a11ySelection && /^[a-z]+.[a-z-]+.[0-9]+$/.test(a11ySelection)) {
+  if (a11ySelection && /^[a-z]+\.[a-z-]+\.[0-9]+$/.test(a11ySelection)) {

Also, the comment says “violation” but the actual type is “violations”. Consider aligning the example or accepting both forms when parsing.

Also applies to: 304-321


161-168: Prefer immutable Map updates for clarity

Current code mutates prev (delete/set) before cloning. Use a fresh Map to avoid in‑place mutation while keeping logic the same.

Apply this diff:

-  setSelectedItems((prev) => new Map(prev.delete(key) ? prev : prev.set(key, `${key}.1`)));
+  setSelectedItems((prev) => {
+    const next = new Map(prev);
+    if (next.has(key)) next.delete(key);
+    else next.set(key, `${key}.1`);
+    return next;
+  });

304-321: Comment drift

Comment refers to setHighlighted/setTab/setUiState which no longer exist as separate setters. Update to reflect setState to prevent future confusion.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9ec584f and 6afa95b.

📒 Files selected for processing (4)
  • code/addons/a11y/src/components/A11yContext.test.tsx (1 hunks)
  • code/addons/a11y/src/components/A11yContext.tsx (14 hunks)
  • code/addons/a11y/src/manager.tsx (1 hunks)
  • code/addons/a11y/src/types.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • code/addons/a11y/src/components/A11yContext.test.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,jsx,json,html,ts,tsx,mjs}: Run Prettier formatting on changed files before committing
Run ESLint on changed files and fix all errors/warnings before committing (use yarn lint:js:cmd <file>)

Files:

  • code/addons/a11y/src/types.ts
  • code/addons/a11y/src/components/A11yContext.tsx
  • code/addons/a11y/src/manager.tsx
**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Export functions from modules when they need to be unit-tested

Files:

  • code/addons/a11y/src/types.ts
  • code/addons/a11y/src/components/A11yContext.tsx
  • code/addons/a11y/src/manager.tsx
code/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

In application code, use Storybook loggers instead of console.* (client code: storybook/internal/client-logger; server code: storybook/internal/node-logger)

Files:

  • code/addons/a11y/src/types.ts
  • code/addons/a11y/src/components/A11yContext.tsx
  • code/addons/a11y/src/manager.tsx
{code/**,scripts/**}/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/addons/a11y/src/types.ts
  • code/addons/a11y/src/components/A11yContext.tsx
  • code/addons/a11y/src/manager.tsx
🧬 Code graph analysis (2)
code/addons/a11y/src/components/A11yContext.tsx (2)
code/core/src/manager-api/root.tsx (1)
  • useAddonState (455-457)
code/addons/a11y/src/types.ts (4)
  • RuleType (33-37)
  • RuleType (39-39)
  • EnhancedResults (49-53)
  • Status (60-67)
code/addons/a11y/src/manager.tsx (2)
code/core/src/manager-api/root.tsx (2)
  • useStorybookApi (294-297)
  • useAddonState (455-457)
code/addons/a11y/src/types.ts (4)
  • RuleType (33-37)
  • RuleType (39-39)
  • EnhancedResults (49-53)
  • Status (60-67)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Core Unit Tests, windows-latest
  • GitHub Check: Danger JS
  • GitHub Check: normal
🔇 Additional comments (1)
code/addons/a11y/src/types.ts (1)

60-67: Shared Status type looks good

Centralizing status strings here improves consistency across manager/context/tests. No issues spotted.

Comment on lines 16 to 23
const [{ results }] = useAddonState<{
ui: { highlighted: boolean; tab: RuleType };
results: EnhancedResults | undefined;
error: unknown;
status: Status;
}>(ADDON_ID);
const violationsNb = results?.violations?.length || 0;
const incompleteNb = results?.incomplete?.length || 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Unsafe destructuring of shared addon state can throw on first mount

If Title renders before any component sets a default for ADDON_ID, state is undefined and const [{ results }] = useAddonState(...) throws. Use a null‑safe pattern.

Apply this diff:

-  const [{ results }] = useAddonState<{
+  const [state] = useAddonState<{
     ui: { highlighted: boolean; tab: RuleType };
     results: EnhancedResults | undefined;
     error: unknown;
     status: Status;
   }>(ADDON_ID);
-  const violationsNb = results?.violations?.length || 0;
-  const incompleteNb = results?.incomplete?.length || 0;
+  const results = state?.results;
+  const violationsNb = results?.violations?.length ?? 0;
+  const incompleteNb = results?.incomplete?.length ?? 0;

Run to find any other call sites that may assume the old shape or destructure unsafely:


🏁 Script executed:

#!/bin/bash
# Search for useAddonState calls keyed by ADDON_ID and show surrounding context
rg -nP -C3 'useAddonState<[^>]*>\(\s*ADDON_ID' --type ts --type tsx code | sed -n '1,200p'

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search for useAddonState calls with proper file type handling
rg -nP -C3 'useAddonState' --type ts code/addons/a11y/src/manager.tsx

Length of output: 552


🏁 Script executed:

#!/bin/bash
# Broader search across code directory for all useAddonState patterns
rg -nP -C3 'useAddonState' --type ts code | head -100

Length of output: 6818


🏁 Script executed:

#!/bin/bash
# Check if ADDON_ID is defined and where
rg -nP 'ADDON_ID\s*=' --type ts code/addons/a11y | head -50

Length of output: 77


🏁 Script executed:

#!/bin/bash
# View the useAddonState implementation to understand when it returns undefined
cat code/core/src/manager-api/root.tsx | sed -n '455,480p'

Length of output: 916


🏁 Script executed:

#!/bin/bash
# Check the current state of A11yContext.tsx around line 107 for similar issue
sed -n '100,120p' code/addons/a11y/src/components/A11yContext.tsx

Length of output: 538


🏁 Script executed:

#!/bin/bash
# Check current lines 16-25 in manager.tsx to see if fix is already applied
sed -n '13,30p' code/addons/a11y/src/manager.tsx

Length of output: 605


Apply the proposed diff to code/addons/a11y/src/manager.tsx (lines 16-23)

The code destructures unsafely from useAddonState(ADDON_ID) with no default state. The test suite proves this can return [undefined], which throws when destructuring. The fix correctly introduces null-safety:

-  const [{ results }] = useAddonState<{
+  const [state] = useAddonState<{
     ui: { highlighted: boolean; tab: RuleType };
     results: EnhancedResults | undefined;
     error: unknown;
     status: Status;
   }>(ADDON_ID);
-  const violationsNb = results?.violations?.length || 0;
-  const incompleteNb = results?.incomplete?.length || 0;
+  const results = state?.results;
+  const violationsNb = results?.violations?.length ?? 0;
+  const incompleteNb = results?.incomplete?.length ?? 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [{ results }] = useAddonState<{
ui: { highlighted: boolean; tab: RuleType };
results: EnhancedResults | undefined;
error: unknown;
status: Status;
}>(ADDON_ID);
const violationsNb = results?.violations?.length || 0;
const incompleteNb = results?.incomplete?.length || 0;
const [state] = useAddonState<{
ui: { highlighted: boolean; tab: RuleType };
results: EnhancedResults | undefined;
error: unknown;
status: Status;
}>(ADDON_ID);
const results = state?.results;
const violationsNb = results?.violations?.length ?? 0;
const incompleteNb = results?.incomplete?.length ?? 0;
🤖 Prompt for AI Agents
In code/addons/a11y/src/manager.tsx around lines 16 to 23, the call to
useAddonState(ADDON_ID) is destructured without a default and can return
[undefined], causing a crash; change the destructuring to provide a safe default
state (e.g. const [state = {} as { ui?: { highlighted: boolean; tab: RuleType };
results?: EnhancedResults; error?: unknown; status?: Status }] =
useAddonState(ADDON_ID)) and then read values off state (const violationsNb =
state.results?.violations?.length || 0; const incompleteNb =
state.results?.incomplete?.length || 0) so the code never throws when the hook
returns undefined.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
code/addons/a11y/src/components/A11yContext.tsx (1)

205-213: Critical: Stale closure bug remains unfixed.

The setTimeout callback checks if (status === 'ran') using the captured value from line 122, not the state you just set on line 203. This condition will typically fail because status was likely 'running' or 'initial' when this callback was created.

Apply the fix from the previous review:

       setTimeout(() => {
-        if (status === 'ran') {
-          setState((prev) => ({ ...prev, status: 'ready' }));
-        }
+        setState((prev) =>
+          prev.status === 'ran' ? { ...prev, status: 'ready' } : prev
+        );
         if (selectedItems.size === 1) {
           const [key] = selectedItems.values();
           document.getElementById(key)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
         }
       }, 900);

Note: The selectedItems check on line 209 also reads a captured value, but since selectedItems is local state (not in the addon state), and the check is only for UX (scrolling), this is lower priority.

🧹 Nitpick comments (1)
code/addons/a11y/src/components/A11yContext.tsx (1)

453-462: Consider moving inline callbacks outside the return statement.

Defining setTab and setStatus inline within the JSX is unconventional and slightly reduces readability. Consider defining them alongside other handlers (e.g., near handleManual on line 285).

For example:

const setTab = useCallback(
  (type: RuleType) => setState((prev) => ({ ...prev, ui: { ...prev.ui, tab: type } })),
  [setState]
);

const setStatus = useCallback(
  (status: Status) => setState((prev) => ({ ...prev, status })),
  [setState]
);

return (
  <A11yContext.Provider
    value={{
      // ...
      setTab,
      setStatus,
      // ...
    }}
    {...props}
  />
);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6afa95b and 53b41db.

📒 Files selected for processing (2)
  • code/addons/a11y/src/components/A11yContext.tsx (14 hunks)
  • code/addons/a11y/src/manager.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • code/addons/a11y/src/manager.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,jsx,json,html,ts,tsx,mjs}: Run Prettier formatting on changed files before committing
Run ESLint on changed files and fix all errors/warnings before committing (use yarn lint:js:cmd <file>)

Files:

  • code/addons/a11y/src/components/A11yContext.tsx
**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Export functions from modules when they need to be unit-tested

Files:

  • code/addons/a11y/src/components/A11yContext.tsx
code/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

In application code, use Storybook loggers instead of console.* (client code: storybook/internal/client-logger; server code: storybook/internal/node-logger)

Files:

  • code/addons/a11y/src/components/A11yContext.tsx
{code/**,scripts/**}/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/addons/a11y/src/components/A11yContext.tsx
🧬 Code graph analysis (1)
code/addons/a11y/src/components/A11yContext.tsx (2)
code/core/src/manager-api/root.tsx (1)
  • useAddonState (455-457)
code/addons/a11y/src/types.ts (4)
  • RuleType (33-37)
  • RuleType (39-39)
  • EnhancedResults (49-53)
  • Status (60-67)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: normal
🔇 Additional comments (3)
code/addons/a11y/src/components/A11yContext.tsx (3)

107-120: LGTM! State structure aligns with persistence goals.

The composite state initialization correctly groups UI preferences (highlighted, tab) with ephemeral data (results, error, status), enabling persistence of user preferences across navigation.


142-144: LGTM! Consistent state management patterns.

All state updates correctly use the updater form of setState, ensuring atomic reads and writes of the composite state. The refactoring consistently routes tab and highlight changes through the ui substate.

Also applies to: 193-198, 254-267, 285-288, 300-302


329-423: LGTM! Highlight effect correctly uses persisted UI state.

The effect consistently references ui.tab and ui.highlighted throughout, ensuring highlight behavior reflects the persisted user preferences. Color mapping, result filtering, and menu generation all correctly key off ui.tab.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
code/addons/a11y/src/manager.test.tsx (1)

11-11: Add spy: true option to the mock.

As per coding guidelines, all package and file mocks should use the spy: true option.

Apply this diff:

-vi.mock('storybook/manager-api');
+vi.mock('storybook/manager-api', { spy: true });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53b41db and 4147d30.

📒 Files selected for processing (1)
  • code/addons/a11y/src/manager.test.tsx (2 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
code/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.cursorrules)

code/**/*.{test,spec}.{ts,tsx}: Place all test files under the code/ directory
Name test files as *.test.ts, *.test.tsx, *.spec.ts, or *.spec.tsx

Files:

  • code/addons/a11y/src/manager.test.tsx
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/spy-mocking.mdc)

**/*.test.{ts,tsx,js,jsx}: Use vi.mock() with the spy: true option for all package and file mocks in Vitest tests
Place all mocks at the top of the test file before any test cases
Use vi.mocked() to type and access mocked functions
Implement mock behaviors in beforeEach blocks
Mock all required dependencies that the test subject uses
Mock implementations should be placed in beforeEach blocks
Each mock implementation should return a Promise for async functions
Mock implementations should match the expected return type of the original function
Use vi.mocked() to access and implement mock behaviors
Mock all required properties and methods that the test subject uses
Avoid direct function mocking without vi.mocked()
Avoid mock implementations outside of beforeEach blocks
Avoid mocking without the spy: true option
Avoid inline mock implementations within test cases
Avoid mocking only a subset of required dependencies
Mock at the highest level of abstraction needed
Keep mock implementations simple and focused
Use type-safe mocking with vi.mocked()
Document complex mock behaviors
Group related mocks together

Files:

  • code/addons/a11y/src/manager.test.tsx
**/*.{js,jsx,json,html,ts,tsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,jsx,json,html,ts,tsx,mjs}: Run Prettier formatting on changed files before committing
Run ESLint on changed files and fix all errors/warnings before committing (use yarn lint:js:cmd <file>)

Files:

  • code/addons/a11y/src/manager.test.tsx
**/*.@(test|spec).{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.@(test|spec).{ts,tsx,js,jsx}: Unit tests should import and execute the functions under test rather than only asserting on syntax patterns
Mock external dependencies in tests using vi.mock() (e.g., filesystem, loggers)

Files:

  • code/addons/a11y/src/manager.test.tsx
**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Export functions from modules when they need to be unit-tested

Files:

  • code/addons/a11y/src/manager.test.tsx
code/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

In application code, use Storybook loggers instead of console.* (client code: storybook/internal/client-logger; server code: storybook/internal/node-logger)

Files:

  • code/addons/a11y/src/manager.test.tsx
{code/**,scripts/**}/**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/addons/a11y/src/manager.test.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: normal
🔇 Additional comments (2)
code/addons/a11y/src/manager.test.tsx (2)

66-73: LGTM! State structure correctly updated.

The mock correctly reflects the new nested state structure where violations and incomplete are nested under results. The test logic properly validates the badge count (3 = 1 violation + 2 incomplete).

Same note as the previous test: ensure the partial state object (omitting ui, error, status) is safe for the manager code.


41-41: No issues found. The partial state mock is intentional and correct.

The Title component at line 17 of manager.tsx only destructures { results } from the addon state, despite the full state type including ui, error, and a status property. The mock at line 41 correctly provides only the results property that the component actually uses. This is a valid pattern—TypeScript allows destructuring a subset of object properties, and the test correctly mocks only what's necessary for the Title component to render. No changes are needed.

@ndelangen ndelangen merged commit 7ac43e3 into storybookjs:next Oct 27, 2025
56 of 58 checks passed
@github-actions github-actions bot mentioned this pull request Oct 27, 2025
6 tasks
@404Dealer
Copy link
Contributor Author

Thanks for merging this, @ndelangen! Really appreciate the feedback and tweaks. My first opensource merged PR got me jumping out my seat hahaha :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: SB10 - A11y addon loses state when navigating to docs page

4 participants