Skip to content

Commit e314e15

Browse files
authored
feat(design-system): add color prop to DsIcon [AR-56004] (#433)
1 parent c925bf0 commit e314e15

5 files changed

Lines changed: 138 additions & 9 deletions

File tree

.changeset/thirty-horses-smile.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@drivenets/design-system': patch
3+
---
4+
5+
Add `color` prop to `DsIcon`
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { FC, SVGProps } from 'react';
2+
import { describe, expect, it, vi } from 'vitest';
3+
import { page } from 'vitest/browser';
4+
import DsIcon from '../ds-icon';
5+
import { iconColors } from '../ds-icon.types';
6+
7+
const TestSvg: FC<SVGProps<SVGSVGElement>> = (props) => (
8+
<svg data-testid="test-svg" viewBox="0 0 24 24" {...props}>
9+
<circle cx="12" cy="12" r="10" fill="currentColor" />
10+
</svg>
11+
);
12+
13+
describe('DsIcon', () => {
14+
it('should render a material icon', async () => {
15+
await page.render(<DsIcon icon="home" />);
16+
17+
const icon = page.getByText('home');
18+
await expect.element(icon).toBeVisible();
19+
});
20+
21+
it('should render a custom SVG icon', async () => {
22+
await page.render(<DsIcon icon="special-home" />);
23+
24+
const svg = page.elementLocator(document.querySelector('svg') as unknown as HTMLElement);
25+
await expect.element(svg).toBeInTheDocument();
26+
});
27+
28+
it('should render an inline SVG component', async () => {
29+
await page.render(<DsIcon icon={TestSvg} />);
30+
31+
await expect.element(page.getByTestId('test-svg')).toBeInTheDocument();
32+
});
33+
34+
describe('color prop', () => {
35+
it.each(iconColors.map((c) => [c]))(
36+
'should resolve semantic color "%s" to var(--icon-%s)',
37+
async (color) => {
38+
await page.render(<DsIcon icon="home" color={color} />);
39+
40+
const icon = page.getByText('home');
41+
await expect.element(icon).toHaveAttribute('style', expect.stringContaining(`var(--icon-${color})`));
42+
},
43+
);
44+
45+
it('should pass CSS variables through unchanged', async () => {
46+
await page.render(<DsIcon icon="home" color="var(--my-custom-color)" />);
47+
48+
const icon = page.getByText('home');
49+
await expect.element(icon).toHaveAttribute('style', expect.stringContaining('var(--my-custom-color)'));
50+
});
51+
52+
it('should apply color to a custom SVG icon', async () => {
53+
await page.render(<DsIcon icon="special-home" color="success" />);
54+
55+
const svg = page.elementLocator(document.querySelector('svg') as unknown as HTMLElement);
56+
await expect.element(svg).toHaveAttribute('style', expect.stringContaining('var(--icon-success)'));
57+
});
58+
59+
it('should apply color to an inline SVG component', async () => {
60+
await page.render(<DsIcon icon={TestSvg} color="error" />);
61+
62+
const svg = page.getByTestId('test-svg');
63+
await expect.element(svg).toHaveAttribute('style', expect.stringContaining('var(--icon-error)'));
64+
});
65+
66+
it('should let style.color override the color prop', async () => {
67+
await page.render(<DsIcon icon="home" color="error" style={{ color: 'var(--icon-warning)' }} />);
68+
69+
const icon = page.getByText('home');
70+
await expect.element(icon).toHaveAttribute('style', expect.stringContaining('var(--icon-warning)'));
71+
});
72+
});
73+
74+
it('should call onClick when clicked', async () => {
75+
const onClick = vi.fn();
76+
77+
await page.render(<DsIcon icon="home" onClick={onClick} />);
78+
79+
await page.getByText('home').click();
80+
expect(onClick).toHaveBeenCalledOnce();
81+
});
82+
});

packages/design-system/src/components/ds-icon/ds-icon.stories.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useEffect, useMemo, useState } from 'react';
33
import DsIcon from './ds-icon';
44
import './ds-icon.stories.scss';
55
import { materialIcons } from './material-icons';
6-
import { iconSizes, iconVariants, type IconName } from './ds-icon.types';
6+
import { iconColors, iconSizes, iconVariants, type IconName } from './ds-icon.types';
77
import { customIcons, type CustomIconName } from './custom-icons';
88

99
const meta: Meta<typeof DsIcon> = {
@@ -31,6 +31,10 @@ const meta: Meta<typeof DsIcon> = {
3131
control: 'boolean',
3232
description: 'Whether the icon should be filled',
3333
},
34+
color: {
35+
control: { type: 'select' },
36+
options: iconColors,
37+
},
3438
onClick: { action: 'clicked' },
3539
},
3640
};
@@ -49,13 +53,15 @@ export const Colored: Story = {
4953
args: {
5054
size: 'medium',
5155
filled: true,
52-
style: { color: '#4CAF50' },
5356
},
5457
render: function Render(args) {
5558
return (
56-
<div>
57-
<DsIcon {...args} icon="check_circle" />
58-
<DsIcon {...args} icon="special-market" />
59+
<div style={{ display: 'flex', gap: 16 }}>
60+
<DsIcon {...args} icon="check_circle" color="success" />
61+
<DsIcon {...args} icon="error" color="error" />
62+
<DsIcon {...args} icon="warning" color="warning" />
63+
<DsIcon {...args} icon="info" color="information-main" />
64+
<DsIcon {...args} icon="special-market" color="action" />
5965
</div>
6066
);
6167
},

packages/design-system/src/components/ds-icon/ds-icon.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import classNames from 'classnames';
22
import styles from './ds-icon.module.scss';
3-
import type { DsIconProps } from './ds-icon.types';
3+
import { iconColors, type DsIconProps } from './ds-icon.types';
44
import { customIcons, isCustomIcon } from './custom-icons';
55

6+
const iconColorSet = new Set<string>(iconColors);
7+
8+
const resolveColor = (color: string): string => (iconColorSet.has(color) ? `var(--icon-${color})` : color);
9+
610
/**
711
* Design system Icon component that renders Google Material Icons, custom SVG icons, or inline SVGs
812
*/
@@ -11,29 +15,31 @@ const DsIcon = ({
1115
size = 'medium',
1216
variant = 'outlined',
1317
filled,
18+
color,
1419
className = '',
1520
style = {},
1621
...rest
1722
}: DsIconProps) => {
1823
const iconClass = classNames(styles.icon, styles[size], { [styles.filled]: filled }, className);
24+
const mergedStyle = color ? { color: resolveColor(color), ...style } : style;
1925

2026
// 1. SVG component passed directly
2127
if (typeof icon === 'function') {
2228
const SvgComponent = icon;
23-
return <SvgComponent className={iconClass} style={style} {...rest} />;
29+
return <SvgComponent className={iconClass} style={mergedStyle} {...rest} />;
2430
}
2531

2632
// 2. Custom registered icon from custom-icons registry
2733
if (isCustomIcon(icon)) {
2834
const SvgComponent = customIcons[icon];
29-
return <SvgComponent className={iconClass} style={style} {...rest} />;
35+
return <SvgComponent className={iconClass} style={mergedStyle} {...rest} />;
3036
}
3137

3238
// 3. Material Icon (font-based)
3339
const variantClass = `material-symbols-${variant}`;
3440

3541
return (
36-
<span className={classNames(iconClass, variantClass)} style={style} {...rest}>
42+
<span className={classNames(iconClass, variantClass)} style={mergedStyle} {...rest}>
3743
{icon}
3844
</span>
3945
);

packages/design-system/src/components/ds-icon/ds-icon.types.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@ export type IconSize = (typeof iconSizes)[number];
77
export const iconVariants = ['outlined', 'rounded'] as const;
88
export type IconVariant = (typeof iconVariants)[number];
99

10+
export const iconColors = [
11+
'action',
12+
'action-hover',
13+
'action-secondary',
14+
'disabled',
15+
'error',
16+
'execution',
17+
'information-main',
18+
'information-secondary',
19+
'inverse',
20+
'main',
21+
'on-button',
22+
'on-dark-disabled',
23+
'pause',
24+
'pending',
25+
'secondary',
26+
'success',
27+
'success-light',
28+
'warning',
29+
'warning-light',
30+
] as const;
31+
export type IconColor = (typeof iconColors)[number];
32+
1033
export type MaterialIconName = {
1134
[K in keyof typeof materialIcons]: K extends `${IconPrefix}::${infer Name}` ? Name : never;
1235
}[keyof typeof materialIcons];
@@ -39,6 +62,13 @@ export interface DsIconProps {
3962
*/
4063
filled?: boolean;
4164

65+
/**
66+
* Icon color. Semantic names map to `--icon-*` tokens (e.g. `'error'` →
67+
* `var(--icon-error)`); raw CSS values (hex, rgb, hsl, CSS variables)
68+
* pass through unchanged. Omit to inherit from parent.
69+
*/
70+
color?: IconColor | (string & {});
71+
4272
/**
4373
* Additional CSS class names
4474
*/

0 commit comments

Comments
 (0)