Skip to content

Commit

Permalink
FCT 663: add LeadingIcon component (#2701)
Browse files Browse the repository at this point in the history
* feat(leading icon): initial implementation of leading icon, add storybook

* feat(leading icon): update behavior when neither svg nor icon prop is passed to component

* feat(leading icon): remove stale comment

* feat(leading icon): simplify getSize function

* feat(leading icon): add unit and visual tests, update icon README, refactor component to handle styling for customsvgs

* feat(leading icon): incorporate style map from Carlos' PR, update styling for size 10 to make icons 22X22, update styling for light white theme to make sure icon size is same as in other colors, update chageset, remove default values, update tests based on feedback

* feat(leading icon): add inverted state to custom svg background, update tests

* feat(leading icon): update leading icon description in icons README, remove unnecessary styling functions in leading-icon.styles.ts, update visual spec to be a bit less verbose

* feat(leading icon): update readme correctly this time

* Update packages/components/icons/README.md

Co-authored-by: Stephanie Sprinkle <[email protected]>

* feat(leading icon): add type for color theme styles

---------

Co-authored-by: Stephanie Sprinkle <[email protected]>
  • Loading branch information
ByronDWall and stephsprinkle authored Jan 31, 2024
1 parent f31886c commit a0abf51
Show file tree
Hide file tree
Showing 12 changed files with 420 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/dull-hounds-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@commercetools-uikit/icons': minor
---

We've added a new component to manage icons when you need to render an icon with a themed bounding-box. Icons can be either from the `ui-kit` icons set or a custom SVG.
33 changes: 33 additions & 0 deletions packages/components/icons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,36 @@ The `data` passed to the component is run through a DOM sanitizer to prevent unw
### Where to use

This component can be used whenever the icon has to be rendered dynamically on runtime. For example in the Merchant Center this can be the case for the navigation menu icons, etc.

## Leading Icon

The leading icon is a an eye-catching visual element that should be used when an additional visual prominence is needed for a content section in the UI. The different colours in combination with the icons can be utilised to create certain categorisation of the elements in the UI.

The component is exported as a separate entry point:

```js
import LeadingIcon from '@commercetools-uikit/icons/leading-icon';
```

### Usage

```js
import LeadingIcon from '@commercetools-uikit/icons/leading-icon';
import { ExportIcon } from '@commercetools-uikit/icons';

const app = () => <LeadingIcon icon={<ExportIcon />} />;
```

### Properties

| Props | Type | Required | Values | Default | Description |
| ------------ | -------------- | :------: | ------------------------------------------------------------ | --------- | --------------------------------------------------------------------------------------------------------------------------- |
| `size` | `string` | | '10', '20', '30', '40' | '20' | Specifies the icon size |
| `color` | `string` | | 'accent', 'brown', 'neutral', 'purple', 'turquoise', 'white' | 'neutral' | Specifies the icon's background color and fill color |
| `isInverted` | `boolean` | | `true`, `false` | `false` | Specifies whether the icon has a light background and dark fill (`false`), or dark background and light fill (`true`) |
| `icon` | `ReactElement` | | UI Kit `<Icon/>` component | | Icon that is displayed within the component, you must supply a child icon with with this prop or the `svg` prop |
| `svg` | `string` | | A custom SVG to display | | Icon that is displayed using the `InlineSvg` component, you must supply a child icon with with this prop or the `icon` prop |

### Where to use

This component can be used wherever it is necessary to display a themed icon.
4 changes: 4 additions & 0 deletions packages/components/icons/leading-icon/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"main": "dist/commercetools-uikit-icons-leading-icon.cjs.js",
"module": "dist/commercetools-uikit-icons-leading-icon.esm.js"
}
8 changes: 6 additions & 2 deletions packages/components/icons/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@
"main": "dist/commercetools-uikit-icons.cjs.js",
"module": "dist/commercetools-uikit-icons.esm.js",
"preconstruct": {
"entrypoints": ["./index.ts", "./inline-svg/index.ts"]
"entrypoints": [
"./index.ts",
"./inline-svg/index.ts",
"./leading-icon/index.ts"
]
},
"files": ["dist", "inline-svg"],
"files": ["dist", "inline-svg", "leading-icon"],
"scripts": {
"generate-icons": "svgr -d src/generated -- src/svg"
},
Expand Down
5 changes: 5 additions & 0 deletions packages/components/icons/src/fixtures/raw-svg.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 24 additions & 3 deletions packages/components/icons/src/icon.story.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { createElement, useState } from 'react';
import PropTypes from 'prop-types';
import { storiesOf } from '@storybook/react';
import { withKnobs, select } from '@storybook/addon-knobs/react';
import { withKnobs, select, boolean } from '@storybook/addon-knobs/react';
import styled from '@emotion/styled';
import { css } from '@emotion/react';
import { designTokens } from '@commercetools-uikit/design-system';
Expand All @@ -17,6 +17,7 @@ import Text from '../../text';
import Readme from '../README.md';
import xssFixtures from './fixtures/xss';
import InlineSvg from './inline-svg';
import LeadingIcon from './leading-icon';
import * as icons from '.';

const DEPRECATED_ICONS_NAMES = [
Expand Down Expand Up @@ -73,6 +74,7 @@ const colorValues = [
'warning',
'error',
];

const sizeValues = ['small', 'medium', 'big', 'scale'];
const svgFixtures = {
cleanSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M13.7324356,13 C13.3866262,13.5978014 12.7402824,14 12,14 C10.8954305,14 10,13.1045695 10,12 C10,11.2597176 10.4021986,10.6133738 11,10.2675644 L11,7 L11,7 C11,6.44771525 11.4477153,6 12,6 C12.5522847,6 13,6.44771525 13,7 L13,10.2675644 C13.303628,10.4432037 13.5567963,10.696372 13.7324356,11 L15,11 C15.5522847,11 16,11.4477153 16,12 C16,12.5522847 15.5522847,13 15,13 L13.7324356,13 Z M12,21 C7.02943725,21 3,16.9705627 3,12 C3,7.02943725 7.02943725,3 12,3 C16.9705627,3 21,7.02943725 21,12 C21,16.9705627 16.9705627,21 12,21 Z M12,19 C15.8659932,19 19,15.8659932 19,12 C19,8.13400675 15.8659932,5 12,5 C8.13400675,5 5,8.13400675 5,12 C5,15.8659932 8.13400675,19 12,19 Z"/></svg>`,
Expand Down Expand Up @@ -271,4 +273,23 @@ storiesOf('Components|Icons', module)
<Section>
<InlineSvgPage />
</Section>
));
))
.add('LeadingIcon', () => {
// storybook knobs escape input data to html, so we cannot use them to send unescaped svg, so setting it here using a boolean
const svg = boolean('use custom svg', false);
return (
<Section>
<LeadingIcon
color={select(
'color',
['accent', 'brown', 'turquoise', 'purple', 'neutral', 'white'],
'neutral'
)}
size={select('size', ['10', '20', '30', '40'], '20')}
isInverted={boolean('isInverted', false)}
icon={createElement(icons[select('icon', iconNames, iconNames[0])])}
svg={svg ? svgFixtures.cleanSvg : null}
/>
</Section>
);
});
83 changes: 80 additions & 3 deletions packages/components/icons/src/icons.visualroute.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@ import { Switch, Route } from 'react-router-dom';
import { designTokens } from '@commercetools-uikit/design-system';
import * as icons from '@commercetools-uikit/icons';
import InlineSvg from '@commercetools-uikit/icons/inline-svg';
import LeadingIcon from '@commercetools-uikit/icons/leading-icon';
import Text from '@commercetools-uikit/text';
import Spacings from '@commercetools-uikit/spacings';
import rawSvg from './fixtures/raw-svg';
import { Suite, Spec } from '../../../../test/percy';

const IconList = styled.div`
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
grid-template-columns: repeat(8, 1fr);
`;

const LeadingIconList = styled.div`
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 16px;
`;

const IconItem = styled.div`
Expand All @@ -20,6 +28,10 @@ const IconItem = styled.div`
flex: 1;
`;

const LeadingIconItem = styled(IconItem)`
gap: 16px;
`;

const IconContainer = styled.div`
margin: 8px 0;
display: flex;
Expand All @@ -42,7 +54,16 @@ const colors = [
'error',
];

const inlineSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#000000" fill-rule="evenodd" d="M13.7324356,13 C13.3866262,13.5978014 12.7402824,14 12,14 C10.8954305,14 10,13.1045695 10,12 C10,11.2597176 10.4021986,10.6133738 11,10.2675644 L11,7 L11,7 C11,6.44771525 11.4477153,6 12,6 C12.5522847,6 13,6.44771525 13,7 L13,10.2675644 C13.303628,10.4432037 13.5567963,10.696372 13.7324356,11 L15,11 C15.5522847,11 16,11.4477153 16,12 C16,12.5522847 15.5522847,13 15,13 L13.7324356,13 Z M12,21 C7.02943725,21 3,16.9705627 3,12 C3,7.02943725 7.02943725,3 12,3 C16.9705627,3 21,7.02943725 21,12 C21,16.9705627 16.9705627,21 12,21 Z M12,19 C15.8659932,19 19,15.8659932 19,12 C19,8.13400675 15.8659932,5 12,5 C8.13400675,5 5,8.13400675 5,12 C5,15.8659932 8.13400675,19 12,19 Z"/></svg>`;
const leadingIconColors = [
'accent',
'brown',
'turquoise',
'purple',
'neutral',
'white',
];
const leadingIconSizes = ['10', '20', '30', '40'];
const IconForLeadingIcon = icons[allIconNames[0]];

export const routePath = '/icons';

Expand Down Expand Up @@ -73,6 +94,11 @@ export const component = () => (
<li>
<a href={`${routePath}/inline-svg`}>{`${routePath}/inline-svg`}</a>
</li>
<li>
<a
href={`${routePath}/leading-icon`}
>{`${routePath}/leading-icon`}</a>
</li>
</ul>
</Route>
{colors.map((color) => (
Expand Down Expand Up @@ -111,7 +137,7 @@ export const component = () => (
}}
key={`${size}-${color}`}
>
<InlineSvg data={inlineSvg} color={color} size={size} />
<InlineSvg data={rawSvg.clock} color={color} size={size} />
</div>
))}
</IconList>
Expand All @@ -120,5 +146,56 @@ export const component = () => (
</Spacings.Stack>
</Suite>
</Route>
<Route exact path={`${routePath}/leading-icon`}>
<Suite>
{leadingIconSizes.map((size) => (
<Spec key={size} label={`Leading Icon - Size: ${size}`} omitPropsList>
<LeadingIconList>
{leadingIconColors.map((color) => (
<LeadingIconItem key={`${size}-${color}`}>
<Spacings.Stack alignItems="center">
<LeadingIcon
size={size}
color={color}
icon={<IconForLeadingIcon />}
/>
<Text.Detail>{`${color}`}</Text.Detail>
</Spacings.Stack>
<Spacings.Stack alignItems="center">
<LeadingIcon
size={size}
color={color}
icon={<IconForLeadingIcon />}
isInverted={true}
/>
<Text.Detail>{`inverted`}</Text.Detail>
</Spacings.Stack>
</LeadingIconItem>
))}
</LeadingIconList>
</Spec>
))}
<Spec label={`Leading Icon - Custom SVG`} omitPropsList>
<LeadingIconList label={`Leading Icon - Custom SVG`}>
{leadingIconSizes.map((size) => (
<LeadingIconItem key={size}>
<Spacings.Stack alignItems="center">
<LeadingIcon size={size} svg={rawSvg.clock} />
<Text.Detail>{`custom-svg size ${size}`}</Text.Detail>
</Spacings.Stack>
<Spacings.Stack alignItems="center">
<LeadingIcon
size={size}
svg={rawSvg.clock}
isInverted={true}
/>
<Text.Detail>{`inverted`}</Text.Detail>
</Spacings.Stack>
</LeadingIconItem>
))}
</LeadingIconList>
</Spec>
</Suite>
</Route>
</Switch>
);
5 changes: 5 additions & 0 deletions packages/components/icons/src/icons.visualspec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,9 @@ describe('Icons', () => {
await page.waitForSelector('text/Inline SVG');
await percySnapshot(page, `Icons - Inline SVG`);
});
it('Leading Icon', async () => {
await page.goto(`${globalThis.HOST}/icons/leading-icon`);
await page.waitForSelector('text/Leading Icon');
await percySnapshot(page, `Icons - Leading Icon`);
});
});
1 change: 1 addition & 0 deletions packages/components/icons/src/leading-icon/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './leading-icon';
41 changes: 41 additions & 0 deletions packages/components/icons/src/leading-icon/leading-icon.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ArrowLeftIcon } from '../generated';
import { screen, render } from '../../../../../test/test-utils';
import rawSvg from '../fixtures/raw-svg';
import LeadingIcon, { type TLeadingIconProps } from './leading-icon';

type TLeadingIconTestProps = Pick<
TLeadingIconProps,
'color' | 'size' | 'icon' | 'isInverted'
> & {
'data-testid'?: string;
'aria-label': string;
};

const createTestProps = (
custom?: Partial<TLeadingIconTestProps>
): TLeadingIconTestProps => ({
color: 'neutral',
size: '20',
icon: <ArrowLeftIcon aria-label="arrowLeft" />,
'aria-label': 'leading-icon',
...custom,
});

describe('LeadingIcon', () => {
let props: Partial<TLeadingIconTestProps>;
beforeEach(() => {
props = createTestProps();
});
it('should render a react component and pass aria attributes', async () => {
render(<LeadingIcon {...props} />);
await screen.findByRole('img', { name: 'leading-icon' });
});
it('should pass data attributes', async () => {
render(<LeadingIcon {...props} data-testid="test-testid" />);
await screen.findByTestId('test-testid');
});
it('should render a custom svg when svg prop is passed', async () => {
render(<LeadingIcon svg={rawSvg.clock} />);
await screen.findByLabelText('custom clock svg');
});
});
Loading

0 comments on commit a0abf51

Please sign in to comment.