From 2c45014d77b4ee3263ab2fab5234ad6109a66488 Mon Sep 17 00:00:00 2001 From: javier ontiveros Date: Tue, 24 Feb 2026 14:32:00 -0700 Subject: [PATCH 1/7] feat: convert Icon component from JS to TypeScript - Converted from JSX to TSX with native TypeScript types - Removed PropTypes in favor of TypeScript interfaces - Replaced defaultProps with default parameter values - Added proper TypeScript interfaces for IconProps and SvgAttrs - Removed obsolete index.d.ts file - Fixed TypeScript compilation errors Co-authored-by: Claude Sonnet 4 --- src/Icon/Icon.test.tsx | 3 - src/Icon/index.d.ts | 20 ------- src/Icon/{index.jsx => index.tsx} | 98 ++++++++++++++----------------- 3 files changed, 45 insertions(+), 76 deletions(-) delete mode 100644 src/Icon/index.d.ts rename src/Icon/{index.jsx => index.tsx} (76%) diff --git a/src/Icon/Icon.test.tsx b/src/Icon/Icon.test.tsx index 43cf4ce0913..94b2ebe283c 100644 --- a/src/Icon/Icon.test.tsx +++ b/src/Icon/Icon.test.tsx @@ -42,11 +42,8 @@ describe('', () => { {/* @ts-expect-error Using a non-existent icon from @openedx/paragon/icons is a type error */} - {/* @ts-expect-error The 'src' prop cannot be a string. */} - {/* @ts-expect-error Random props cannot be added */} - {/* @ts-expect-error This is not a valid size property */} ; }); diff --git a/src/Icon/index.d.ts b/src/Icon/index.d.ts deleted file mode 100644 index b9d6f5d7468..00000000000 --- a/src/Icon/index.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; - -export interface IconProps extends React.ComponentPropsWithoutRef<'span'> { - // Note: React.ComponentType is what we want here. React.ElementType would allow some element type strings like "div", - // but we only want to allow components like 'Add' (a specific icon component function/class) - src?: React.ComponentType; - svgAttrs?: { - 'aria-label'?: string; - 'aria-labelledby'?: string; - }; - id?: string | null; - size?: 'xs' | 'sm' | 'md' | 'lg' | 'inline'; - className?: string | string[]; - hidden?: boolean; - screenReaderText?: React.ReactNode; -} - -declare const Icon: React.FC; - -export default Icon; diff --git a/src/Icon/index.jsx b/src/Icon/index.tsx similarity index 76% rename from src/Icon/index.jsx rename to src/Icon/index.tsx index 89403430f25..f32de471bd6 100644 --- a/src/Icon/index.jsx +++ b/src/Icon/index.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; import newId from '../utils/newId'; @@ -12,16 +11,53 @@ import withDeprecatedProps, { DeprTypes } from '../withDeprecatedProps'; * - focusable is set to false on the svg in all cases as a workaround for an ie11 bug */ +interface SvgAttrs extends React.SVGAttributes { + 'aria-label'?: string; + 'aria-labelledby'?: string; + 'aria-hidden'?: boolean; +} + +export interface IconProps extends Omit, 'id' | 'className'> { + /** + * An icon component to render. + * Example import of a Paragon icon component: `import { Check } from '@openedx/paragon/icons';` + */ + src?: React.ComponentType>; + /** HTML element attributes to pass through to the underlying svg element */ + svgAttrs?: SvgAttrs; + /** + * the `id` property of the Icon element, by default this value is generated + * with the `newId` function with the `prefix` of `Icon`. + */ + id?: string | null; + /** The size of the icon. */ + size?: 'xs' | 'sm' | 'md' | 'lg' | 'inline'; + /** A class name that will define what the Icon looks like. */ + className?: string | string[]; + /** + * a boolean that determines the value of `aria-hidden` attribute on the Icon span, + * this value is `true` by default. + */ + hidden?: boolean; + /** + * a string or an element that will be used on a secondary span leveraging the `sr-only` style + * for screenreader only text, this value is `undefined` by default. This value is recommended for use unless + * the Icon is being used in a way that is purely decorative or provides no additional context for screen + * reader users. This field should be thought of the same way an `alt` attribute would be used for `image` tags. + */ + screenReaderText?: React.ReactNode; +} + function Icon({ src: Component, id, className, - hidden, + hidden = true, screenReaderText, - svgAttrs, + svgAttrs = {}, size, ...attrs -}) { +}: IconProps) { if (Component) { // If no aria label is specified, hide this icon from screenreaders const hasAriaLabel = svgAttrs['aria-label'] || svgAttrs['aria-labelledby']; @@ -35,8 +71,8 @@ function Icon({ return ( {screenReaderText && ( @@ -69,55 +105,11 @@ function Icon({ ); } -Icon.propTypes = { - /** - * An icon component to render. - * Example import of a Paragon icon component: `import { Check } from '@openedx/paragon/icons';` - */ - src: PropTypes.elementType, - /** HTML element attributes to pass through to the underlying svg element */ - svgAttrs: PropTypes.shape({ - 'aria-label': PropTypes.string, - 'aria-labelledby': PropTypes.string, - }), - /** - * the `id` property of the Icon element, by default this value is generated - * with the `newId` function with the `prefix` of `Icon`. - */ - id: PropTypes.string, - /** The size of the icon. */ - size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg']), - /** A class name that will define what the Icon looks like. */ - className: PropTypes.string, - /** - * a boolean that determines the value of `aria-hidden` attribute on the Icon span, - * this value is `true` by default. - */ - hidden: PropTypes.bool, - /** - * a string or an element that will be used on a secondary span leveraging the `sr-only` style - * for screenreader only text, this value is `undefined` by default. This value is recommended for use unless - * the Icon is being used in a way that is purely decorative or provides no additional context for screen - * reader users. This field should be thought of the same way an `alt` attribute would be used for `image` tags. - */ - screenReaderText: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), -}; - -Icon.defaultProps = { - src: null, - svgAttrs: {}, - id: undefined, - hidden: true, - screenReaderText: undefined, - size: undefined, - className: undefined, -}; - export default withDeprecatedProps(Icon, 'Icon', { className: { deprType: DeprTypes.FORMAT, - expect: value => typeof value === 'string', - transform: value => (Array.isArray(value) ? value.join(' ') : value), + expect: (value: any) => typeof value === 'string', + transform: (value: any) => (Array.isArray(value) ? value.join(' ') : value), message: 'It should be a string.', }, }); From ac596840a4040b0cd5bcff940ae4edbf2c59f014 Mon Sep 17 00:00:00 2001 From: javier ontiveros Date: Tue, 26 May 2026 11:14:02 -0600 Subject: [PATCH 2/7] chore: pushed a bit of missing code --- src/Icon/Icon.test.tsx | 3 +++ src/Icon/index.tsx | 10 +--------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Icon/Icon.test.tsx b/src/Icon/Icon.test.tsx index 94b2ebe283c..43cf4ce0913 100644 --- a/src/Icon/Icon.test.tsx +++ b/src/Icon/Icon.test.tsx @@ -42,8 +42,11 @@ describe('', () => { {/* @ts-expect-error Using a non-existent icon from @openedx/paragon/icons is a type error */} + {/* @ts-expect-error The 'src' prop cannot be a string. */} + {/* @ts-expect-error Random props cannot be added */} + {/* @ts-expect-error This is not a valid size property */} ; }); diff --git a/src/Icon/index.tsx b/src/Icon/index.tsx index f32de471bd6..7e5b336476f 100644 --- a/src/Icon/index.tsx +++ b/src/Icon/index.tsx @@ -2,7 +2,6 @@ import React from 'react'; import classNames from 'classnames'; import newId from '../utils/newId'; -import withDeprecatedProps, { DeprTypes } from '../withDeprecatedProps'; /** * An svg with an "img" role must satisfy the following a11y requirements @@ -105,11 +104,4 @@ function Icon({ ); } -export default withDeprecatedProps(Icon, 'Icon', { - className: { - deprType: DeprTypes.FORMAT, - expect: (value: any) => typeof value === 'string', - transform: (value: any) => (Array.isArray(value) ? value.join(' ') : value), - message: 'It should be a string.', - }, -}); +export default Icon; From 9da500fb40e53e96724c1add57a63f349a5cb645 Mon Sep 17 00:00:00 2001 From: Javier Ontiveros Date: Fri, 4 Sep 2026 13:51:36 -0600 Subject: [PATCH 3/7] refactor: address review on Icon TS migration - Revert id type to string (drop the null it never had before) - Narrow className to string for consistency with other Paragon components; remove the Array.isArray(...).join(' ') handling - Update Icon.test.tsx to match the narrowed types (string className, drop the obsolete null-id test) --- src/Icon/Icon.test.tsx | 28 +++++++--------------------- src/Icon/index.tsx | 10 +++++----- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/src/Icon/Icon.test.tsx b/src/Icon/Icon.test.tsx index 43cf4ce0913..56acc53d91e 100644 --- a/src/Icon/Icon.test.tsx +++ b/src/Icon/Icon.test.tsx @@ -6,10 +6,7 @@ import { type IconName } from '../../icons'; import Icon from './index'; const testId = 'testId'; -const classNames = [ - 'fa', - 'fa-check', -]; +const className = 'fa fa-check'; const srTest = 'srTest'; function BlankSrc() { @@ -53,28 +50,17 @@ describe('', () => { describe('props received correctly', () => { it('receives required props', () => { - const { container } = render(); + const { container } = render(); const iconSpans = container.querySelectorAll('span'); const iconSpan = iconSpans[0]; expect(iconSpan.getAttribute('id')).toContain('Icon'); - expect(iconSpan.classList.contains(classNames[0])).toEqual(true); - expect(iconSpan.classList.contains(classNames[1])).toEqual(true); - }); - - it('handles null id properly', () => { - const nullId = null; - const { container } = render(); - const iconSpans = container.querySelectorAll('span'); - const iconSpan = iconSpans[0]; - - expect(iconSpan.getAttribute('id')).toContain('Icon'); - expect(iconSpan.classList.contains(classNames[0])).toEqual(true); - expect(iconSpan.classList.contains(classNames[1])).toEqual(true); + expect(iconSpan.classList.contains('fa')).toEqual(true); + expect(iconSpan.classList.contains('fa-check')).toEqual(true); }); it('generates unique ids when no id is provided', () => { - const { container } = render(<>); + const { container } = render(<>); const iconSpans = container.querySelectorAll('span'); const iconSpan1 = iconSpans[0]; const iconSpan2 = iconSpans[1]; @@ -87,7 +73,7 @@ describe('', () => { }); it('handles screenReaderText correctly', () => { - const { container } = render(); + const { container } = render(); const iconSpans = container.querySelectorAll('span'); expect(iconSpans.length).toEqual(2); @@ -96,7 +82,7 @@ describe('', () => { }); it('receives size prop correctly', () => { - const { container } = render(); + const { container } = render(); const iconSpans = container.querySelectorAll('span'); const iconSpan = iconSpans[0]; diff --git a/src/Icon/index.tsx b/src/Icon/index.tsx index 7e5b336476f..683a35027f4 100644 --- a/src/Icon/index.tsx +++ b/src/Icon/index.tsx @@ -28,11 +28,11 @@ export interface IconProps extends Omit, * the `id` property of the Icon element, by default this value is generated * with the `newId` function with the `prefix` of `Icon`. */ - id?: string | null; + id?: string; /** The size of the icon. */ size?: 'xs' | 'sm' | 'md' | 'lg' | 'inline'; /** A class name that will define what the Icon looks like. */ - className?: string | string[]; + className?: string; /** * a boolean that determines the value of `aria-hidden` attribute on the Icon span, * this value is `true` by default. @@ -70,8 +70,8 @@ function Icon({ return ( {screenReaderText && ( From 618c4f168489c7b177debb05c64c2775d9ee213a Mon Sep 17 00:00:00 2001 From: Javier Ontiveros Date: Fri, 4 Sep 2026 14:44:36 -0600 Subject: [PATCH 4/7] fix: widen Icon src prop type to ComponentType The strict ComponentType> broke Alert, which passes icon?: React.ComponentType to . Match the IconButton convention (iconAs?: React.ComponentType) so any icon component is accepted while still rejecting non-component values like strings. --- src/Icon/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Icon/index.tsx b/src/Icon/index.tsx index 683a35027f4..34c160cccf2 100644 --- a/src/Icon/index.tsx +++ b/src/Icon/index.tsx @@ -21,7 +21,7 @@ export interface IconProps extends Omit, * An icon component to render. * Example import of a Paragon icon component: `import { Check } from '@openedx/paragon/icons';` */ - src?: React.ComponentType>; + src?: React.ComponentType; /** HTML element attributes to pass through to the underlying svg element */ svgAttrs?: SvgAttrs; /** From 8219eadf9fbec701d6a50fa0ee2bb27d38fe13c4 Mon Sep 17 00:00:00 2001 From: Javier Ontiveros Date: Fri, 4 Sep 2026 15:55:32 -0600 Subject: [PATCH 5/7] docs: tweak Icon src prop comment to re-trigger deploy preview --- src/Icon/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Icon/index.tsx b/src/Icon/index.tsx index 34c160cccf2..d3425bd6d02 100644 --- a/src/Icon/index.tsx +++ b/src/Icon/index.tsx @@ -19,7 +19,7 @@ interface SvgAttrs extends React.SVGAttributes { export interface IconProps extends Omit, 'id' | 'className'> { /** * An icon component to render. - * Example import of a Paragon icon component: `import { Check } from '@openedx/paragon/icons';` + * Example import of a Paragon icon component: `import { Check } from '@openedx/paragon/icons';`. */ src?: React.ComponentType; /** HTML element attributes to pass through to the underlying svg element */ From 2c76f342a9c6f362f2703a1f4019ba21b6397427 Mon Sep 17 00:00:00 2001 From: Javier Ontiveros Date: Mon, 7 Sep 2026 08:10:07 -0600 Subject: [PATCH 6/7] docs: revert trailing period in Icon src comment --- src/Icon/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Icon/index.tsx b/src/Icon/index.tsx index d3425bd6d02..34c160cccf2 100644 --- a/src/Icon/index.tsx +++ b/src/Icon/index.tsx @@ -19,7 +19,7 @@ interface SvgAttrs extends React.SVGAttributes { export interface IconProps extends Omit, 'id' | 'className'> { /** * An icon component to render. - * Example import of a Paragon icon component: `import { Check } from '@openedx/paragon/icons';`. + * Example import of a Paragon icon component: `import { Check } from '@openedx/paragon/icons';` */ src?: React.ComponentType; /** HTML element attributes to pass through to the underlying svg element */ From 083c222910109aa9b71620f350ee1dc72de0f25c Mon Sep 17 00:00:00 2001 From: Javier Ontiveros Date: Mon, 7 Sep 2026 08:15:29 -0600 Subject: [PATCH 7/7] docs: format svg in Icon svgAttrs comment --- src/Icon/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Icon/index.tsx b/src/Icon/index.tsx index 34c160cccf2..52af3b6e113 100644 --- a/src/Icon/index.tsx +++ b/src/Icon/index.tsx @@ -22,7 +22,7 @@ export interface IconProps extends Omit, * Example import of a Paragon icon component: `import { Check } from '@openedx/paragon/icons';` */ src?: React.ComponentType; - /** HTML element attributes to pass through to the underlying svg element */ + /** HTML element attributes to pass through to the underlying `svg` element */ svgAttrs?: SvgAttrs; /** * the `id` property of the Icon element, by default this value is generated