// Copyright (c) Meta Platforms, Inc. and affiliates. "use client"; /** * @file Link.tsx * @input Uses React, AnchorHTMLAttributes, ReactNode * @output Exports Link component, LinkProps * @position Core implementation; consumed by index.ts, tested by Link.test.tsx * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/Link/Link.doc.mjs (props table, features, implementation notes) * - /packages/core/src/Link/Link.test.tsx (tests for new/changed behavior) * - /packages/core/src/Link/index.ts (exports if types change) * - /apps/storybook/stories/Link.stories.tsx (storybook stories) * - /packages/cli/assets/templates/blocks/components/Link/ (showcase blocks) */ import type { ReactNode } from "react"; import type { BaseProps } from "../BaseProps.js"; import * as stylex from "../style-runtime.js"; import "../theme/tokens.stylex.js"; import { colorVars, durationVars, easeVars, spacingVars, typeScaleVars, } from "../theme/tokens.stylex.js"; import { Icon } from "../Icon/index.js"; import { Tooltip } from "../Tooltip/index.js"; import { Text } from "../Text/index.js"; import { VisuallyHidden } from "../VisuallyHidden/index.js"; import type { TextType, TextSize, TextColor, TextWeight, TextDisplay, } from "../theme/types.js"; import { useLinkComponent } from "./useLinkComponent.js"; import type { LinkComponentType } from "./types.js"; import { mergeProps } from "../utils/index.js"; import { computeTargetAndRel } from "./computeTargetAndRel.js"; import { useInteractiveRole } from "../hooks/useInteractiveRole.js"; import { themeProps } from "../utils/themeProps.js"; import { focusOutlineProps } from "../utils/focusOutline.stylex.js"; import { interactionOverlayStyles } from "../utils/interactionOverlay.stylex.js"; import { useTranslator } from "../i18n/index.js"; /** * Base link styles */ const styles = { base: { k1xSpc: "x3nfvp2", kGNEyG: "x6s0dn4", kOIVth: "x1lsbc85", kMv6JI: "xjb2p0i", kGuDYH: "x1qlqyl8", kLWn49: "x15bjb6t", k63SB2: "x1pd3egz", kybGjl: "x1hl2dhg x13nosk6", kkrTdU: "x1ypdohk x16khyan", k1ekBW: "x1mpt4pi", kIyJzY: "xuedmi6", kAMwcw: "xlr8y92", $$css: true as const, }, buttonReset: { kWkggS: "xjbqb8w", ksu8eU: "xng3xce", kmVPX3: "x1717udv", kfzvcC: "x67bb7w", kVAEAm: "x1n2onr6", $$css: true as const, }, hasUnderline: { kybGjl: "x1bvjpef", $$css: true as const, }, disabled: { kkrTdU: "xt0e3qv", kSiTet: "xbyyjgo", kfzvcC: "x47corl", $$css: true as const, }, standalone: { kGuDYH: "xjm74w1", kLWn49: "xw6l6zx", $$css: true as const, }, }; /** * Link color styles — applied to the element so the underline * and icon colors match the text color set by Text. */ const linkColorStyles = { primary: { kMwMTN: "x1tgivj0 xcunlro", $$css: true as const, }, secondary: { kMwMTN: "xv1l7n4 x7jm8ul", $$css: true as const, }, disabled: { kMwMTN: "xnbbluu", $$css: true as const, }, placeholder: { kMwMTN: "xv1l7n4", $$css: true as const, }, accent: { kMwMTN: "xjse4m1 x1rfoswn", $$css: true as const, }, inherit: { kMwMTN: "x1heor9g", $$css: true as const, }, }; export interface LinkProps extends BaseProps< HTMLAnchorElement | HTMLButtonElement > { /** Ref forwarded to the root element */ ref?: React.Ref; /** * Custom component to render instead of ``. * Overrides the provider-level default set by LinkProvider. * Must accept href, className, style, and children props. * Only used when href is provided. */ as?: LinkComponentType; /** * Accessible label for the link. * Used as aria-label when content is not self-descriptive * (e.g. icon-only links). When children are text, this is * unnecessary — the link text itself serves as the label. */ label?: string; /** * Link destination URL. * When undefined, renders as a ` ); } else if (isDisabled) { // A disabled link renders as a plain with no href: an href-less // anchor is not focusable and exposes no link affordance, so programmatic // focus + Enter, AT activation commands, and middle-click cannot navigate // or fire the consumer onClick. The router LinkComponent is deliberately // skipped — a disabled link performs no navigation, and custom router // links may require a live href. target/rel are omitted with the href. linkElement = ( } onClick={preventDefaultClick} aria-label={label || undefined} aria-disabled={true} tabIndex={-1} {...mergeProps( themeProps("link", { color, animatedUnderline: animatedUnderline ? "true" : undefined, }), focusOutlineProps.focusVisible( styles.base, linkColorStyles[color], hasUnderline && styles.hasUnderline, isStandalone && styles.standalone, styles.disabled, xstyle, ), className, style, )} {...props} > {sharedContent} ); } else { linkElement = ( } href={href} target={target} rel={rel} onClick={onClick} aria-label={label || undefined} aria-disabled={isDisabled || undefined} tabIndex={isDisabled ? -1 : undefined} {...mergeProps( themeProps("link", { color, animatedUnderline: animatedUnderline ? "true" : undefined, }), focusOutlineProps.focusVisible( styles.base, linkColorStyles[color], !isDisabled && interactionOverlayStyles.pressedBackgroundColor, hasUnderline && styles.hasUnderline, isStandalone && styles.standalone, isDisabled && styles.disabled, xstyle, ), className, style, )} {...props} > {sharedContent} ); } // Wrap with tooltip if provided if (tooltip) { return ( {linkElement} ); } return linkElement; } Link.displayName = "Link";