From a11ba9e1cde74a3e5bc725173122d17c1fdc33c8 Mon Sep 17 00:00:00 2001 From: Jakob Guddas Date: Thu, 17 Apr 2025 21:02:05 +0200 Subject: [PATCH] fix(react): added aria-hidden fallback for decorative icons (#2158) --- packages/lucide-react/src/Icon.ts | 3 +- packages/lucide-react/tests/Icon.spec.tsx | 60 +++++++++++++++++++ .../tests/__snapshots__/Icon.spec.tsx.snap | 1 + .../createLucideIcon.spec.tsx.snap | 3 + .../__snapshots__/lucide-react.spec.tsx.snap | 3 + packages/shared/src/utils.ts | 14 +++++ 6 files changed, 83 insertions(+), 1 deletion(-) diff --git a/packages/lucide-react/src/Icon.ts b/packages/lucide-react/src/Icon.ts index c623c0e3e..5f243b129 100644 --- a/packages/lucide-react/src/Icon.ts +++ b/packages/lucide-react/src/Icon.ts @@ -1,7 +1,7 @@ import { createElement, forwardRef } from 'react'; import defaultAttributes from './defaultAttributes'; import { IconNode, LucideProps } from './types'; -import { mergeClasses } from '@lucide/shared'; +import { mergeClasses, hasA11yProp } from '@lucide/shared'; interface IconComponentProps extends LucideProps { iconNode: IconNode; @@ -46,6 +46,7 @@ const Icon = forwardRef( stroke: color, strokeWidth: absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth, className: mergeClasses('lucide', className), + ...(!children && !hasA11yProp(rest) && { 'aria-hidden': 'true' }), ...rest, }, [ diff --git a/packages/lucide-react/tests/Icon.spec.tsx b/packages/lucide-react/tests/Icon.spec.tsx index 906f0c311..22f0837b4 100644 --- a/packages/lucide-react/tests/Icon.spec.tsx +++ b/packages/lucide-react/tests/Icon.spec.tsx @@ -31,3 +31,63 @@ describe('Using Icon Component', () => { expect(container.firstChild).toMatchSnapshot(); }); }); + +describe('Icon Component Accessibility', () => { + it('should not have aria-hidden prop when aria prop is present', async () => { + const { container } = render( + , + ); + + expect(container.firstChild).not.toHaveAttribute('aria-hidden'); + }); + + it('should not have aria-hidden prop when title prop is present', async () => { + const { container } = render( + , + ); + + expect(container.firstChild).not.toHaveAttribute('aria-hidden'); + }); + + it('should not have aria-hidden prop when there are children that could be a element', async () => { + const { container } = render( + <Icon + iconNode={airVent} + size={48} + stroke="red" + absoluteStrokeWidth + > + <title>Some title + , + ); + + expect(container.firstChild).not.toHaveAttribute('aria-hidden'); + }); + + it('should never override aria-hidden prop', async () => { + const { container } = render( + , + ); + + expect(container.firstChild).toHaveAttribute('aria-hidden', 'false'); + }); +}); diff --git a/packages/lucide-react/tests/__snapshots__/Icon.spec.tsx.snap b/packages/lucide-react/tests/__snapshots__/Icon.spec.tsx.snap index 635ca428a..956aa8168 100644 --- a/packages/lucide-react/tests/__snapshots__/Icon.spec.tsx.snap +++ b/packages/lucide-react/tests/__snapshots__/Icon.spec.tsx.snap @@ -2,6 +2,7 @@ exports[`Using Icon Component > should render icon and match snapshot 1`] = `