Browse Source

Stop passing extra props to every node in `@tabler/icons-react-native` (#1628)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
pull/1635/head
Paweł Kuna 6 days ago
committed by GitHub
parent
commit
c78c1b1fe3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      packages/icons-react-native/README.md
  2. 33
      packages/icons-react-native/src/createReactNativeComponent.ts
  3. 66
      packages/icons-react-native/test.spec.tsx

2
packages/icons-react-native/README.md

@ -70,7 +70,7 @@ Pass props to adjust the icon:
| `size` | _number | string_ | 24 | Width and height of the icon |
| `color` | _string_ | currentColor | Stroke color for outline icons, fill color for filled icons |
| `strokeWidth` | _number | string_ | 2 | Stroke width, outline icons only |
| `title` | _string_ | – | Adds a `<title>` element for accessibility |
| `title` | _string_ | – | Accessibility label read by screen readers (sets `accessible`, `accessibilityRole="image"` and `accessibilityLabel` on the `Svg`) |
Any other prop is forwarded to the underlying `Svg` component. Components forward refs and ship with TypeScript declarations.

33
packages/icons-react-native/src/createReactNativeComponent.ts

@ -14,13 +14,32 @@ const createReactNativeComponent = (
{ color = 'currentColor', size = 24, strokeWidth = 2, title, children, ...rest }: IconProps,
ref,
) => {
const customAttrs = {
// Only the paint attributes are shared with the icon's nodes, because the
// nodes set them explicitly and would not inherit them from the root.
// Every other prop (`testID`, `opacity`, `onPress`, `style`, …) belongs to
// the root `Svg` alone — repeating it on each node would apply it twice.
const paintAttrs: Record<string, unknown> = {
...childDefaultAttributes[type],
stroke: type === 'filled' ? 'none' : color,
fill: type === 'filled' ? color : 'none',
strokeWidth,
...rest,
};
for (const key of Object.keys(paintAttrs)) {
const value = (rest as Record<string, unknown>)[key];
if (value != null) {
paintAttrs[key] = value;
}
}
// React Native has no `<title>` element, so the title is exposed to
// screen readers through the accessibility props instead. Explicitly
// passed accessibility props still take precedence.
const titleAttrs = title
? { accessible: true, accessibilityRole: 'image', accessibilityLabel: title }
: {};
return createElement(
NativeSvg.Svg as unknown as string,
{
@ -28,8 +47,9 @@ const createReactNativeComponent = (
...defaultAttributes[type],
width: size,
height: size,
...customAttrs,
...titleAttrs,
...rest,
...paintAttrs,
},
[
...iconNode.map(([tag, attrs]) => {
@ -38,13 +58,10 @@ const createReactNativeComponent = (
return createElement(
NativeSvg[upperCasedTag] as FunctionComponent<IconProps>,
{ ...childDefaultAttributes[type], ...customAttrs, ...attrs } as IconProps,
{ ...paintAttrs, ...attrs } as IconProps,
);
}),
[
title && createElement('title', { key: 'svg-title' }, title),
...((Array.isArray(children) ? children : [children]) || []),
],
...(Array.isArray(children) ? children : [children]),
],
);
},

66
packages/icons-react-native/test.spec.tsx

@ -1,12 +1,25 @@
import { describe, it, expect, afterEach } from 'vitest';
import { render, cleanup } from '@testing-library/react';
import { Path } from 'react-native-svg';
import type { ReactElement } from 'react';
import { act, create } from 'react-test-renderer';
import { Path, Svg } from 'react-native-svg';
import {
IconAccessible,
IconAccessibleFilled,
createReactComponent,
} from './src/tabler-icons-react-native';
// props the icon passes to the root `Svg`, before the DOM mock drops the ones
// that are not valid DOM attributes (e.g. `accessible`)
const renderSvgProps = (element: ReactElement) => {
let renderer!: ReturnType<typeof create>;
act(() => {
renderer = create(element);
});
return renderer.root.findByType(Svg).props;
};
describe('React Native Icon component', () => {
afterEach(() => {
cleanup();
@ -61,6 +74,31 @@ describe('React Native Icon component', () => {
expect(svg.getAttribute('opacity')).toBe('0.5');
});
it('should not repeat other props on the icon nodes', () => {
const { container } = render(<IconAccessible opacity={0.5} accessibilityLabel="icon" />);
const paths = Array.from(container.getElementsByTagName('path'));
expect(paths.length).toBe(3);
paths.forEach((path) => {
expect(path.getAttribute('opacity')).toBe(null);
expect(path.getAttribute('accessibilityLabel')).toBe(null);
});
});
it('should still apply explicit stroke and fill props to the svg and its nodes', () => {
const { container } = render(
<IconAccessible stroke="blue" fill="yellow" strokeLinecap="square" />,
);
const svg = container.getElementsByTagName('svg')[0];
const path = container.getElementsByTagName('path')[0];
expect(svg.getAttribute('stroke')).toBe('blue');
expect(svg.getAttribute('fill')).toBe('yellow');
expect(path.getAttribute('stroke')).toBe('blue');
expect(path.getAttribute('fill')).toBe('yellow');
expect(path.getAttribute('stroke-linecap')).toBe('square');
});
it('should render children after the icon nodes', () => {
const { container } = render(
<IconAccessible>
@ -73,12 +111,30 @@ describe('React Native Icon component', () => {
expect(paths[3].getAttribute('d')).toBe('M0 0h24');
});
it('should add title child element to svg when title prop is passed', () => {
it('should expose the title as accessibility props instead of a title element', () => {
const { container } = render(<IconAccessible title="Accessible Icon" />);
const svg = container.getElementsByTagName('svg')[0];
const title = svg.getElementsByTagName('title')[0];
expect(container.getElementsByTagName('title').length).toBe(0);
const svgProps = renderSvgProps(<IconAccessible title="Accessible Icon" />);
expect(svgProps.accessible).toBe(true);
expect(svgProps.accessibilityRole).toBe('image');
expect(svgProps.accessibilityLabel).toBe('Accessible Icon');
});
it('should let explicit accessibility props override the title', () => {
const svgProps = renderSvgProps(
<IconAccessible title="Accessible Icon" accessibilityLabel="Custom label" />,
);
expect(svgProps.accessibilityLabel).toBe('Custom label');
});
it('should not add accessibility props without a title', () => {
const svgProps = renderSvgProps(<IconAccessible />);
expect(title).toHaveTextContent('Accessible Icon');
expect(svgProps.accessible).toBeUndefined();
expect(svgProps.accessibilityRole).toBeUndefined();
expect(svgProps.accessibilityLabel).toBeUndefined();
});
it('should set display name of the component', () => {

Loading…
Cancel
Save