diff --git a/packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/index.tsx b/packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/index.tsx index 54fb5aa866..d46cf49793 100644 --- a/packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/index.tsx +++ b/packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/index.tsx @@ -54,8 +54,15 @@ class BreadcrumbLink extends Component { } render() { - const { children, href, renderIcon, iconPlacement, onClick, onMouseEnter } = - this.props + const { + children, + href, + renderIcon, + iconPlacement, + onClick, + onMouseEnter, + isCurrentPage + } = this.props const props = omitProps(this.props, BreadcrumbLink.allowedProps) @@ -69,6 +76,7 @@ class BreadcrumbLink extends Component { onMouseEnter={onMouseEnter} isWithinText={false} elementRef={this.handleRef} + {...(isCurrentPage && { 'aria-current': 'page' })} > {children} diff --git a/packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/props.ts b/packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/props.ts index cca8fd0e45..e5f3b1eff9 100644 --- a/packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/props.ts +++ b/packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/props.ts @@ -63,6 +63,11 @@ type BreadcrumbLinkOwnProps = { * Place the icon before or after the text in the Breadcrumb.Link */ iconPlacement?: 'start' | 'end' + /** + * Whether the page this breadcrumb points to is the current one. If true, it sets aria-current="page". + * If this prop is not set to true on any breadcrumb element, the one recieving the aria-current="page" will always be the last element, unless the last element's isCurrentPage prop is explicity set to false. + */ + isCurrentPage?: boolean } type PropKeys = keyof BreadcrumbLinkOwnProps @@ -89,7 +94,8 @@ const propTypes: PropValidators = { onMouseEnter: PropTypes.func, size: PropTypes.oneOf(['small', 'medium', 'large']), renderIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - iconPlacement: PropTypes.oneOf(['start', 'end']) + iconPlacement: PropTypes.oneOf(['start', 'end']), + isCurrentPage: PropTypes.bool } const allowedProps: AllowedPropKeys = [ @@ -99,7 +105,8 @@ const allowedProps: AllowedPropKeys = [ 'onClick', 'onMouseEnter', 'renderIcon', - 'size' + 'size', + 'isCurrentPage' ] export type { BreadcrumbLinkProps } diff --git a/packages/ui-breadcrumb/src/Breadcrumb/README.md b/packages/ui-breadcrumb/src/Breadcrumb/README.md index 7f39e1bd8c..e08950daeb 100644 --- a/packages/ui-breadcrumb/src/Breadcrumb/README.md +++ b/packages/ui-breadcrumb/src/Breadcrumb/README.md @@ -23,7 +23,7 @@ type: example {(props, matches) => { if (matches.includes('tablet')) { return ( - + Student Forecast University of Utah University of Utah Colleges @@ -52,7 +52,7 @@ Change the `size` prop to control the font-size of the breadcrumbs (default is ` type: example ---
- + English 204 Rabbit Is Rich - + English 204 Rabbit Is Rich - + English 204 ``` + +```js +--- +type: embed +--- + +
+ + To indicate the current element within a breadcrumb, the aria-current attribute is used. In this component, aria-current="page" will automatically be applied to the last element, and we recommend that the current page always be the last element in the breadcrumb. If the last element is not the current page, the isCurrentPage property should be applied to the relevant Breadcrumb.Link to ensure compatibility with screen readers. + +
+
+``` diff --git a/packages/ui-breadcrumb/src/Breadcrumb/__new-tests__/Breadcrumb.test.tsx b/packages/ui-breadcrumb/src/Breadcrumb/__new-tests__/Breadcrumb.test.tsx index 7fdbb7ca55..03e156d048 100644 --- a/packages/ui-breadcrumb/src/Breadcrumb/__new-tests__/Breadcrumb.test.tsx +++ b/packages/ui-breadcrumb/src/Breadcrumb/__new-tests__/Breadcrumb.test.tsx @@ -111,4 +111,68 @@ describe('', () => { expect(icon).toBeInTheDocument() expect(icon).toHaveAttribute('aria-hidden', 'true') }) + + it('should add aria-current="page" to the last element by default', () => { + const { container } = render( + + {TEST_TEXT_01} + {TEST_TEXT_02} + + ) + const links = container.querySelectorAll('[class$="--block-link"]') + const firstLink = links[0] + const lastLink = links[links.length - 1] + + expect(firstLink).not.toHaveAttribute('aria-current', 'page') + expect(lastLink).toHaveAttribute('aria-current', 'page') + }) + + it('should add aria-current="page" to the element if isCurrent is true', () => { + const { container } = render( + + + {TEST_TEXT_01} + + {TEST_TEXT_02} + + ) + const links = container.querySelectorAll('[class$="--block-link"]') + const firstLink = links[0] + const lastLink = links[links.length - 1] + + expect(firstLink).toHaveAttribute('aria-current', 'page') + expect(lastLink).not.toHaveAttribute('aria-current', 'page') + }) + + it('should throw a warning when multiple elements have isCurrent set to true ', () => { + render( + + + {TEST_TEXT_01} + + {TEST_TEXT_02} + + ) + + expect(consoleWarningMock).toHaveBeenCalledWith( + expect.stringContaining( + 'Warning: Multiple elements with isCurrentPage=true found. Only one element should be set to current.' + ) + ) + }) + + it('should not add aria-current="page" to the last element if it set to false', () => { + const { container } = render( + + {TEST_TEXT_01} + {TEST_TEXT_02} + + ) + const links = container.querySelectorAll('[class$="--block-link"]') + const firstLink = links[0] + const lastLink = links[links.length - 1] + + expect(firstLink).not.toHaveAttribute('aria-current', 'page') + expect(lastLink).not.toHaveAttribute('aria-current', 'page') + }) }) diff --git a/packages/ui-breadcrumb/src/Breadcrumb/index.tsx b/packages/ui-breadcrumb/src/Breadcrumb/index.tsx index 03f12dc385..ae2509f8ca 100644 --- a/packages/ui-breadcrumb/src/Breadcrumb/index.tsx +++ b/packages/ui-breadcrumb/src/Breadcrumb/index.tsx @@ -62,6 +62,16 @@ class Breadcrumb extends Component { this.ref = el } + addAriaCurrent = (child: React.ReactNode) => { + const updatedChild = React.cloneElement( + child as React.ReactElement<{ 'aria-current'?: string }>, + { + 'aria-current': 'page' + } + ) + return updatedChild + } + componentDidMount() { this.props.makeStyles?.() } @@ -77,10 +87,28 @@ class Breadcrumb extends Component { const inlineStyle = { maxWidth: `${Math.floor(100 / numChildren)}%` } + let isAriaCurrentSet = false + return React.Children.map(children, (child, index) => { + const isLastElement = index === numChildren - 1 + if (React.isValidElement(child)) { + const isCurrentPage = child.props.isCurrentPage || false + if (isAriaCurrentSet && isCurrentPage) { + console.warn( + `Warning: Multiple elements with isCurrentPage=true found. Only one element should be set to current.` + ) + } + if (isCurrentPage) { + isAriaCurrentSet = true + } + } return (
  • - {child} + {!isAriaCurrentSet && + isLastElement && + (child as React.ReactElement).props.isCurrentPage !== false + ? this.addAriaCurrent(child) + : child} {index < numChildren - 1 && ( )}