Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Avatar component #1134

Merged
merged 31 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
eb33180
WIP: scaffold avatar component
RubenSibon Mar 14, 2024
56c5caf
feat(component): :sparkles: new avatar component
RubenSibon Mar 14, 2024
4116616
Merge branch 'develop' into feat/DES-653-avatar
RubenSibon Mar 14, 2024
44d7101
feat(component): :sparkles: picture support & default user icon
RubenSibon Mar 15, 2024
0633875
Merge remote-tracking branch 'origin/develop' into feat/DES-653-avatar
RubenSibon Mar 15, 2024
daf5ebd
fix(component): :ambulance: replace bg images & address feedback
RubenSibon Mar 15, 2024
d280398
Merge remote-tracking branch 'origin/develop' into feat/DES-653-avatar
RubenSibon Mar 15, 2024
c6d57b2
feat(story): :art: add example story with header
RubenSibon Mar 15, 2024
5e1ebbc
refactor(ts): :label: remove already implied types
RubenSibon Mar 15, 2024
5ea676f
fix(component): :lipstick: image position and clipping
RubenSibon Mar 15, 2024
49adfde
Use type for span element
VincentSmedinga Mar 18, 2024
a0b55d6
Tidy up Header story
VincentSmedinga Mar 18, 2024
f6b7f28
Use design token for aspect ratio
VincentSmedinga Mar 18, 2024
7131b44
Simplify slicing short labels
VincentSmedinga Mar 18, 2024
1eca268
Decrease padding to match space tokens scale
VincentSmedinga Mar 18, 2024
f833133
Display the fallback icon correctly
VincentSmedinga Mar 18, 2024
ff50d82
Split padding tokens
VincentSmedinga Mar 18, 2024
a98b87e
Simplify image sizing
VincentSmedinga Mar 18, 2024
d9b7844
Merge branch 'develop' into feat/DES-653-avatar
VincentSmedinga Mar 19, 2024
9d2af93
Remove useMemo, extract content rendering logic
alimpens Mar 19, 2024
187ce70
Add alt, remove redundant initials
alimpens Mar 19, 2024
75de96b
Hide visible text from screenreaders
alimpens Mar 19, 2024
98d7528
Fix invalid HTML
alimpens Mar 19, 2024
d25177e
Fix test
VincentSmedinga Mar 19, 2024
ce52b1a
Remove property setting the initial value
VincentSmedinga Mar 19, 2024
5dd40fd
Group components with their prop types
VincentSmedinga Mar 19, 2024
197f4e0
Use getByText in tests where possible
alimpens Mar 20, 2024
7f79270
Don't reuse aspect ratio component token
alimpens Mar 20, 2024
b8fd59e
Use rem instead of px
alimpens Mar 20, 2024
9f87473
Remove phantom white space when image doesn't load
alimpens Mar 20, 2024
c1f683d
Add comment
alimpens Mar 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/css/src/components/avatar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- @license CC0-1.0 -->

# Avatar

A prominently coloured circular button containing two initials of a currently logged-in user.

Clicking or tapping the button would typically open a context menu or view with information and actions related to the user.

## Design

The avatar contains two initial letters from the user's full name.
The default background colour is dark blue.
Suggestions on when to use the other colours will follow soon.
70 changes: 70 additions & 0 deletions packages/css/src/components/avatar/avatar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

.ams-avatar {
border-radius: 50%;
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved
border-style: var(--ams-avatar-border-style);
border-width: var(--ams-avatar-border-width);
display: inline-block;
font-family: var(--ams-avatar-font-family);
font-size: var(--ams-avatar-font-size);
line-height: var(--ams-avatar-line-height);
min-width: 1lh;
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved
text-align: center;
}

.ams-avatar--blue {
background-color: var(--ams-avatar-blue-background-color);
border-color: var(--ams-avatar-blue-background-color);
color: var(--ams-avatar-blue-color);
}

.ams-avatar--dark-blue {
background-color: var(--ams-avatar-dark-blue-background-color);
border-color: var(--ams-avatar-dark-blue-background-color);
color: var(--ams-avatar-dark-blue-color);
}

.ams-avatar--dark-green {
background-color: var(--ams-avatar-dark-green-background-color);
border-color: var(--ams-avatar-dark-green-background-color);
color: var(--ams-avatar-dark-green-color);
}

.ams-avatar--green {
background-color: var(--ams-avatar-green-background-color);
border-color: var(--ams-avatar-green-background-color);
color: var(--ams-avatar-green-color);
}

.ams-avatar--magenta {
background-color: var(--ams-avatar-magenta-background-color);
border-color: var(--ams-avatar-magenta-background-color);
color: var(--ams-avatar-magenta-color);
}

.ams-avatar--orange {
background-color: var(--ams-avatar-orange-background-color);
border-color: var(--ams-avatar-orange-background-color);
color: var(--ams-avatar-orange-color);
}

.ams-avatar--purple {
background-color: var(--ams-avatar-purple-background-color);
border-color: var(--ams-avatar-purple-background-color);
color: var(--ams-avatar-purple-color);
}

.ams-avatar--red {
background-color: var(--ams-avatar-red-background-color);
border-color: var(--ams-avatar-red-background-color);
color: var(--ams-avatar-red-color);
}

.ams-avatar--yellow {
background-color: var(--ams-avatar-yellow-background-color);
border-color: var(--ams-avatar-yellow-background-color);
color: var(--ams-avatar-yellow-color);
}
1 change: 1 addition & 0 deletions packages/css/src/components/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

/* Append here */
@import "./avatar/avatar";
@import "./row/row";
@import "./radio/radio";
@import "./tabs/tabs";
Expand Down
67 changes: 67 additions & 0 deletions packages/react/src/Avatar/Avatar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { render } from '@testing-library/react'
import { createRef } from 'react'
import { Avatar, avatarColors } from './Avatar'
import '@testing-library/jest-dom'

describe('Avatar', () => {
it('renders', () => {
const { container } = render(<Avatar label="NR" />)

const component = container.querySelector(':only-child')

expect(component).toBeInTheDocument()
expect(component).toBeVisible()
})

it('renders a design system BEM class name', () => {
const { container } = render(<Avatar label="RS" />)

const component = container.querySelector(':only-child')

expect(component).toHaveClass('ams-avatar')
})

it('renders an additional class name', () => {
const { container } = render(<Avatar label="VS" className="extra" />)

const component = container.querySelector(':only-child')

expect(component).toHaveClass('ams-avatar extra')
})

it('supports ForwardRef in React', () => {
const ref = createRef<HTMLElement>()

const { container } = render(<Avatar label="AL" ref={ref} />)

const component = container.querySelector(':only-child')

expect(ref.current).toBe(component)
})

it('renders a label consisting of two letters', () => {
const { container } = render(<Avatar label="RS" />)

const component = container.querySelector(':only-child')

expect(component?.textContent).toHaveLength(2)
})

it('renders with default color', () => {
const { container } = render(<Avatar label="VS" />)

const component = container.querySelector(':only-child')

expect(component).toHaveClass('ams-avatar--dark-blue')
})

avatarColors.map((color) =>
it(`renders with ${color} color`, () => {
const { container } = render(<Avatar label="AL" color={color} />)

const component = container.querySelector(':only-child')

expect(component).toHaveClass(`ams-avatar--${color}`)
}),
)
})
61 changes: 61 additions & 0 deletions packages/react/src/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import clsx from 'clsx'
import { forwardRef, useMemo } from 'react'
import type { ForwardedRef, HTMLAttributes } from 'react'

export const avatarColors = [
'blue',
'dark-blue',
'dark-green',
'green',
'magenta',
'orange',
'purple',
'red',
'yellow',
] as const

type AvatarColor = (typeof avatarColors)[number]

export type AvatarProps = {
color?: AvatarColor
label: string
} & HTMLAttributes<HTMLElement>
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved

export const Avatar = forwardRef(
({ label, className, color = 'dark-blue', ...restProps }: AvatarProps, ref: ForwardedRef<HTMLElement>) => {
if (label.length !== 2) {
// TODO: should we log this somewhere or throw an error to the consumer?
console.warn(`Avatar label should be no more and no less than two characters. Got: "${label}".`)
}

const initials = useMemo(() => {
if (label.length === 0) {
return 'n.b.'
} else if (label.length > 2) {
return label.slice(0, 2).toUpperCase()
} else {
return label.toUpperCase()
}
}, [label])

const initialsDotted = useMemo(() => `${initials.split('').join('.')}.`, [initials])

return (
<span
{...restProps}
ref={ref}
className={clsx('ams-avatar', `ams-avatar--${color}`, className)}
aria-label={`Initialen gebruiker: ${initialsDotted}`}
>
{initials}
</span>
)
},
)

Avatar.displayName = 'Avatar'
5 changes: 5 additions & 0 deletions packages/react/src/Avatar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- @license CC0-1.0 -->

# React Avatar component

[Avatar documentation](../../../css/src/components/avatar/README.md)
2 changes: 2 additions & 0 deletions packages/react/src/Avatar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Avatar } from './Avatar'
export type { AvatarProps } from './Avatar'
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

/* Append here */
export * from './Avatar'
export * from './Row'
export * from './Radio'
export * from './Tabs'
Expand Down
48 changes: 48 additions & 0 deletions proprietary/tokens/src/components/ams/avatar.tokens.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"ams": {
"avatar": {
"border-width": { "value": "{ams.border-width.md}" },
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved
"border-style": { "value": "solid" },
"font-family": { "value": "{ams.text.font-family}" },
"font-size": { "value": "{ams.text.level.6.font-size}" },
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved
"font-weight": { "value": "{ams.text.font-weight.normal}" },
"line-height": { "value": "{ams.text.level.6.line-height}" },
"blue": {
"background-color": { "value": "{ams.color.blue}" },
"color": { "value": "{ams.color.primary-black}" }
},
"dark-blue": {
"background-color": { "value": "{ams.color.primary-blue}" },
"color": { "value": "{ams.color.primary-white}" }
},
"dark-green": {
"background-color": { "value": "{ams.color.dark-green}" },
"color": { "value": "{ams.color.primary-white}" }
},
"green": {
"background-color": { "value": "{ams.color.green}" },
"color": { "value": "{ams.color.primary-black}" }
},
"magenta": {
"background-color": { "value": "{ams.color.magenta}" },
"color": { "value": "{ams.color.primary-white}" }
},
"orange": {
"background-color": { "value": "{ams.color.orange}" },
"color": { "value": "{ams.color.primary-black}" }
},
"purple": {
"background-color": { "value": "{ams.color.purple}" },
"color": { "value": "{ams.color.primary-white}" }
},
"red": {
"background-color": { "value": "{ams.color.primary-red}" },
"color": { "value": "{ams.color.primary-white}" }
},
"yellow": {
"background-color": { "value": "{ams.color.yellow}" },
"color": { "value": "{ams.color.primary-black}" }
}
}
}
}
15 changes: 15 additions & 0 deletions storybook/src/components/Avatar/Avatar.docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Controls, Markdown, Meta, Primary } from "@storybook/blocks";
import * as AvatarStories from "./Avatar.stories.tsx";
import README from "../../../../packages/css/src/components/avatar/README.md?raw";

<Meta of={AvatarStories} />

<Markdown>{README}</Markdown>

## Stories

### Default

<Primary />

<Controls />
30 changes: 30 additions & 0 deletions storybook/src/components/Avatar/Avatar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import { Avatar } from '@amsterdam/design-system-react'
import { Meta, StoryObj } from '@storybook/react'

const meta = {
title: 'Components/Feedback/Avatar',
component: Avatar,
args: {
label: 'DS',
},
argTypes: {
color: {
control: {
type: 'select',
},
options: ['blue', 'dark-blue', 'dark-green', 'green', 'magenta', 'orange', 'purple', 'red', 'yellow'],
selected: 'dark-blue',
},
},
} satisfies Meta<typeof Avatar>

export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {}
Loading