diff --git a/.changeset/lucky-socks-carry.md b/.changeset/lucky-socks-carry.md new file mode 100644 index 0000000000..6a0e8fdc67 --- /dev/null +++ b/.changeset/lucky-socks-carry.md @@ -0,0 +1,5 @@ +--- +'@primer/react-brand': patch +--- + +Added descriptive labels to `Footnote` return links for an improved screen-reader experience. Return links will now be read aloud as "Back to content {Link label}" diff --git a/package-lock.json b/package-lock.json index 0cadee09ab..0bf979e58a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,7 +83,7 @@ }, "apps/storybook": { "name": "@primer/brand-storybook", - "version": "0.60.1", + "version": "0.61.0", "license": "MIT", "devDependencies": { "@babel/preset-env": "7.26.9", @@ -23689,7 +23689,7 @@ }, "packages/css": { "name": "@primer/brand-css", - "version": "0.60.1", + "version": "0.61.0", "license": "UNLICENSED", "devDependencies": { "@types/node": "22.18.1", @@ -23705,7 +23705,7 @@ }, "packages/design-tokens": { "name": "@primer/brand-primitives", - "version": "0.60.1", + "version": "0.61.0", "license": "MIT", "devDependencies": { "@primer/primitives": "9.1.1", @@ -23719,7 +23719,7 @@ }, "packages/e2e": { "name": "@primer/brand-e2e", - "version": "0.60.1", + "version": "0.61.0", "license": "MIT", "devDependencies": { "@github/axe-github": "^0.5.0", @@ -23735,7 +23735,7 @@ }, "packages/fonts": { "name": "@primer/brand-fonts", - "version": "0.60.1", + "version": "0.61.0", "license": "MIT", "engines": { "node": ">=22.0.0", @@ -23744,7 +23744,7 @@ }, "packages/react": { "name": "@primer/react-brand", - "version": "0.60.1", + "version": "0.61.0", "license": "MIT", "dependencies": { "@oddbird/popover-polyfill": "0.5.2", @@ -23901,7 +23901,7 @@ }, "packages/repo-configs": { "name": "@primer/brand-config", - "version": "0.60.1", + "version": "0.61.0", "license": "MIT" } } diff --git a/packages/react/src/Footnotes/Footnotes.test.tsx b/packages/react/src/Footnotes/Footnotes.test.tsx index fbaa32118e..b0f3cb06b8 100644 --- a/packages/react/src/Footnotes/Footnotes.test.tsx +++ b/packages/react/src/Footnotes/Footnotes.test.tsx @@ -4,6 +4,7 @@ import '@testing-library/jest-dom' import {axe, toHaveNoViolations} from 'jest-axe' import {Footnotes} from './Footnotes' +import {InlineLink} from '../InlineLink' import '../test-utils/mocks/match-media-mock' expect.extend(toHaveNoViolations) @@ -89,18 +90,35 @@ describe('Footnotes', () => { const mockHref = 'https://github.com' const {getByText, getByRole} = render( - {mockItemText} + {mockItemText} , ) const itemEl = getByText(mockItemText) expect(itemEl).toBeInTheDocument() - const backLink = getByRole('link', {name: 'Back to content'}) + const backLink = getByRole('link', {name: `Back to content ${mockItemText}`}) expect(backLink).toBeInTheDocument() expect(backLink).toHaveAttribute('href', mockHref) const iconEl = backLink.querySelector('svg') expect(iconEl).toBeInTheDocument() }) + + it('extracts text content from nested children for aria-label', async () => { + const mockHref = 'https://github.com' + const {getByRole} = render( + + + This factor is based on data from the industry's{' '} + longest running analysis by Acme Corp. + + , + ) + + const backLink = getByRole('link', { + name: "Back to content This factor is based on data from the industry's longest running analysis by Acme Corp.", + }) + expect(backLink).toBeInTheDocument() + }) }) diff --git a/packages/react/src/Footnotes/Footnotes.tsx b/packages/react/src/Footnotes/Footnotes.tsx index 2fe3834b13..01101ca837 100644 --- a/packages/react/src/Footnotes/Footnotes.tsx +++ b/packages/react/src/Footnotes/Footnotes.tsx @@ -7,6 +7,7 @@ import type {BaseProps} from '../component-helpers' /** * Main Stylesheet (as a CSS Module) */ import styles from './Footnotes.module.css' +import {getTextContent} from '../utils/getTextContent' export const FootnotesTags = ['div', 'ol'] as const @@ -92,7 +93,7 @@ function Item({className, children, _variant = 'citation', ...rest}: FootnotesIt {children} {href && ( - + )} diff --git a/packages/react/src/utils/getTextContent.ts b/packages/react/src/utils/getTextContent.ts new file mode 100644 index 0000000000..63a45051c7 --- /dev/null +++ b/packages/react/src/utils/getTextContent.ts @@ -0,0 +1,20 @@ +import React from 'react' + +export function getTextContent(node: React.ReactNode): string { + if (!node) return '' + + if (typeof node === 'string' || typeof node === 'number') { + return String(node) + } + + if (Array.isArray(node)) { + return node.map(getTextContent).join('') + } + + if (React.isValidElement(node)) { + const props = node.props as {children?: React.ReactNode} + return getTextContent(props.children) + } + + return '' +}