Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

### Internal

- Add `Avatar` and `AvatarGroup` components as private APIs ([#75595](https://github.com/WordPress/gutenberg/pull/75595)).
- Remove `Picker` from private APIs ([#75394](https://github.com/WordPress/gutenberg/pull/75394)).
- Expose `useDrag` from `@use-gesture/react` package via private API's ([#66735](https://github.com/WordPress/gutenberg/pull/66735)).
- `Disabled`, `Modal`, `Popover`, `Tooltip`: Move context code to separate files to help docgen prop extraction ([#75316](https://github.com/WordPress/gutenberg/pull/75316)).
Expand Down
46 changes: 46 additions & 0 deletions packages/components/src/avatar-group/component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* External dependencies
*/
import clsx from 'clsx';

/**
* WordPress dependencies
*/
import { Children } from '@wordpress/element';

/**
* Internal dependencies
*/
import type { AvatarGroupProps } from './types';
import type { WordPressComponentProps } from '../context';

function AvatarGroup( {
className,
max = 3,
children,
...props
}: WordPressComponentProps< AvatarGroupProps, 'div', false > ) {
const childArray = Children.toArray( children );
const visible = childArray.slice( 0, max );
const overflowCount = childArray.length - max;

return (
<div
role="group"
className={ clsx( 'components-avatar-group', className ) }
{ ...props }
>
{ visible }
{ overflowCount > 0 && (
<span
className="components-avatar-group__overflow"
aria-label={ `${ overflowCount } more` }
>
{ `+${ overflowCount }` }
</span>
) }
</div>
);
}

export default AvatarGroup;
2 changes: 2 additions & 0 deletions packages/components/src/avatar-group/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './component';
export type { AvatarGroupProps } from './types';
32 changes: 32 additions & 0 deletions packages/components/src/avatar-group/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@use "@wordpress/base-styles/colors" as *;
@use "@wordpress/base-styles/variables" as *;

.components-avatar-group {
display: flex;
align-items: center;

// Overlap subsequent avatars.
> .components-avatar + .components-avatar {
margin-inline-start: -$grid-unit-10;
}

// Stack earlier avatars on top of later ones so the overlap
// is visually correct, and elevate on hover for badge expansion.
> .components-avatar {
position: relative;

@for $i from 1 through 10 {
&:nth-child(#{$i}) {
z-index: #{11 - $i};
}
}
}
}

.components-avatar-group__overflow {
margin-inline-start: $grid-unit-05;
font-size: $font-size-small;
line-height: $font-line-height-small;
color: $gray-900;
white-space: nowrap;
}
13 changes: 13 additions & 0 deletions packages/components/src/avatar-group/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type AvatarGroupProps = {
/**
* Maximum number of avatars to display before showing an
* overflow indicator.
*
* @default 3
*/
max?: number;
/**
* Avatar elements to display in the group.
*/
children: React.ReactNode;
};
82 changes: 82 additions & 0 deletions packages/components/src/avatar/component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* External dependencies
*/
import clsx from 'clsx';

/**
* Internal dependencies
*/
import Icon from '../icon';
import Tooltip from '../tooltip';
import type { AvatarProps } from './types';
import type { WordPressComponentProps } from '../context';

function Avatar( {
className,
src,
name,
label,
badge = false,
size = 'default',
borderColor,
status,
statusIndicator,
style,
...props
}: WordPressComponentProps< AvatarProps, 'div', false > ) {
const showBadge = badge && !! name;
const initials = name
? name
.split( /\s+/ )
.slice( 0, 2 )
.map( ( word ) => word[ 0 ] )
.join( '' )
.toUpperCase()
: undefined;
const customProperties = {
...style,
...( src ? { '--components-avatar-url': `url(${ src })` } : {} ),
...( borderColor
? { '--components-avatar-outline-color': borderColor }
: {} ),
} as React.CSSProperties;

const avatar = (
<div
className={ clsx( 'components-avatar', className, {
'has-avatar-border-color': !! borderColor,
'has-src': !! src,
'has-badge': showBadge,
'is-small': size === 'small',
'has-status': !! status,
[ `is-${ status }` ]: !! status,
} ) }
style={ customProperties }
role="img"
aria-label={ name }
{ ...props }
>
<span className="components-avatar__image">
{ ! src && initials }
{ !! status && !! statusIndicator && (
<span className="components-avatar__status-indicator">
<Icon icon={ statusIndicator } />
</span>
) }
</span>
{ showBadge && (
<span className="components-avatar__name">
{ label || name }
</span>
) }
</div>
);

if ( name && ( ! showBadge || label ) ) {
return <Tooltip text={ name }>{ avatar }</Tooltip>;
}

return avatar;
}

export default Avatar;
2 changes: 2 additions & 0 deletions packages/components/src/avatar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from './component';
export type { AvatarProps } from './types';
152 changes: 152 additions & 0 deletions packages/components/src/avatar/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
@use "@wordpress/base-styles/colors" as *;
@use "@wordpress/base-styles/variables" as *;
@use "../utils/theme-variables" as *;

.components-avatar {
display: inline-flex;
align-items: center;
border-radius: $radius-full;
overflow: clip;
flex-shrink: 0;
background-color: $components-color-accent;
box-shadow: 0 0 0 var(--wp-admin-border-width-focus) $white, $elevation-x-small;
}

.components-avatar__image {
box-sizing: border-box;
position: relative;
width: $button-size-compact;
height: $button-size-compact;
border-radius: $radius-full;
border: 0;
background-color: $components-color-accent;
overflow: clip;
flex-shrink: 0;
font-size: 0;
color: $white;

.is-small > & {
width: $button-size-small;
height: $button-size-small;
}

.has-src > & {
background-image: var(--components-avatar-url);
background-size: cover;
background-position: center;
}

.has-avatar-border-color > & {
border: var(--wp-admin-border-width-focus) solid var(--components-avatar-outline-color);
box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) $white;
background-clip: padding-box;
}
}

// Initials fallback: show name characters when no image.
.components-avatar:not(.has-src) > .components-avatar__image {
display: flex;
align-items: center;
justify-content: center;
font-size: $font-size-x-small;
font-weight: $font-weight-medium;
border: 0;
box-shadow: none;
background-clip: border-box;
}

.components-avatar:not(.has-src).has-avatar-border-color > .components-avatar__image {
background-color: var(--components-avatar-outline-color);
}

.components-avatar__name {
font-size: $font-size-medium;
line-height: $font-line-height-small;
color: $white;
min-width: 0;
padding-bottom: calc($grid-unit-05 / 2);
overflow: hidden;
opacity: 0;
white-space: nowrap;
transition: opacity 0.15s cubic-bezier(0.15, 0, 0.15, 1);
}

// Badge mode: use grid so the name column animates from 0 to natural width.
// Spacing is on the container (column-gap + padding-inline-end) so it
// transitions alongside grid-template-columns instead of causing a bump.
.components-avatar.has-badge {
display: inline-grid;
grid-template-columns: min-content 0fr;
column-gap: 0;
padding-inline-end: 0;
transition:
grid-template-columns 0.3s cubic-bezier(0.15, 0, 0.15, 1),
column-gap 0.3s cubic-bezier(0.15, 0, 0.15, 1),
padding-inline-end 0.3s cubic-bezier(0.15, 0, 0.15, 1);

&:hover {
grid-template-columns: min-content 1fr;
column-gap: $grid-unit-05;
padding-inline-end: $grid-unit-10;
transition-timing-function: cubic-bezier(0.85, 0, 0.85, 1);
}

&:hover .components-avatar__name {
opacity: 1;
transition-timing-function: cubic-bezier(0.85, 0, 0.85, 1);
}
}

.components-avatar.has-badge.has-avatar-border-color {
background-color: var(--components-avatar-outline-color);
}

// Status: dim and desaturate content so the background color shows through.
// Images: move to ::before with luminosity blend over the solid bg color.
// Initials: reduce text opacity.
.components-avatar.has-status.has-src > .components-avatar__image {
background-image: none;

&::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background-image: var(--components-avatar-url);
background-size: cover;
background-position: center;
mix-blend-mode: luminosity;
opacity: 0.3;
}
}

.components-avatar.has-status:not(.has-src) > .components-avatar__image {
color: rgba($white, 0.3);
}

.components-avatar.has-status.has-avatar-border-color > .components-avatar__image {
background-color: var(--components-avatar-outline-color);
}

.components-avatar__status-indicator {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
color: $white;
fill: $white;

svg {
width: 75%;
height: 75%;
}
}

@media (prefers-reduced-motion: reduce) {
.components-avatar.has-badge,
.components-avatar__name {
transition: none;
}
}
Loading
Loading