Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 7 additions & 21 deletions src/Icon/Icon.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -53,28 +50,17 @@ describe('<Icon />', () => {

describe('props received correctly', () => {
it('receives required props', () => {
const { container } = render(<Icon className={classNames} />);
const { container } = render(<Icon className={className} />);
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(<Icon id={nullId} className={classNames} />);
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(<><Icon className={classNames} /><Icon className={classNames} /></>);
const { container } = render(<><Icon className={className} /><Icon className={className} /></>);
const iconSpans = container.querySelectorAll('span');
const iconSpan1 = iconSpans[0];
const iconSpan2 = iconSpans[1];
Expand All @@ -87,7 +73,7 @@ describe('<Icon />', () => {
});

it('handles screenReaderText correctly', () => {
const { container } = render(<Icon id={testId} className={classNames} screenReaderText={srTest} />);
const { container } = render(<Icon id={testId} className={className} screenReaderText={srTest} />);
const iconSpans = container.querySelectorAll('span');

expect(iconSpans.length).toEqual(2);
Expand All @@ -96,7 +82,7 @@ describe('<Icon />', () => {
});

it('receives size prop correctly', () => {
const { container } = render(<Icon src={BlankSrc} className={classNames} size="xs" />);
const { container } = render(<Icon src={BlankSrc} className={className} size="xs" />);
const iconSpans = container.querySelectorAll('span');
const iconSpan = iconSpans[0];

Expand Down
20 changes: 0 additions & 20 deletions src/Icon/index.d.ts

This file was deleted.

98 changes: 41 additions & 57 deletions src/Icon/index.jsx → src/Icon/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
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
Expand All @@ -12,16 +10,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<SVGElement> {
'aria-label'?: string;
'aria-labelledby'?: string;
'aria-hidden'?: boolean;
}

export interface IconProps extends Omit<React.ComponentPropsWithoutRef<'span'>, 'id' | 'className'> {
/**
* An icon component to render.
* Example import of a Paragon icon component: `import { Check } from '@openedx/paragon/icons';`
*/
src?: React.ComponentType<any>;
/** 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;
/** The size of the icon. */
size?: 'xs' | 'sm' | 'md' | 'lg' | 'inline';
/** A class name that will define what the Icon looks like. */
className?: 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'];
Expand Down Expand Up @@ -69,55 +104,4 @@ 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),
message: 'It should be a string.',
},
});
export default Icon;