Skip to content

Commit b4063d2

Browse files
[DSR] Added Blockies (#465)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This PR adds the `Blockies` component to DSR <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Related issues** Fixes: #404 ## **Manual testing steps** 1. Run `yarn storybook` from root 2. Go to React Components > Blockies 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** https://github.com/user-attachments/assets/4fedd8e0-eaa1-4f9c-9ffb-4087ba51010c <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: georgewrmarshall <[email protected]> Co-authored-by: George Marshall <[email protected]>
1 parent d14c529 commit b4063d2

File tree

10 files changed

+285
-3
lines changed

10 files changed

+285
-3
lines changed

packages/design-system-react-native/src/primitives/Blockies/Blockies.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import { toDataUrl } from './Blockies.utilities';
77

88
import type { BlockiesProps } from './Blockies.types';
99

10-
const Blockies = ({ address, size = 32, ...imageProps }: BlockiesProps) => {
10+
const Blockies = ({ address, size = 32, ...props }: BlockiesProps) => {
1111
return (
1212
<Image
1313
source={{ uri: toDataUrl(address) }}
1414
width={size}
1515
height={size}
16-
{...imageProps}
16+
{...props}
1717
/>
1818
);
1919
};

packages/design-system-react/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
},
5252
"dependencies": {
5353
"@radix-ui/react-slot": "^1.1.0",
54+
"blo": "^1.2.0",
5455
"tailwind-merge": "^2.0.0"
5556
},
5657
"devDependencies": {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import React from 'react';
3+
4+
import { Blockies } from './Blockies';
5+
import type { BlockiesProps } from './Blockies.types';
6+
import README from './README.mdx';
7+
8+
const meta: Meta<BlockiesProps> = {
9+
title: 'React Components/Blockies',
10+
component: Blockies,
11+
parameters: {
12+
docs: {
13+
page: README,
14+
},
15+
},
16+
args: {
17+
address: '0x9Cbf7c41B7787F6c621115010D3B044029FE2Ce8',
18+
size: 32,
19+
},
20+
argTypes: {
21+
address: {
22+
control: 'text',
23+
description:
24+
'Required address used as a unique identifier to generate the Blockies.',
25+
},
26+
size: {
27+
control: 'number',
28+
description: 'Optional prop to control the size of the Blockies.',
29+
},
30+
className: {
31+
control: 'text',
32+
description:
33+
'Optional prop for additional CSS classes to be applied to the Blockies component.',
34+
},
35+
},
36+
};
37+
38+
export default meta;
39+
type Story = StoryObj<BlockiesProps>;
40+
41+
const sampleAccountAddresses = [
42+
'0x9Cbf7c41B7787F6c621115010D3B044029FE2Ce8',
43+
'0xb9b81f6bd23B953c5257C3b5E2F0c03B07E944eB',
44+
'0x360507dfEC4Bf0c03495f91154A78C672599F308',
45+
'0x50cA820Ff810F7687E7d0aDb23A830e3ac6032C3',
46+
'0x840C9Eb73729E626673714D6E4dA8afc8Ccc90d3',
47+
'0xCA0361BE89B7d47a6233d1875F0727ddeAB23377',
48+
'0xD78CBcA88eCd65c6128511e46a518CDc6c66fC74',
49+
'0xCFc8b1d1031ef2ecce3a98d5D79ce4D75Edb06bA',
50+
'0xDe53fa2E659b6134991bB56F64B691990e5C44E7',
51+
'0x8AceA3A9748294d1B5C25a08EFE37b756AafDFdd',
52+
'0xEC5CE72f2e18B0017C88F7B12d3308119C5Cf129',
53+
'0xeC56Da21c90Af6b50E4Ba5ec252bD97e735290fc',
54+
];
55+
56+
export const Default: Story = {};
57+
58+
export const Address: Story = {
59+
render: () => (
60+
<div className="flex flex-wrap gap-2">
61+
{sampleAccountAddresses.map((address) => (
62+
<Blockies key={address} address={address} size={32} />
63+
))}
64+
</div>
65+
),
66+
};
67+
68+
export const Size: Story = {
69+
render: () => (
70+
<div className="flex items-center gap-4">
71+
<Blockies address={sampleAccountAddresses[0]} size={16} />
72+
<Blockies address={sampleAccountAddresses[1]} size={24} />
73+
<Blockies address={sampleAccountAddresses[2]} size={32} />
74+
<Blockies address={sampleAccountAddresses[3]} size={48} />
75+
<Blockies address={sampleAccountAddresses[4]} size={64} />
76+
</div>
77+
),
78+
};
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from 'react';
2+
import { render, screen, waitFor } from '@testing-library/react';
3+
import { Blockies } from './Blockies';
4+
5+
// Mock the 'blo' module to return a predictable output for testing.
6+
jest.mock('blo', () => ({
7+
blo: (address: string) => `generated_${address}`,
8+
}));
9+
10+
describe('Blockies', () => {
11+
const address = '0x1234567890abcdef';
12+
13+
it('renders an img element with the correct src, height, and width', async () => {
14+
render(
15+
<Blockies
16+
address={address}
17+
size={48}
18+
data-testid="blockies"
19+
alt="Blockies Avatar"
20+
/>,
21+
);
22+
23+
// Wait for the image to be rendered after the dynamic import resolves.
24+
const img = await waitFor(() => screen.getByTestId('blockies'));
25+
26+
expect(img.tagName).toBe('IMG');
27+
expect(img).toHaveAttribute('src', `generated_${address}`);
28+
expect(img).toHaveAttribute('height', '48');
29+
expect(img).toHaveAttribute('width', '48');
30+
expect(img).toHaveAttribute('alt', 'Blockies Avatar');
31+
});
32+
33+
it('applies the default size of 32 when no size prop is provided', async () => {
34+
render(<Blockies address={address} data-testid="blockies" />);
35+
36+
const img = await waitFor(() => screen.getByTestId('blockies'));
37+
38+
expect(img).toHaveAttribute('height', '32');
39+
expect(img).toHaveAttribute('width', '32');
40+
});
41+
42+
it('spreads additional image props to the img element', async () => {
43+
render(
44+
<Blockies
45+
address={address}
46+
data-testid="blockies"
47+
title="Blockies Title"
48+
/>,
49+
);
50+
51+
const img = await waitFor(() => screen.getByTestId('blockies'));
52+
53+
expect(img).toHaveAttribute('title', 'Blockies Title');
54+
});
55+
56+
it('applies default alt text when no alt prop is provided', async () => {
57+
render(<Blockies address={address} data-testid="blockies" />);
58+
59+
const img = await waitFor(() => screen.getByTestId('blockies'));
60+
61+
expect(img).toHaveAttribute('alt', `Blockies for ${address}`);
62+
});
63+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React, { useState, useEffect } from 'react';
2+
import type { BlockiesProps } from './Blockies.types';
3+
4+
export const Blockies = ({ address, size = 32, ...props }: BlockiesProps) => {
5+
const [bloModule, setBloModule] = useState<{
6+
blo: (address: string) => string;
7+
} | null>(null);
8+
9+
useEffect(() => {
10+
import('blo').then((module) =>
11+
setBloModule(module as { blo: (address: string) => string }),
12+
);
13+
}, []);
14+
15+
if (!bloModule) {
16+
return null;
17+
}
18+
19+
return (
20+
<img
21+
src={bloModule.blo(address)}
22+
height={size}
23+
width={size}
24+
alt={`Blockies for ${address}`} // TODO: Add localization for this
25+
{...props}
26+
/>
27+
);
28+
};
29+
30+
Blockies.displayName = 'Blockies';
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { ComponentProps } from 'react';
2+
3+
export type BlockiesProps = ComponentProps<'img'> & {
4+
/**
5+
* Required address used as a unique identifier to generate the Blockies.
6+
*/
7+
address: string;
8+
/**
9+
* Optional prop to control the size of the Blockies.
10+
*/
11+
size?: number;
12+
};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { Controls, Canvas } from '@storybook/blocks';
2+
import * as BlockiesStories from './Blockies.stories';
3+
4+
# Blockies
5+
6+
The **Blockies** component generates a unique, blocky avatar image based on a provided address.
7+
8+
```tsx
9+
import { Blockies } from '@metamask/design-system-react';
10+
11+
<Blockies address="0x1234567890abcdef1234567890abcdef12345678" />;
12+
```
13+
14+
<Canvas of={BlockiesStories.Default} />
15+
16+
## Props
17+
18+
### Address
19+
20+
The `address` prop is required and used as a unique identifier to generate the Blockies image. Each address will generate a unique, deterministic pattern.
21+
22+
<Canvas of={BlockiesStories.Address} />
23+
24+
### Size
25+
26+
The `size` prop controls the dimensions (both height and width) of the Blockies image in pixels. The default size is 32px.
27+
28+
<Canvas of={BlockiesStories.Size} />
29+
30+
### Accessibility
31+
32+
The `alt` attribute provides alternative text for screen readers. By default, it includes the address for context (`Blockies for {address}`). You can override this with custom alt text:
33+
34+
#### Default alt text
35+
36+
```tsx
37+
// Default alt text
38+
<Blockies address="0x1234567890abcdef" />
39+
```
40+
41+
```html
42+
<!-- Renders: -->
43+
<img alt="Blockies for 0x1234567890abcdef" />
44+
```
45+
46+
#### Custom alt text
47+
48+
```tsx
49+
// Custom alt text
50+
<Blockies address="0x1234567890abcdef" alt="User avatar for John Doe" />
51+
```
52+
53+
```html
54+
<!-- Renders: -->
55+
<img alt="User avatar for John Doe" />
56+
```
57+
58+
### Class Name
59+
60+
Use the `className` prop to add custom CSS classes to the component. These classes will be merged with the component's default classes using `twMerge`, allowing you to:
61+
62+
- Add new styles that don't exist in the default component
63+
- Override the component's default styles when needed (Blockies has no default styles)
64+
65+
Example:
66+
67+
```tsx
68+
// Adding new styles
69+
<Blockies
70+
address="0x1234567890abcdef1234567890abcdef12345678"
71+
className="my-4 mx-2"
72+
/>
73+
```
74+
75+
Note: When using `className` to override default styles, the custom classes will take precedence over the component's default classes.
76+
77+
### Style
78+
79+
The `style` prop should primarily be used for dynamic inline styles that cannot be achieved with className alone. For static styles, prefer using className with Tailwind classes.
80+
81+
## Component API
82+
83+
<Controls of={BlockiesStories.Default} />
84+
85+
## References
86+
87+
[MetaMask Design System Guides](https://www.notion.so/MetaMask-Design-System-Guides-Design-f86ecc914d6b4eb6873a122b83c12940)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { Blockies } from './Blockies';
2+
export type { BlockiesProps } from './Blockies.types';

packages/design-system-react/tsconfig.build.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"rootDir": "./src",
77
"jsx": "react",
88
"types": ["react"],
9-
"lib": ["ES2020", "DOM"]
9+
"lib": ["ES2020", "DOM"],
10+
"skipLibCheck": true // TODO: Remove this once we have a proper type definition for the blo module for blockies
1011
},
1112
"references": [],
1213
"include": ["./src"]

yarn.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3236,6 +3236,7 @@ __metadata:
32363236
"@types/node": "npm:^16.18.54"
32373237
"@types/react": "npm:^18.2.0"
32383238
"@types/react-dom": "npm:^18.2.0"
3239+
blo: "npm:^1.2.0"
32393240
deepmerge: "npm:^4.2.2"
32403241
jest: "npm:^29.7.0"
32413242
jest-environment-jsdom: "npm:^29.7.0"
@@ -7439,6 +7440,13 @@ __metadata:
74397440
languageName: node
74407441
linkType: hard
74417442

7443+
"blo@npm:^1.2.0":
7444+
version: 1.2.0
7445+
resolution: "blo@npm:1.2.0"
7446+
checksum: 10/17ec61e41b201bbba8ab3c874cad94696604b6ac4029de5050c92ea5817dff6814d8642b9b2948e0d6804194437b751fa2755652201f26fa5d606913c489b757
7447+
languageName: node
7448+
linkType: hard
7449+
74427450
"bluebird@npm:^3.5.5":
74437451
version: 3.7.2
74447452
resolution: "bluebird@npm:3.7.2"

0 commit comments

Comments
 (0)