Skip to content

UI: Ensure Link without href is keyboard-reachable#34163

Merged
Sidnioulz merged 8 commits intonextfrom
sidnioulz/10-3-opportunistic-link-isbutton-bugfix
Mar 18, 2026
Merged

UI: Ensure Link without href is keyboard-reachable#34163
Sidnioulz merged 8 commits intonextfrom
sidnioulz/10-3-opportunistic-link-isbutton-bugfix

Conversation

@Sidnioulz
Copy link
Copy Markdown
Member

@Sidnioulz Sidnioulz commented Mar 16, 2026

What I did

Tied the isButton logic of the Link component to the absence of a href. Without a href, an HTML anchor will not be keyboard-reachable. This implies buttons that use the Link styling with <Link isButton onClick={() => ... } /> are not accessible.

As all our usages of <Link isBUtton are cases where we don't have a href, it makes sense for us to tie the logic to that criteria. This will avoid future regressions and ensure those "links" have the proper HTML markup.

I've gone through a deprecation phase for external users of the component, adding the traditional deprecate call.

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

  1. Visit http://localhost:6006/?path=/settings/guide
  2. Try to focus the little link at the bottom of all the accordions
image

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=<PR_NUMBER>

Summary by CodeRabbit

  • Documentation
    • Added ariaLabel guidance for popovers and modals, noted deprecation of the Link isButton prop, and minor formatting fixes.
  • Improvements
    • Enhanced Link button styling for focus and accessibility.
    • Several previously button-styled links now display as standard links while preserving click behavior.
  • Tests / Stories
    • Stories updated to typed format with an interactive link test; e2e tests adjusted selectors to match element type changes.

@Sidnioulz Sidnioulz requested a review from ghengeveld March 16, 2026 18:46
@Sidnioulz Sidnioulz added bug accessibility ci:normal a11y: keyboard Accessibility issues related to keyboard navigation or shortcuts labels Mar 16, 2026
@nx-cloud
Copy link
Copy Markdown

nx-cloud bot commented Mar 16, 2026

View your CI Pipeline Execution ↗ for commit fda8f06

Command Status Duration Result
nx run-many -t compile -c production --parallel=1 ✅ Succeeded 5m 20s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-18 09:31:09 UTC

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 73d07ccf-752d-4acf-8f1d-25ff74411b77

📥 Commits

Reviewing files that changed from the base of the PR and between f5212d8 and fda8f06.

📒 Files selected for processing (2)
  • test-storybooks/portable-stories-kitchen-sink/react-vitest-3/e2e-tests/component-testing.spec.ts
  • test-storybooks/portable-stories-kitchen-sink/react/e2e-tests/component-testing.spec.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • test-storybooks/portable-stories-kitchen-sink/react/e2e-tests/component-testing.spec.ts
  • test-storybooks/portable-stories-kitchen-sink/react-vitest-3/e2e-tests/component-testing.spec.ts

📝 Walkthrough

Walkthrough

Deprecates Link's isButton prop; Link now renders as button when no href and as a when href is present. Removes isButton at call sites, updates Link stories to typed Storybook pattern, adjusts tests/selectors to match rendered element, and adds ariaLabel entries in MIGRATION.md.

Changes

Cohort / File(s) Summary
Documentation
MIGRATION.md
Added ariaLabel documentation references for PopoverProvider and Modal; noted deprecation of Link isButton; minor formatting/path-escaping tweaks.
Link component
code/core/src/components/components/typography/link/link.tsx
Introduces deprecation warning when isButton is provided; chooses rendered element via as={href ? 'a' : 'button'} and treats button behavior when no href; preserves onClick behavior and adds focus/button styling.
Stories
code/core/src/components/components/typography/link/link.stories.tsx
Migrated stories to typed Storybook (Meta/StoryObj, satisfies); added ButtonLink interactive story with play/test assertions; updated existing stories to typed pattern.
Call sites / UI usage
code/addons/vitest/src/components/Description.tsx, code/core/src/manager/components/sidebar/RefBlocks.tsx
Removed isButton prop from Link usages, changing presentation from button-styled to standard link while preserving click handlers and icons.
E2E tests
test-storybooks/.../component-testing.spec.ts (react, react-vitest-3)
Updated selectors to target button elements (replacing a) where Link now renders as button; test flow unchanged.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
📝 Coding Plan
  • Generate coding plan for human review comments

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

Copy link
Copy Markdown
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

🧹 Nitpick comments (2)
code/core/src/components/components/typography/link/link.tsx (1)

197-211: Update forwardRef type to reflect polymorphic rendering.

The Link component renders as either <a> or <button> depending on the href prop (line 220), but the forwardRef type is fixed to HTMLAnchorElement. Update the type to union both element types:

Suggested patch
-export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
+export const Link = forwardRef<HTMLAnchorElement | HTMLButtonElement, LinkProps>(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/components/components/typography/link/link.tsx` around lines
197 - 211, The forwardRef typing for the Link component is wrong because the
component can render as an anchor or a button; update the forwardRef generic to
accept a union of HTMLAnchorElement | HTMLButtonElement instead of only
HTMLAnchorElement so the ref type matches both rendering paths (change the
signature on forwardRef used for the Link component and any related LinkProps
that reference the ref type). Ensure the change references the Link function and
the forwardRef call so consumers using href (anchor) or button rendering get the
correct element type for refs.
code/core/src/components/components/typography/link/link.stories.tsx (1)

56-69: Good test coverage for the accessibility fix.

The story correctly validates that a Link without href renders as a button and is keyboard-reachable. Using getByRole('button') confirms the correct semantic element is rendered.

Consider using userEvent from @storybook/test for more realistic interaction testing that better simulates user behavior:

♻️ Optional: Use userEvent for more realistic interactions
+import { expect, fn, userEvent } from 'storybook/test';
-import { expect, fn } from 'storybook/test';
 
 export const ButtonLink = {
   args: {
     children: 'Click me',
     onClick: fn(),
   },
   name: 'Link-styled button',
   play: async ({ args, canvas }) => {
     const link = canvas.getByRole('button', { name: 'Click me' });
-    link.focus();
+    await userEvent.tab();
     expect(link).toHaveFocus();
-    link.click();
+    await userEvent.click(link);
     expect(args.onClick).toHaveBeenCalled();
   },
 } satisfies Story;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/components/components/typography/link/link.stories.tsx` around
lines 56 - 69, Replace the direct DOM click/focus calls in the ButtonLink story
play function with the userEvent helpers to simulate real user interactions: add
an import for userEvent from '@storybook/test', then in the play function for
ButtonLink use await userEvent.focus(link) (or await userEvent.tab() if you want
keyboard navigation) instead of link.focus(), and replace link.click() with
await userEvent.click(link); keep the existing assertions
(expect(link).toHaveFocus(), expect(args.onClick).toHaveBeenCalled()) and update
the async flow accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/core/src/components/components/typography/link/link.tsx`:
- Around line 219-225: The Link component renders a native <button> when href is
not provided, so add an explicit type="button" for that path to avoid implicit
type="submit"; update the A element props (the JSX line that sets as={href ? 'a'
: 'button'} and isButton={!href || isButton === true}) to include type={href ?
undefined : 'button'} (or type="button" when as is 'button'), ensuring the prop
is passed alongside ref, onClick, isButton, cancelled, cancel and {...rest}.

In `@MIGRATION.md`:
- Line 684: The migration note text uses incorrect article "a" before "HTML
`button`"; update the sentence starting "Link now renders automatically as a
HTML `button`..." to use "an HTML `button`" instead, leaving the rest of the
sentence and references to the `isButton` prop and Storybook 10/11 behavior
unchanged.

---

Nitpick comments:
In `@code/core/src/components/components/typography/link/link.stories.tsx`:
- Around line 56-69: Replace the direct DOM click/focus calls in the ButtonLink
story play function with the userEvent helpers to simulate real user
interactions: add an import for userEvent from '@storybook/test', then in the
play function for ButtonLink use await userEvent.focus(link) (or await
userEvent.tab() if you want keyboard navigation) instead of link.focus(), and
replace link.click() with await userEvent.click(link); keep the existing
assertions (expect(link).toHaveFocus(), expect(args.onClick).toHaveBeenCalled())
and update the async flow accordingly.

In `@code/core/src/components/components/typography/link/link.tsx`:
- Around line 197-211: The forwardRef typing for the Link component is wrong
because the component can render as an anchor or a button; update the forwardRef
generic to accept a union of HTMLAnchorElement | HTMLButtonElement instead of
only HTMLAnchorElement so the ref type matches both rendering paths (change the
signature on forwardRef used for the Link component and any related LinkProps
that reference the ref type). Ensure the change references the Link function and
the forwardRef call so consumers using href (anchor) or button rendering get the
correct element type for refs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4ebf4b0e-5b4c-4570-8bc9-faf072159a9d

📥 Commits

Reviewing files that changed from the base of the PR and between 546aece and d34b80e.

📒 Files selected for processing (5)
  • MIGRATION.md
  • code/addons/vitest/src/components/Description.tsx
  • code/core/src/components/components/typography/link/link.stories.tsx
  • code/core/src/components/components/typography/link/link.tsx
  • code/core/src/manager/components/sidebar/RefBlocks.tsx

Comment on lines +219 to +225
<A
as={href ? 'a' : 'button'}
href={href}
{...rest}
ref={ref}
isButton={!href || isButton === true}
onClick={onClick && cancel ? (e) => cancelled(e, onClick) : onClick}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate the exact file
find code -name "link.tsx" -path "*/typography/*" | head -20

Repository: storybookjs/storybook

Length of output: 125


🏁 Script executed:

# Get the file size and content around the specified lines
wc -l code/core/src/components/components/typography/link/link.tsx

Repository: storybookjs/storybook

Length of output: 129


🏁 Script executed:

# Read the file to see the full context
cat -n code/core/src/components/components/typography/link/link.tsx

Repository: storybookjs/storybook

Length of output: 7286


🏁 Script executed:

# Search for Link component usage with form elements
rg -A 5 -B 5 "Link\b.*href=\{false\}|Link\b.*href=\{undefined\}|as=.*button" code/core/src --type tsx --type ts | head -100

Repository: storybookjs/storybook

Length of output: 92


🏁 Script executed:

# Check if Link is used in form contexts
rg "<form" code/core/src --type tsx --type ts -l | head -10

Repository: storybookjs/storybook

Length of output: 92


🏁 Script executed:

# Look for any existing patterns where Link might be used without href
rg "<Link" code/core/src --type tsx --type ts -A 2 | grep -E "(Link|href|onClick)" | head -50

Repository: storybookjs/storybook

Length of output: 92


🏁 Script executed:

# Search for Link component without href to understand actual usage patterns
rg "<Link" code/core/src -A 3 | grep -E "(Link|href|onClick|as=)" | head -60

Repository: storybookjs/storybook

Length of output: 5586


🏁 Script executed:

# Check if there are any form contexts where Link is used
rg -B 5 -A 5 "as=.*button" code/core/src | head -80

Repository: storybookjs/storybook

Length of output: 987


🏁 Script executed:

# Let's verify the A styled component's prop handling
ast-grep --pattern 'const A = styled.a'

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

# Look at the GuidePage.tsx usage to confirm button usage without href
cat -n code/core/src/manager/settings/GuidePage.tsx | grep -A 2 -B 2 "Link onClick"

Repository: storybookjs/storybook

Length of output: 563


🏁 Script executed:

# Check UpgradeBlock.tsx usage
cat -n code/core/src/manager/components/upgrade/UpgradeBlock.tsx | grep -A 2 -B 2 "Link onClick"

Repository: storybookjs/storybook

Length of output: 252


🏁 Script executed:

# Verify the AProps type definition and what it includes
sed -n '66,66p' code/core/src/components/components/typography/link/link.tsx

Repository: storybookjs/storybook

Length of output: 119


🏁 Script executed:

# Check if styled-components passes through type attribute on button elements
# Search for other button type handling patterns in the codebase
rg "type=.*button" code/core/src --max-count 20

Repository: storybookjs/storybook

Length of output: 1010


Set an explicit button type for the no-href path.

When href is missing (lines 220–225), the component renders a real <button> element. Without an explicit type attribute, HTML defaults to type="submit", which can cause unintended form submissions if the Link is used within a form.

Real usages exist in the codebase (e.g., GuidePage.tsx, UpgradeBlock.tsx) where Link is called without href, triggering button rendering. The codebase's other button-based components consistently use explicit type="button".

Suggested patch
       <A
         as={href ? 'a' : 'button'}
         href={href}
+        type={href ? undefined : 'button'}
         {...rest}
         ref={ref}
         isButton={!href || isButton === true}
         onClick={onClick && cancel ? (e) => cancelled(e, onClick) : onClick}
         className={className}
       >
📝 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
<A
as={href ? 'a' : 'button'}
href={href}
{...rest}
ref={ref}
isButton={!href || isButton === true}
onClick={onClick && cancel ? (e) => cancelled(e, onClick) : onClick}
<A
as={href ? 'a' : 'button'}
href={href}
type={href ? undefined : 'button'}
{...rest}
ref={ref}
isButton={!href || isButton === true}
onClick={onClick && cancel ? (e) => cancelled(e, onClick) : onClick}
className={className}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/components/components/typography/link/link.tsx` around lines
219 - 225, The Link component renders a native <button> when href is not
provided, so add an explicit type="button" for that path to avoid implicit
type="submit"; update the A element props (the JSX line that sets as={href ? 'a'
: 'button'} and isButton={!href || isButton === true}) to include type={href ?
undefined : 'button'} (or type="button" when as is 'button'), ensuring the prop
is passed alongside ref, onClick, isButton, cancelled, cancel and {...rest}.

Comment thread MIGRATION.md Outdated
Copy link
Copy Markdown
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

Caution

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

⚠️ Outside diff range comments (1)
code/core/src/components/components/typography/link/link.tsx (1)

66-66: ⚠️ Potential issue | 🟠 Major

Fix ref/prop typings to match conditional <a>/<button> rendering.

The Link component conditionally renders <a> or <button> based on the href prop (line 221: as={href ? 'a' : 'button'}), but the ref is typed as HTMLAnchorElement only (line 198). This creates a type safety mismatch: developers can pass LinkProps with href undefined and render a button, but TypeScript claims the ref is an anchor.

Use a discriminated union to align types with runtime behavior:

Type-shape direction
-import type { AnchorHTMLAttributes, MouseEvent } from 'react';
+import type { AnchorHTMLAttributes, ButtonHTMLAttributes, MouseEvent } from 'react';
...
-type AProps = AnchorHTMLAttributes<HTMLAnchorElement>;
+type AnchorProps = AnchorHTMLAttributes<HTMLAnchorElement>;
+type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
...
-export interface LinkProps extends LinkInnerProps, LinkStylesProps, AProps {
-  cancel?: boolean;
-  className?: string;
-  style?: object;
-  onClick?: (e: MouseEvent) => void;
-  href?: string;
-}
+type LinkBaseProps = LinkInnerProps &
+  LinkStylesProps & {
+    cancel?: boolean;
+    className?: string;
+    style?: object;
+    onClick?: (e: MouseEvent) => void;
+  };
+
+export type LinkProps =
+  | (LinkBaseProps & AnchorProps & { href: string })
+  | (LinkBaseProps & ButtonProps & { href?: undefined });
...
-export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
+export const Link = forwardRef<HTMLAnchorElement | HTMLButtonElement, LinkProps>(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/components/components/typography/link/link.tsx` at line 66, The
Link component currently types AProps as AnchorHTMLAttributes<HTMLAnchorElement>
while rendering either an <a> or <button> (as={href ? 'a' : 'button'}), causing
a ref/type mismatch; change the prop typing to a discriminated union (e.g., one
variant for when href is present using AnchorHTMLAttributes and ref
HTMLAnchorElement, and another for when href is absent using
ButtonHTMLAttributes and ref HTMLButtonElement), update the LinkProps and the
ref/react.forwardRef signature to accept the union so TypeScript knows which
element/ref type corresponds to href presence, and adjust any usages of AProps,
LinkProps, or the Link component's forwarded ref typing to match the new
discriminated union.
♻️ Duplicate comments (1)
code/core/src/components/components/typography/link/link.tsx (1)

220-227: ⚠️ Potential issue | 🟠 Major

Set an explicit type="button" for the no-href rendering path.

Line 221 renders a native <button> when href is absent, but no explicit type is set. Inside forms, this defaults to submit behavior.

Suggested patch
       <A
         as={href ? 'a' : 'button'}
         href={href}
+        type={href ? undefined : 'button'}
         {...rest}
         ref={ref}
         isButton={!href || isButton === true}
         onClick={onClick && cancel ? (e) => cancelled(e, onClick) : onClick}
         className={className}
       >
#!/bin/bash
# Verify the current render path and whether `type` is explicitly set.
sed -n '218,232p' code/core/src/components/components/typography/link/link.tsx

# Optional impact check: find Link usages without href.
rg -nP --type=tsx --type=ts '<Link(?![^>]*\bhref=)' code/core/src -C1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/components/components/typography/link/link.tsx` around lines
220 - 227, The A element renders as a native button when href is absent but no
explicit type is set, causing unintended form submits; update the A JSX props so
that when href is falsy (the no-href path) you pass type="button" (or
type={rest.type ?? 'button'} to respect an explicit type prop) — change the A
element usage (the component with props as, href, isButton, onClick, ref,
className) to include type="button" for the button rendering path to prevent
default submit behavior inside forms.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/core/src/components/components/typography/link/link.tsx`:
- Around line 221-225: The truthy checks on href in the Link component cause
empty-string hrefs to be treated as absent; update the conditional checks in
link.tsx (the JSX props using as={...} and isButton={...}) to use explicit
null/undefined comparison: change as={href ? 'a' : 'button'} to as={href != null
? 'a' : 'button'} and change isButton={!href || isButton === true} to
isButton={href == null || isButton === true} so an empty string href is treated
as a valid link while preserving existing isButton override behavior.

---

Outside diff comments:
In `@code/core/src/components/components/typography/link/link.tsx`:
- Line 66: The Link component currently types AProps as
AnchorHTMLAttributes<HTMLAnchorElement> while rendering either an <a> or
<button> (as={href ? 'a' : 'button'}), causing a ref/type mismatch; change the
prop typing to a discriminated union (e.g., one variant for when href is present
using AnchorHTMLAttributes and ref HTMLAnchorElement, and another for when href
is absent using ButtonHTMLAttributes and ref HTMLButtonElement), update the
LinkProps and the ref/react.forwardRef signature to accept the union so
TypeScript knows which element/ref type corresponds to href presence, and adjust
any usages of AProps, LinkProps, or the Link component's forwarded ref typing to
match the new discriminated union.

---

Duplicate comments:
In `@code/core/src/components/components/typography/link/link.tsx`:
- Around line 220-227: The A element renders as a native button when href is
absent but no explicit type is set, causing unintended form submits; update the
A JSX props so that when href is falsy (the no-href path) you pass type="button"
(or type={rest.type ?? 'button'} to respect an explicit type prop) — change the
A element usage (the component with props as, href, isButton, onClick, ref,
className) to include type="button" for the button rendering path to prevent
default submit behavior inside forms.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 616181a9-8746-4556-b549-52cb3e3dcb8d

📥 Commits

Reviewing files that changed from the base of the PR and between 967d1cb and f5212d8.

📒 Files selected for processing (1)
  • code/core/src/components/components/typography/link/link.tsx

Comment on lines +221 to +225
as={href ? 'a' : 'button'}
href={href}
{...rest}
ref={ref}
isButton={!href || isButton === true}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for empty-string href usage that would be affected by truthiness checks.
rg -n --type=tsx --type=ts 'href=""|href=\{""\}' code/core/src -C1
rg -n --type=tsx --type=ts "href=''" code/core/src -C1

Repository: storybookjs/storybook

Length of output: 120


🏁 Script executed:

cat -n code/core/src/components/components/typography/link/link.tsx | sed -n '215,235p'

Repository: storybookjs/storybook

Length of output: 821


🏁 Script executed:

# Correct rg syntax for TypeScript/TSX files
rg 'href=""' code/core/src --type=ts -C1 | head -40

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

# Also check for href={""} pattern
rg 'href=\{""\}' code/core/src --type=ts -C1 | head -40

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

# Find the Link component type definition and href parameter type
rg -n 'interface.*Link|type.*Link' code/core/src/components/components/typography/link/link.tsx --type=ts -A5

Repository: storybookjs/storybook

Length of output: 599


🏁 Script executed:

# Look for the function signature of the Link component
head -50 code/core/src/components/components/typography/link/link.tsx | cat -n

Repository: storybookjs/storybook

Length of output: 1695


Improve clarity of href presence check using explicit null/undefined comparison.

Lines 221 and 225 use truthiness checks on href, which would treat an empty string href="" as falsy and switch to button behavior. Since the type definition allows href?: string, use href != null to explicitly check for the absence of the prop rather than relying on truthiness.

Suggested patch
+    const hasHref = href != null;
     return (
       <A
-        as={href ? 'a' : 'button'}
+        as={hasHref ? 'a' : 'button'}
         href={href}
         {...rest}
         ref={ref}
-        isButton={!href || isButton === true}
+        isButton={!hasHref || isButton === true}
         onClick={onClick && cancel ? (e) => cancelled(e, onClick) : onClick}
         className={className}
       >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/components/components/typography/link/link.tsx` around lines
221 - 225, The truthy checks on href in the Link component cause empty-string
hrefs to be treated as absent; update the conditional checks in link.tsx (the
JSX props using as={...} and isButton={...}) to use explicit null/undefined
comparison: change as={href ? 'a' : 'button'} to as={href != null ? 'a' :
'button'} and change isButton={!href || isButton === true} to isButton={href ==
null || isButton === true} so an empty string href is treated as a valid link
while preserving existing isButton override behavior.

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

Labels

a11y: keyboard Accessibility issues related to keyboard navigation or shortcuts accessibility bug ci:normal

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants