Skip to content

fix(F0Link): resolve stability audit BLOCKING issues#3903

Open
eliseo-juan wants to merge 1 commit intomainfrom
fix/stability-3877-f0link
Open

fix(F0Link): resolve stability audit BLOCKING issues#3903
eliseo-juan wants to merge 1 commit intomainfrom
fix/stability-3877-f0link

Conversation

@eliseo-juan
Copy link
Copy Markdown
Contributor

Summary

Resolves BLOCKING findings from stability audit issue #3877 for F0Link.

Changes

Accessibility

  • Decorative icon: Add aria-hidden={true} to <F0Icon icon={ExternalLink} /> so screen readers skip it
  • External link warning: Add <span className="sr-only">{i18n.link.opensInNewTab}</span> when external is true (WCAG 2.1 SC 3.2.2 / G201)
  • i18n key: Add link.opensInNewTab: "opens in new tab" to i18n-provider-defaults.ts
  • aria-label: Remove props.title fallback — only pass aria-label when explicit ariaLabel prop is provided (fixes WCAG 2.5.3 "Label in Name" violation)
  • href fallback: Remove href || "#" — pass href conditionally so Action/linkHandler can render <button>/<span> when no href given, preventing unwanted scroll-to-top

Tests

  • Replace import { render, screen } from "@testing-library/react" with import { zeroRender, screen } from "@/testing/test-utils"
  • Replace raw DOM traversal querySelector("svg") with accessible getByText("opens in new tab") query

Stories

  • Change satisfies Meta<ComponentProps<typeof F0Link>>satisfies Meta<typeof F0Link>

Testing

  • pnpm tsc --noEmit
  • pnpm lint
  • pnpm vitest:ci src/components/F0Link ✅ (7 passed)

Closes #3877

- Remove href || '#' fallback; pass href conditionally so Action can use
  the no-href span/button branch instead of triggering page scroll-to-top
- Add aria-hidden={true} to decorative ExternalLink icon
- Add sr-only 'opens in new tab' span for external links (WCAG 3.2.2 G201)
- Add i18n key link.opensInNewTab to i18n-provider-defaults.ts
- Remove props.title fallback from aria-label (violates WCAG 2.5.3)
- Fix tests: import from @/testing/test-utils, use zeroRender
- Fix test: replace DOM traversal querySelector with accessible sr-only query
- Fix stories: Meta<typeof F0Link> instead of Meta<ComponentProps<...>>
Copilot AI review requested due to automatic review settings April 8, 2026 16:35
@eliseo-juan eliseo-juan requested a review from a team as a code owner April 8, 2026 16:35
@github-actions github-actions bot added the fix label Apr 8, 2026
@github-actions github-actions bot added the react Changes affect packages/react label Apr 8, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

✅ No New Circular Dependencies

No new circular dependencies detected. Current count: 0

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

📦 Alpha Package Version Published

Use pnpm i github:factorialco/f0#npm/alpha-pr-3903 to install the package

Use pnpm i github:factorialco/f0#b8754c004e8acc1eb9b50ad99f636348b001cc3f to install this specific commit

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

🔍 Visual review for your branch is published 🔍

Here are the links to:

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

Coverage Report for packages/react

Status Category Percentage Covered / Total
🔵 Lines 45.42% 11171 / 24590
🔵 Statements 44.67% 11517 / 25778
🔵 Functions 37.16% 2510 / 6753
🔵 Branches 37.37% 7337 / 19629
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/react/src/components/F0Link/F0Link.tsx 72.72% 66.66% 50% 72.72% 37-40
packages/react/src/lib/providers/i18n/i18n-provider-defaults.ts 100% 100% 100% 100%
Generated in workflow #12672 for commit 8c53c0a by the Vitest Coverage Report Action

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Resolves stability-audit BLOCKING findings for F0Link by improving external-link accessibility, aligning tests with the repo’s testing utilities, and tightening Storybook typing for better inference.

Changes:

  • Add external-link a11y affordances (decorative icon aria-hidden, SR-only “opens in new tab”, remove titlearia-label fallback, and remove href="#" fallback behavior).
  • Update F0Link unit tests to use zeroRender and avoid DOM-structure-coupled assertions.
  • Adjust Storybook meta typing to satisfies Meta<typeof F0Link>.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
packages/react/src/lib/providers/i18n/i18n-provider-defaults.ts Adds default i18n string for the external-link SR-only warning.
packages/react/src/components/F0Link/F0Link.tsx Implements a11y fixes and conditional href handling for F0Link.
packages/react/src/components/F0Link/tests/index.test.tsx Migrates tests to zeroRender and uses an accessible assertion for the external indicator.
packages/react/src/components/F0Link/stories/F0Link.stories.tsx Updates Storybook Meta typing to improve arg-type inference.
Comments suppressed due to low confidence (1)

packages/react/src/components/F0Link/F0Link.tsx:37

  • F0Link can now omit href, which means Action will render a <button> (not an <a>). The component is still typed as forwardRef<HTMLAnchorElement, F0LinkProps> and handleClick is typed with React.MouseEvent<HTMLAnchorElement>, so the public ref/event types no longer match what can be rendered. Consider updating the ref generic and F0LinkProps to a discriminated union (link vs button) so consumers get correct types without needing casts.
const _F0Link = forwardRef<HTMLAnchorElement, F0LinkProps>(function Link(
  {
    className,
    children,
    stopPropagation = false,
    "aria-label": ariaLabel,
    href,
    ...props
  },
  ref
) {
  const i18n = useI18n()
  const { target } = props
  const external = target === "_blank"

  const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
    if (stopPropagation) {

Comment on lines +43 to +62
// Build Action props: include href only when provided so that Action can
// fall back to a button/span when no href is given (WCAG / no scroll-to-top).
const actionProps: ActionProps = (
href !== undefined
? {
...props,
href,
onClick: handleClick,
rel: external ? "noopener noreferrer" : undefined,
"aria-label": ariaLabel,
className,
}
: {
...props,
onClick:
handleClick as unknown as React.MouseEventHandler<HTMLButtonElement>,
"aria-label": ariaLabel,
className,
}
) as ActionProps
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

actionProps is being forced to ActionProps via a cast and an as unknown as React.MouseEventHandler<HTMLButtonElement> cast. This defeats the ActionLinkProps/ActionButtonProps type safety and makes it easy to accidentally pass link-only props when rendering as a button. Prefer restructuring so TypeScript can infer the correct branch (e.g., build two separately typed prop objects and render <Action> conditionally), which should also remove the need for the onClick cast.

Copilot uses AI. Check for mistakes.
...props,
href,
onClick: handleClick,
rel: external ? "noopener noreferrer" : undefined,
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Action already derives rel="noopener noreferrer" internally when target === "_blank" (it overrides any rel passed through props). Setting rel in F0Link is redundant and can be misleading to future readers (especially if they expect it to affect the rendered output). Consider removing rel from F0Link’s actionProps and letting Action be the single source of truth.

Suggested change
rel: external ? "noopener noreferrer" : undefined,

Copilot uses AI. Check for mistakes.
Comment on lines 29 to 35
},
ref
) {
const i18n = useI18n()
const { target } = props
const external = target === "_blank"

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

_F0Link is a forwardRef component but doesn’t set an explicit displayName. This repo’s conventions expect forwardRef components to set displayName, and it also helps withDataTestId preserve a meaningful name in DevTools. Add _F0Link.displayName = "F0Link" after the declaration (before wrapping with withDataTestId).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix react Changes affect packages/react

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stability audit: F0Link

2 participants