From 3585dd58b445a2a213eff0cc5302ffa71a151d18 Mon Sep 17 00:00:00 2001 From: ManoharPaturi <186662190+manoharpaturi@users.noreply.github.com> Date: Fri, 4 Sep 2026 23:56:12 +0530 Subject: [PATCH 1/4] fix(core): render plain Link anchors inline so an ancestor Text clamp can truncate them --- .changeset/link-anchor-clamp.md | 6 ++++ packages/core/src/Link/Link.doc.mjs | 6 ++++ packages/core/src/Link/Link.test.tsx | 52 ++++++++++++++++++++++++++++ packages/core/src/Link/Link.tsx | 28 ++++++++++++--- packages/core/src/Text/Text.doc.mjs | 14 ++++++++ 5 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 .changeset/link-anchor-clamp.md diff --git a/.changeset/link-anchor-clamp.md b/.changeset/link-anchor-clamp.md new file mode 100644 index 0000000000000..8360121a8bead --- /dev/null +++ b/.changeset/link-anchor-clamp.md @@ -0,0 +1,6 @@ +--- +'@astryxdesign/core': patch +--- + +[fix] Render a plain Link anchor as `inline` so an ancestor `` clamp can truncate it; flex layout is kept for the external-link icon and button forms, and Link/Text docs now cover the external-link clamp limitation. (#6021) +@ManoharPaturi diff --git a/packages/core/src/Link/Link.doc.mjs b/packages/core/src/Link/Link.doc.mjs index 48cc492bdb4f2..b68e77160411a 100644 --- a/packages/core/src/Link/Link.doc.mjs +++ b/packages/core/src/Link/Link.doc.mjs @@ -169,10 +169,12 @@ export const docs = { bestPractices: [ { guidance: true, description: 'Write descriptive, concise link text that clearly communicates the destination.' }, { guidance: true, description: 'Set `isStandalone` when the link appears outside of inline text, so it receives proper base font sizing.' }, + { guidance: true, description: 'Let a plain Link be truncated by an ancestor ; the anchor participates in the surrounding line boxes.' }, { guidance: true, description: 'Only set `label` when the link content is not descriptive text (e.g. an icon-only link). For text links, the visible text is already the accessible name; adding `label` overrides it for screen readers, which is harmful.' }, { guidance: false, description: 'Use Link for actions that do not navigate; use a Button instead.' }, { guidance: false, description: 'Use generic text like "click here" or "read more"; describe the destination.' }, { guidance: false, description: 'Set `label` on text links; `aria-label` prevents assistive technology from reading the actual link content.' }, + { guidance: false, description: 'Expect an ancestor to truncate an external Link. Its external-link icon uses an inline-flex layout, which an ancestor clamp cannot reach — pass `maxLines` to the Link itself.' }, ], anatomy: [ {name: 'Label', required: true, description: 'The visible text of the link.'}, @@ -301,10 +303,12 @@ export const docsZh = { bestPractices: [ { guidance: true, description: 'Write descriptive, concise link text that clearly communicates the destination.' }, { guidance: true, description: 'Set `isStandalone` when the link appears outside of inline text, so it receives proper base font sizing.' }, + { guidance: true, description: 'Let a plain Link be truncated by an ancestor ; the anchor participates in the surrounding line boxes.' }, { guidance: true, description: 'Only set `label` when the link content is not descriptive text (e.g. an icon-only link). For text links, the visible text is already the accessible name; adding `label` overrides it for screen readers, which is harmful.' }, { guidance: false, description: 'Use Link for actions that do not navigate; use a Button instead.' }, { guidance: false, description: 'Use generic text like "click here" or "read more"; describe the destination.' }, { guidance: false, description: 'Set `label` on text links; `aria-label` prevents assistive technology from reading the actual link content.' }, + { guidance: false, description: 'Expect an ancestor to truncate an external Link. Its external-link icon uses an inline-flex layout, which an ancestor clamp cannot reach — pass `maxLines` to the Link itself.' }, ], anatomy: [ {name: 'Label', required: true, description: 'The visible text of the link.'}, @@ -324,10 +328,12 @@ export const docsDense = { bestPractices: [ { guidance: true, description: 'Write descriptive, concise link text that clearly communicates the destination.' }, { guidance: true, description: 'Set `isStandalone` when the link appears outside of inline text, so it receives proper base font sizing.' }, + { guidance: true, description: 'Let a plain Link be truncated by an ancestor ; the anchor participates in the surrounding line boxes.' }, { guidance: true, description: 'Only set `label` when the link content is not descriptive text (e.g. an icon-only link). For text links, the visible text is already the accessible name; adding `label` overrides it for screen readers, which is harmful.' }, { guidance: false, description: 'Use Link for actions that do not navigate; use a Button instead.' }, { guidance: false, description: 'Use generic text like "click here" or "read more"; describe the destination.' }, { guidance: false, description: 'Set `label` on text links; `aria-label` prevents assistive technology from reading the actual link content.' }, + { guidance: false, description: 'Expect an ancestor to truncate an external Link. Its external-link icon uses an inline-flex layout, which an ancestor clamp cannot reach — pass `maxLines` to the Link itself.' }, ], anatomy: [ {name: 'Label', required: true, description: 'The visible text of the link.'}, diff --git a/packages/core/src/Link/Link.test.tsx b/packages/core/src/Link/Link.test.tsx index fafaf5c561286..a7cb19421f30c 100644 --- a/packages/core/src/Link/Link.test.tsx +++ b/packages/core/src/Link/Link.test.tsx @@ -13,6 +13,7 @@ import {describe, it, expect, vi} from 'vitest'; import {fireEvent, render, screen} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import {Link} from './Link'; +import {getAllInjectedCss} from '../__tests__/forcedColors'; import {LinkProvider} from './LinkProvider'; function CustomLink({ @@ -415,3 +416,54 @@ describe('Link', () => { expect(link.className).toContain('secondary'); }); }); + +// ============================================================================= +// Display: an ancestor clamp () can only truncate an anchor +// that participates in the surrounding line boxes, so the plain anchor must +// be `inline`; flex layout is reserved for the icon/button forms (#6021). +// ============================================================================= + +function displayDeclarationsFor(el: Element): string[] { + const classes = Array.from(el.classList); + const declarations: string[] = []; + for (const chunk of getAllInjectedCss().split('}')) { + const sel = chunk.split('{')[0] ?? ''; + const body = chunk.split('{')[1] ?? ''; + if (classes.some(c => sel.includes(c)) && body.includes('display')) { + const match = body.match(/display:\s*([^;]+);/); + if (match) { + declarations.push(match[1].trim()); + } + } + } + return declarations; +} + +describe('Link display', () => { + it('renders a plain anchor inline so an ancestor clamp can reach it', () => { + const {container} = render(Documentation); + const anchor = container.querySelector('a')!; + expect(displayDeclarationsFor(anchor)).toContain('inline'); + expect(displayDeclarationsFor(anchor)).not.toContain('inline-flex'); + }); + + it('keeps flex layout for the external-link icon form', () => { + const {container} = render( + + External docs + , + ); + const anchor = container.querySelector('a')!; + expect(displayDeclarationsFor(anchor)).toContain('inline-flex'); + }); + + it('keeps flex layout for the button-rendered form', () => { + const {container} = render( + {}} role="button"> + Action + , + ); + const button = container.querySelector('button')!; + expect(displayDeclarationsFor(button)).toContain('inline-flex'); + }); +}); diff --git a/packages/core/src/Link/Link.tsx b/packages/core/src/Link/Link.tsx index 72ffa07cdd048..6dcaa9300b4a7 100644 --- a/packages/core/src/Link/Link.tsx +++ b/packages/core/src/Link/Link.tsx @@ -52,9 +52,12 @@ import {useTranslator} from '../i18n'; */ const styles = stylex.create({ base: { - display: 'inline-flex', - alignItems: 'center', - gap: spacingVars['--spacing-0-5'], + // `inline` (not `inline-flex`) so the anchor participates in the + // surrounding line boxes and an ancestor clamp (e.g. ) + // can truncate it. An inline-flex box establishes its own formatting + // context, which an ancestor -webkit-line-clamp cannot reach — + // ... silently did nothing. + display: 'inline', fontFamily: 'inherit', fontSize: 'inherit', lineHeight: 'inherit', @@ -84,6 +87,17 @@ const styles = stylex.create({ pointerEvents: 'auto', position: 'relative', }, + /** + * Flex layout for the cases that need it: external links append an icon + * after the text (icon centering + gap), and the button-rendered form + * keeps its previous inline-flex layout. On plain anchors this is + * deliberately NOT applied — see the `base` display note. + */ + flexLayout: { + display: 'inline-flex', + alignItems: 'center', + gap: spacingVars['--spacing-0-5'], + }, hasUnderline: { textDecoration: 'underline', }, @@ -329,6 +343,7 @@ export function Link({ // render as a