1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 01:39:44 +01:00

Icon house keeping updates (#16621)

This commit is contained in:
George Marshall 2022-11-23 09:58:43 -08:00 committed by GitHub
parent 65f2f17695
commit ab808b670a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 239 additions and 133 deletions

View File

@ -27,7 +27,7 @@ describe('AvatarFavicon', () => {
const { container } = render(
<AvatarFavicon data-testid="avatar-favicon" />,
);
expect(container.getElementsByClassName('icon')).toHaveLength(1);
expect(container.getElementsByClassName('mm-icon')).toHaveLength(1);
});
it('should render fallback image with custom fallbackIconProps if no ImageSource is provided', () => {

View File

@ -8,8 +8,8 @@ exports[`ButtonIcon should render button element correctly 1`] = `
data-testid="button-icon"
>
<div
class="box icon icon--size-lg box--flex-direction-row box--color-inherit"
style="mask-image: url('./images/icons/icon-add-square-filled.svg;"
class="box mm-icon mm-icon--size-lg box--flex-direction-row box--color-inherit"
style="mask-image: url('./images/icons/icon-add-square-filled.svg');"
/>
</button>
</div>

View File

@ -79,7 +79,7 @@ describe('ButtonLink', () => {
<ButtonLink data-testid="icon" icon="add-square-filled" />,
);
const icons = container.getElementsByClassName('icon').length;
const icons = container.getElementsByClassName('mm-icon').length;
expect(icons).toBe(1);
});
});

View File

@ -92,7 +92,7 @@ describe('ButtonPrimary', () => {
<ButtonPrimary data-testid="icon" icon="add-square-filled" />,
);
const icons = container.getElementsByClassName('icon').length;
const icons = container.getElementsByClassName('mm-icon').length;
expect(icons).toBe(1);
});
});

View File

@ -96,7 +96,7 @@ describe('ButtonSecondary', () => {
<ButtonSecondary data-testid="icon" icon="add-square-filled" />,
);
const icons = container.getElementsByClassName('icon').length;
const icons = container.getElementsByClassName('mm-icon').length;
expect(icons).toBe(1);
});
});

View File

@ -20,21 +20,22 @@ The `Icon` accepts all props below as well as all [Box](/docs/ui-components-ui-b
Use the `name` prop and the `ICON_NAMES` object to change the icon.
Use the [IconSearch](/ui-components-component-library-icon-icon-stories-js--name) story to find the icon you want to use.
Use the [IconSearch](/story/ui-components-component-library-icon-icon-stories-js--default-story) story to find the icon you want to use.
<Canvas>
<Story id="ui-components-component-library-icon-icon-stories-js--name" />
</Canvas>
```jsx
import { Icon, ICON_NAMES } from '../../ui/components/component-library';
import { Icon, ICON_NAMES } from '../../components/component-library';
<Icon name={ICON_NAMES.ADD_SQUARE_FILLED} />
<Icon name={ICON_NAMES.BANK_FILLED} />
<Icon name={ICON_NAMES.CALCULATOR_FILLED} />
<Icon name={ICON_NAMES.COIN_FILLED} />
// etc...
```
<Canvas>
<Story id="ui-components-component-library-icon-icon-stories-js--name" />
</Canvas>
### Size
Use the `size` prop and the `SIZES` object from `./ui/helpers/constants/design-system.js` to change the size of `Icon`. Defaults to `SIZES.SM`
@ -55,7 +56,7 @@ Possible sizes include:
```jsx
import { SIZES } from '../../../helpers/constants/design-system';
import { Icon, ICON_NAMES } from '../../ui/components/component-library';
import { Icon, ICON_NAMES } from '../../components/component-library';
<Icon name={ICON_NAMES.ADD_SQUARE_FILLED} size={SIZES.XXS} />
<Icon name={ICON_NAMES.ADD_SQUARE_FILLED} size={SIZES.XS} />
@ -79,7 +80,7 @@ Use the `color` prop and the `COLORS` object from `./ui/helpers/constants/design
```jsx
import { COLORS } from '../../../helpers/constants/design-system';
import { Icon, ICON_NAMES } from '../../ui/components/component-library';
import { Icon, ICON_NAMES } from '../../components/component-library';
<Icon name={ICON_NAMES.ADD_SQUARE_FILLED} color={COLORS.INHERIT} />
<Icon name={ICON_NAMES.ADD_SQUARE_FILLED} color={COLORS.ICON_DEFAULT} />

View File

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Icon should render correctly 1`] = `
<div>
<div
class="box mm-icon mm-icon--size-md box--flex-direction-row box--color-inherit"
data-testid="icon"
style="mask-image: url('./images/icons/icon-add-square-filled.svg');"
/>
</div>
`;

View File

@ -1,11 +1,25 @@
import { SIZES } from '../../../helpers/constants/design-system';
/**
* The ICON_NAMES object contains all the possible icon names.
* It is generated using the generateIconNames script in development/generate-icon-names.js
* and stored in the environment variable ICON_NAMES
* To add a new icon, add the icon svg file to app/images/icons
* Ensure the svg has been optimized, is kebab case and starts with "icon-"
* See "Adding a new icon" in ./README.md for more details
*
* Search for an icon: https://metamask.github.io/metamask-storybook/?path=/story/ui-components-component-library-icon-icon-stories-js--default-story
*
* Add an icon: https://metamask.github.io/metamask-storybook/?path=/docs/ui-components-component-library-icon-icon-stories-js--default-story#adding-a-new-icon
*
* ICON_NAMES is generated using svgs in app/images/icons and
* the generateIconNames script in development/generate-icon-names.js
* then stored as an environment variable
*/
/* eslint-disable prefer-destructuring*/ // process.env is not a standard JavaScript object, so we are not able to use object destructuring
export const ICON_NAMES = JSON.parse(process.env.ICON_NAMES);
export const ICON_SIZES = {
XXS: SIZES.XXS,
XS: SIZES.XS,
SM: SIZES.SM,
MD: SIZES.MD,
LG: SIZES.LG,
XL: SIZES.XL,
AUTO: SIZES.AUTO,
};

View File

@ -10,6 +10,8 @@ import {
ICON_COLORS,
} from '../../../helpers/constants/design-system';
import { ICON_SIZES } from './icon.constants';
export const Icon = ({
name,
size = SIZES.MD,
@ -21,15 +23,15 @@ export const Icon = ({
return (
<Box
color={color}
className={classnames(className, 'icon', `icon--size-${size}`)}
className={classnames(className, 'mm-icon', `mm-icon--size-${size}`)}
style={{
/**
* To reduce the possibility of injection attacks
* the icon component uses mask-image instead of rendering
* the svg directly.
*/
maskImage: `url('./images/icons/icon-${name}.svg`,
WebkitMaskImage: `url('./images/icons/icon-${name}.svg`,
maskImage: `url('./images/icons/icon-${name}.svg')`,
WebkitMaskImage: `url('./images/icons/icon-${name}.svg')`,
...style,
}}
{...props}
@ -44,10 +46,10 @@ Icon.propTypes = {
name: PropTypes.string.isRequired, // Can't set PropTypes.oneOf(ICON_NAMES) because ICON_NAMES is an environment variable
/**
* The size of the Icon.
* Possible values could be 'SIZES.XXS', 'SIZES.XS', 'SIZES.SM', 'SIZES.MD', 'SIZES.LG', 'SIZES.XL',
* Default value is 'SIZES.MD'.
* Possible values could be SIZES.XXS (10px), SIZES.XS (12px), SIZES.SM (16px), SIZES.MD (20px), SIZES.LG (24px), SIZES.XL (32px),
* Default value is SIZES.MD (20px).
*/
size: PropTypes.oneOf(Object.values(SIZES)),
size: PropTypes.oneOf(Object.values(ICON_SIZES)),
/**
* The color of the icon.
* Defaults to COLORS.INHERIT.

View File

@ -1,4 +1,4 @@
.icon {
.mm-icon {
--icon-size: var(--size, 16px);
font-size: var(--icon-size);

View File

@ -8,12 +8,23 @@ import {
FLEX_DIRECTION,
JUSTIFY_CONTENT,
TEXT,
FLEX_WRAP,
TEXT_ALIGN,
} from '../../../helpers/constants/design-system';
import Box from '../../ui/box/box';
import { Text } from '../text';
import { Icon } from './icon';
import { ICON_NAMES } from './icon.constants';
import Box from '../../ui/box/box';
import {
ButtonIcon,
ButtonLink,
ICON_NAMES,
ICON_SIZES,
Icon,
Label,
Text,
TextField,
TextFieldSearch,
} from '..';
import README from './README.mdx';
@ -51,7 +62,7 @@ export default {
},
size: {
control: 'select',
options: Object.values(SIZES),
options: Object.values(ICON_SIZES),
},
color: {
control: 'select',
@ -88,11 +99,7 @@ export default {
},
};
export const DefaultStory = (args) => <Icon {...args} />;
DefaultStory.storyName = 'Default';
export const Name = (args) => {
export const DefaultStory = (args) => {
const [search, setSearch] = useState('');
const iconList = Object.keys(ICON_NAMES)
.filter(
@ -106,98 +113,134 @@ export const Name = (args) => {
setSearch(e.target.value);
};
const handleOnClear = () => {
setSearch('');
};
return (
<>
<Text as="h2" marginBottom={2} variant={TEXT.HEADING_MD}>
Icon search
</Text>
<Box display={DISPLAY.FLEX}>
<Box
marginBottom={4}
borderColor={COLORS.BORDER_DEFAULT}
borderRadius={SIZES.SM}
as="input"
type="text"
onChange={handleSearch}
value={search}
placeholder="Search"
paddingLeft={2}
paddingRight={2}
style={{
height: '40px',
width: '100%',
maxWidth: '300px',
fontSize: 'var(--typography-l-body-md-font-size)',
}}
/>
</Box>
<Box
display={DISPLAY.GRID}
gap={2}
style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))' }}
style={{
gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
}}
>
{iconList.length > 0 ? (
<>
{iconList.map((item) => {
return (
<Box
borderColor={COLORS.BORDER_MUTED}
borderRadius={SIZES.MD}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
alignItems={ALIGN_ITEMS.CENTER}
justifyContent={JUSTIFY_CONTENT.CENTER}
padding={4}
key={item}
>
<Box>
<Icon marginBottom={2} {...args} name={ICON_NAMES[item]} />
</Box>
<Text
variant={TEXT.BODY_XS}
as="pre"
style={{ cursor: 'pointer' }}
backgroundColor={COLORS.BACKGROUND_ALTERNATIVE}
paddingLeft={2}
paddingRight={2}
paddingTop={1}
paddingBottom={1}
borderRadius={SIZES.SM}
title="Copy to clipboard"
onClick={() => {
const tempEl = document.createElement('textarea');
tempEl.value = item;
document.body.appendChild(tempEl);
tempEl.select();
document.execCommand('copy');
document.body.removeChild(tempEl);
}}
>
{item}
</Text>
</Box>
);
})}
</>
) : (
<Text>
No matches. Please try again or ask in the{' '}
<Text
as="a"
color={COLORS.PRIMARY_DEFAULT}
href="https://consensys.slack.com/archives/C0354T27M5M"
target="_blank"
>
#metamask-design-system
</Text>{' '}
channel on slack.
</Text>
)}
<Box
style={{ gridColumnStart: 1, gridColumnEnd: 3 }}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
>
{/* TODO replace with FormTextField */}
<Label htmlFor="icon-search">Name</Label>
<TextFieldSearch
id="icon-search"
marginBottom={4}
onChange={handleSearch}
clearButtonOnClick={handleOnClear}
value={search}
placeholder="Search icon name"
/>
</Box>
</Box>
{iconList.length > 0 ? (
<Box
display={DISPLAY.GRID}
gap={2}
style={{
gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
}}
>
{iconList.map((item) => {
return (
<Box
borderColor={COLORS.BORDER_MUTED}
borderRadius={SIZES.MD}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
alignItems={ALIGN_ITEMS.CENTER}
justifyContent={JUSTIFY_CONTENT.CENTER}
padding={4}
key={item}
>
<Icon marginBottom={2} {...args} name={ICON_NAMES[item]} />
<TextField
placeholder={item}
value={item}
readOnly
size={SIZES.SM}
inputProps={{
variant: TEXT.BODY_XS,
textAlign: TEXT_ALIGN.CENTER,
}}
backgroundColor={COLORS.BACKGROUND_ALTERNATIVE}
rightAccessory={
<ButtonIcon
icon={ICON_NAMES.COPY_FILLED}
size={SIZES.SM}
color={COLORS.ICON_ALTERNATIVE}
ariaLabel="Copy to clipboard"
title="Copy to clipboard"
onClick={() => {
const tempEl = document.createElement('textarea');
tempEl.value = item;
document.body.appendChild(tempEl);
tempEl.select();
document.execCommand('copy');
document.body.removeChild(tempEl);
}}
/>
}
/>
</Box>
);
})}
</Box>
) : (
<Text>
No matches. Please try again or ask in the{' '}
<ButtonLink
size={SIZES.AUTO}
color={COLORS.PRIMARY_DEFAULT}
href="https://consensys.slack.com/archives/C0354T27M5M"
target="_blank"
>
#metamask-design-system
</ButtonLink>{' '}
channel on slack.
</Text>
)}
</>
);
};
DefaultStory.storyName = 'Default';
export const Name = (args) => (
<>
<Box display={DISPLAY.FLEX} flexWrap={FLEX_WRAP.WRAP} gap={2}>
{Object.keys(ICON_NAMES).map((item) => {
console.log('item:', item);
return (
<Box
borderColor={COLORS.BORDER_MUTED}
borderRadius={SIZES.MD}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
alignItems={ALIGN_ITEMS.CENTER}
justifyContent={JUSTIFY_CONTENT.CENTER}
padding={4}
key={item}
>
<Icon {...args} name={ICON_NAMES[item]} />
</Box>
);
})}
</Box>
</>
);
export const Size = (args) => (
<>
@ -220,6 +263,7 @@ export const Size = (args) => (
</Text>
</>
);
export const Color = (args) => (
<Box display={DISPLAY.FLEX} alignItems={ALIGN_ITEMS.BASELINE}>
<Box padding={1} display={DISPLAY.FLEX} alignItems={ALIGN_ITEMS.CENTER}>

View File

@ -12,6 +12,37 @@ describe('Icon', () => {
);
expect(getByTestId('icon')).toBeDefined();
expect(container.querySelector('svg')).toBeDefined();
expect(container).toMatchSnapshot();
});
it('should render with a custom class', () => {
const { getByTestId } = render(
<Icon
name={ICON_NAMES.ADD_SQUARE_FILLED}
data-testid="icon"
className="test-class"
/>,
);
expect(getByTestId('icon')).toHaveClass('test-class');
});
it('should render with an aria-label attribute', () => {
/**
* We aren't specifically adding an ariaLabel prop because in most cases
* the icon should be decorative or be accompanied by text. Also if the icon
* is to be used as a button in most cases ButtonIcon should be used. However
* we should test if it's possible to pass an aria-label attribute to the
* root html element.
*/
const { getByTestId } = render(
<Icon
name={ICON_NAMES.ADD_SQUARE_FILLED}
data-testid="icon"
aria-label="test aria label"
/>,
);
expect(getByTestId('icon')).toHaveAttribute(
'aria-label',
'test aria label',
);
});
it('should render with different icons using mask-image and image urls', () => {
const { getByTestId } = render(
@ -33,16 +64,16 @@ describe('Icon', () => {
);
expect(
window.getComputedStyle(getByTestId('icon-add-square-filled')).maskImage,
).toBe(`url('./images/icons/icon-add-square-filled.svg`);
).toBe(`url('./images/icons/icon-add-square-filled.svg')`);
expect(
window.getComputedStyle(getByTestId('icon-bank-filled')).maskImage,
).toBe(`url('./images/icons/icon-bank-filled.svg`);
).toBe(`url('./images/icons/icon-bank-filled.svg')`);
expect(
window.getComputedStyle(getByTestId('icon-bookmark-filled')).maskImage,
).toBe(`url('./images/icons/icon-bookmark-filled.svg`);
).toBe(`url('./images/icons/icon-bookmark-filled.svg')`);
expect(
window.getComputedStyle(getByTestId('icon-calculator-filled')).maskImage,
).toBe(`url('./images/icons/icon-calculator-filled.svg`);
).toBe(`url('./images/icons/icon-calculator-filled.svg')`);
});
it('should render with different size classes', () => {
const { getByTestId } = render(
@ -84,13 +115,13 @@ describe('Icon', () => {
/>
</>,
);
expect(getByTestId('icon-xxs')).toHaveClass('icon--size-xxs');
expect(getByTestId('icon-xs')).toHaveClass('icon--size-xs');
expect(getByTestId('icon-sm')).toHaveClass('icon--size-sm');
expect(getByTestId('icon-md')).toHaveClass('icon--size-md');
expect(getByTestId('icon-lg')).toHaveClass('icon--size-lg');
expect(getByTestId('icon-xl')).toHaveClass('icon--size-xl');
expect(getByTestId('icon-auto')).toHaveClass('icon--size-auto');
expect(getByTestId('icon-xxs')).toHaveClass('mm-icon--size-xxs');
expect(getByTestId('icon-xs')).toHaveClass('mm-icon--size-xs');
expect(getByTestId('icon-sm')).toHaveClass('mm-icon--size-sm');
expect(getByTestId('icon-md')).toHaveClass('mm-icon--size-md');
expect(getByTestId('icon-lg')).toHaveClass('mm-icon--size-lg');
expect(getByTestId('icon-xl')).toHaveClass('mm-icon--size-xl');
expect(getByTestId('icon-auto')).toHaveClass('mm-icon--size-auto');
});
it('should render with icon colors', () => {
const { getByTestId } = render(

View File

@ -1,2 +1,2 @@
export { Icon } from './icon';
export { ICON_NAMES } from './icon.constants';
export { ICON_NAMES, ICON_SIZES } from './icon.constants';

View File

@ -12,7 +12,7 @@ export { ButtonPrimary } from './button-primary';
export { ButtonSecondary } from './button-secondary';
export { FormTextField } from './form-text-field';
export { HelpText } from './help-text';
export { Icon, ICON_NAMES } from './icon';
export { Icon, ICON_NAMES, ICON_SIZES } from './icon';
export { Label } from './label';
export { PickerNetwork } from './picker-network';
export { Tag } from './tag';
@ -24,3 +24,4 @@ export {
TEXT_FIELD_BASE_SIZES,
TEXT_FIELD_BASE_TYPES,
} from './text-field-base';
export { TextFieldSearch } from './text-field-search';

View File

@ -17,8 +17,8 @@ exports[`PickerNetwork should render the label inside the PickerNetwork 1`] = `
Imported
</p>
<div
class="box mm-picker-network__arrow-down-icon icon icon--size-xs box--flex-direction-row box--color-icon-default"
style="mask-image: url('./images/icons/icon-arrow-down.svg;"
class="box mm-picker-network__arrow-down-icon mm-icon mm-icon--size-xs box--flex-direction-row box--color-icon-default"
style="mask-image: url('./images/icons/icon-arrow-down.svg');"
/>
</button>
</div>

View File

@ -11,8 +11,8 @@ exports[`TagUrl should render the label inside the TagUrl 1`] = `
>
<div
aria-label="avatar-favicon"
class="box icon icon--size-md box--flex-direction-row box--color-icon-default"
style="mask-image: url('./images/icons/icon-global-filled.svg;"
class="box mm-icon mm-icon--size-md box--flex-direction-row box--color-icon-default"
style="mask-image: url('./images/icons/icon-global-filled.svg');"
/>
</div>
<p

View File

@ -8,6 +8,7 @@ import {
ALIGN_ITEMS,
TEXT,
COLORS,
BORDER_RADIUS,
} from '../../../helpers/constants/design-system';
import Box from '../../ui/box';
@ -107,7 +108,7 @@ export const TextFieldBase = ({
backgroundColor={COLORS.BACKGROUND_DEFAULT}
alignItems={ALIGN_ITEMS.CENTER}
borderWidth={1}
borderRadius={SIZES.SM}
borderRadius={BORDER_RADIUS.SM}
paddingLeft={leftAccessory ? 4 : 0}
paddingRight={rightAccessory ? 4 : 0}
onClick={handleClick}

View File

@ -40,6 +40,7 @@
&__input {
border: none;
height: 100%;
width: 100%;
flex-grow: 1;
box-sizing: content-box;
margin: 0;