Skip to content

Commit

Permalink
feat(avatar): support adding a badge to the avatar (#267)
Browse files Browse the repository at this point in the history
  • Loading branch information
HamudeHomsi authored and dgonzalezr committed Jul 3, 2023
1 parent 5ae0451 commit 99c8c42
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 41 deletions.
4 changes: 2 additions & 2 deletions packages/bee-q/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export namespace Components {
*/
"backgroundColor"?: string;
/**
* The size of the badge
* The size of the badge. Relevant if badge has no content.
*/
"size"?: TBadgeSize;
/**
Expand Down Expand Up @@ -912,7 +912,7 @@ declare namespace LocalJSX {
*/
"backgroundColor"?: string;
/**
* The size of the badge
* The size of the badge. Relevant if badge has no content.
*/
"size"?: TBadgeSize;
/**
Expand Down
20 changes: 20 additions & 0 deletions packages/bee-q/src/components/avatar/__tests__/bq-avatar.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,24 @@ describe('bq-avatar', () => {
expect(mediumSquareStyle).toEqual({ borderRadius: '12px', height: '48px', width: '48px' });
expect(largeSquareStyle).toEqual({ borderRadius: '12px', height: '64px', width: '64px' });
});

it('should render <bq-badge> component', async () => {
const page = await newE2EPage();
await page.setContent(`
<bq-avatar
alt-text="User profile"
label="Label"
initials="JS"
shape="circle"
size="medium"
>
<bq-badge slot="badge" text-color="#fff">9</bq-badge>
</bq-avatar>
`);

const avatarElem = await page.find('bq-avatar');
const sideMenuItems = await avatarElem.findAll('bq-badge');

expect(sideMenuItems).toHaveLength(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const meta: Meta = {
label: 'Avatar component label',
shape: 'circle',
size: 'medium',
'badge-content': '9',
},
};
export default meta;
Expand Down Expand Up @@ -55,3 +56,21 @@ export const Initials: Story = {
initials: 'JS',
},
};

export const WithBadge: Story = {
render: (args: Args) =>
html`<bq-avatar
alt-text=${args['alt-text']}
image=${args.image}
label=${args.label}
initials=${args.initials}
shape=${args.shape}
size=${args.size}
>
<bq-badge slot="badge" text-color="#fff">${args['badge-content']}</bq-badge>
</bq-avatar>`,
args: {
image:
'https://images.unsplash.com/photo-1524593689594-aae2f26b75ab?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1000&q=80',
},
};
109 changes: 77 additions & 32 deletions packages/bee-q/src/components/avatar/bq-avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { h, Component, Prop, Watch, State, Element } from '@stencil/core';
import { h, Component, Prop, Watch, State, Element, Host } from '@stencil/core';

import { TAvatarShape, TAvatarSize, AVATAR_SHAPE, AVATAR_SIZE } from './bq-avatar.types';
import { validatePropValue } from '../../shared/utils';
Expand All @@ -19,6 +19,8 @@ export class BqAvatar {
// Own Properties
// ====================

trimmedInitials: string;

// Reference to host HTML element
// ===================================

Expand Down Expand Up @@ -67,6 +69,12 @@ export class BqAvatar {
validatePropValue(AVATAR_SIZE, 'medium', this.el, 'size');
}

@Watch('initials')
@Watch('size')
onInitialsChnage() {
this.trimInitialsBasedOnSize();
}

// Events section
// Requires JSDocs for public API documentation
// ==============================================
Expand All @@ -76,6 +84,7 @@ export class BqAvatar {
// =====================================

componentWillLoad() {
this.trimInitialsBasedOnSize();
this.checkPropValues();
}

Expand All @@ -98,43 +107,79 @@ export class BqAvatar {
this.hasError = true;
};

private trimInitialsBasedOnSize = (): void => {
AVATAR_SIZE.forEach((size: TAvatarSize) => {
if (this.size === size) {
this.trimmedInitials = this.initials.substring(0, this.getIndex(size));
}
});
};

private getIndex = (size: TAvatarSize): number => {
switch (size) {
case 'small':
return 2;
case 'medium':
return 3;
case 'large':
return 4;
default:
// also if size === xsmall
return 1;
}
};

// render() function
// Always the last one in the class.
// ===================================

render() {
return (
<div
class={{
'relative overflow-hidden bg-ui-secondary-light': true,
[`size--${this.size}`]: true,
'rounded-full': this.shape === 'circle',
'rounded-xs': this.shape === 'square' && this.size === 'xsmall',
'rounded-s': this.shape === 'square' && this.size === 'small',
'rounded-m': this.shape === 'square' && (this.size === 'medium' || this.size === 'large'),
}}
aria-label={this.label}
role="img"
part="base"
>
{this.initials && (
<span
class="absolute left-0 top-0 inline-flex h-full w-full items-center justify-center font-bold"
part="text"
>
{this.initials}
</span>
)}
{this.image && !this.hasError && (
<img
class="absolute left-0 top-0 h-full w-full object-cover"
alt={this.altText ?? undefined}
src={this.image}
onError={this.onImageError}
part="img"
/>
)}
</div>
<Host>
<div
class={{
'bq-avatar': true,
[`size--${this.size}`]: true,
'rounded-[var(--bq-avatar--border-radius-circle)]': this.shape === 'circle',
'rounded-[var(--bq-avatar--border-radius-squareXs)]': this.shape === 'square' && this.size === 'xsmall',
'rounded-[var(--bq-avatar--border-radius-squareS)]': this.shape === 'square' && this.size === 'small',
'rounded-[var(--bq-avatar--border-radius-squareM)]':
this.shape === 'square' && (this.size === 'medium' || this.size === 'large'),
}}
aria-label={this.label}
role="img"
part="base"
>
{this.initials && (
<span
class="absolute left-0 top-0 inline-flex h-full w-full items-center justify-center font-bold"
part="text"
>
{this.trimmedInitials}
</span>
)}
{this.image && !this.hasError && (
<img
class="absolute left-0 top-0 h-full w-full object-cover"
alt={this.altText ?? undefined}
src={this.image}
onError={this.onImageError}
part="img"
/>
)}
</div>
<div
class={{
'absolute flex items-center justify-center': true,
'left-[var(--bq-avatar--badge-left-square)] top-[var(--bq-avatar--badge-top-square)]':
this.shape === 'square',
'left-[var(--bq-avatar--badge-left-circle)] top-[var(--bq-avatar--badge-top-circle)]':
this.shape === 'circle',
}}
>
<slot name="badge"></slot>
</div>
</Host>
);
}
}
17 changes: 12 additions & 5 deletions packages/bee-q/src/components/avatar/scss/bq-avatar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,30 @@
@import './bq-avatar.variables';

:host {
@apply inline-block;
@apply relative inline-block;
}

.bq-avatar {
@apply relative overflow-hidden bg-[var(--bq-avatar-background)];
@apply border-[length:var(--bq-avatar--border-width)] border-[color:var(--bq-avatar--border-color)];

border-style: var(--bq-avatar--border-style);
}

.size {
&--xsmall {
@apply h-[var(--bq-avatar--size-xsmall)] w-[var(--bq-avatar--size-xsmall)] text-xs;
@apply h-[var(--bq-avatar--size-xsmall)] w-[var(--bq-avatar--size-xsmall)] text-[length:var(--bq-avatar--font-size-xsmall)];
}

&--small {
@apply h-[var(--bq-avatar--size-small)] w-[var(--bq-avatar--size-small)] text-xs;
@apply h-[var(--bq-avatar--size-small)] w-[var(--bq-avatar--size-small)] text-[length:var(--bq-avatar--font-size-small)];
}

&--medium {
@apply h-[var(--bq-avatar--size-medium)] w-[var(--bq-avatar--size-medium)] text-m;
@apply h-[var(--bq-avatar--size-medium)] w-[var(--bq-avatar--size-medium)] text-[length:var(--bq-avatar--font-size-medium)];
}

&--large {
@apply h-[var(--bq-avatar--size-large)] w-[var(--bq-avatar--size-large)] text-m;
@apply h-[var(--bq-avatar--size-large)] w-[var(--bq-avatar--size-large)] text-[length:var(--bq-avatar--font-size-large)];
}
}
44 changes: 44 additions & 0 deletions packages/bee-q/src/components/avatar/scss/bq-avatar.variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,52 @@
/* -------------------------------------------------------------------------- */

:host {
/**
* @prop --bq-avatar--background: Avatar background color
* @prop --bq-avatar--border-color: Avatar border color
* @prop --bq-avatar--border-style: Avatar border style
* @prop --bq-avatar--border-width: Avatar border width
* @prop --bq-avatar--border-radius-circle: Avatar border radius for circle & any size
* @prop --bq-avatar--border-radius-squareXs: Avatar border radius for square & size xsmall
* @prop --bq-avatar--border-radius-squareS: Avatar border radius for square & size small
* @prop --bq-avatar--border-radius-squareM: Avatar border radius for square & size medium/large
* @prop --bq-avatar--size-xsmall: Avatar xsmall size
* @prop --bq-avatar--size-small: Avatar small size
* @prop --bq-avatar--size-medium: Avatar medium size
* @prop --bq-avatar--size-large: Avatar large size
* @prop --bq-avatar--badge-top-square: Badge top position shape square
* @prop --bq-avatar--badge-left-square: Badge left position shape square
* @prop --bq-avatar--badge-top-circle: Badge top position shape circle
* @prop --bq-avatar--badge-left-circle: Badge left position shape circle
*/

--bq-avatar-background: theme('colors.ui.secondary-light');

--bq-avatar--border-color: theme('colors.stroke.tiertary');
--bq-avatar--border-style: solid;
--bq-avatar--border-width: 2px;

--bq-avatar--border-radius-circle: theme('borderRadius.full');
--bq-avatar--border-radius-squareXs: theme('borderRadius.xs');
--bq-avatar--border-radius-squareS: theme('borderRadius.s');
--bq-avatar--border-radius-squareM: theme('borderRadius.m');

--bq-avatar--size-xsmall: 24px;
--bq-avatar--size-small: 32px;
--bq-avatar--size-medium: 48px;
--bq-avatar--size-large: 64px;

--bq-avatar--font-size-xsmall: theme('fontSize.xs');
--bq-avatar--font-size-small: theme('fontSize.xs');
--bq-avatar--font-size-medium: theme('fontSize.m');
--bq-avatar--font-size-large: theme('fontSize.m');

--bq-avatar--badge-top-square: -5px;
--bq-avatar--badge-left-square: 80%;
--bq-avatar--badge-top-circle: 0;
--bq-avatar--badge-left-circle: 75%;
}
2 changes: 1 addition & 1 deletion packages/bee-q/src/components/badge/bq-badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class BqBadge {
/** Badge number color. The value should be a valid value of the palette color */
@Prop({ mutable: true, reflect: true }) textColor? = 'text--inverse';

/** The size of the badge */
/** The size of the badge. Relevant if badge has no content. */
@Prop({ reflect: true, mutable: true }) size?: TBadgeSize = 'small';

// Prop lifecycle events
Expand Down
2 changes: 1 addition & 1 deletion packages/bee-q/src/components/badge/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
| Property | Attribute | Description | Type | Default |
| ----------------- | ------------------ | ------------------------------------------------------------------------------ | --------------------- | ----------------- |
| `backgroundColor` | `background-color` | Badge background color. The value should be a valid value of the palette color | `string` | `'ui--danger'` |
| `size` | `size` | The size of the badge | `"medium" \| "small"` | `'small'` |
| `size` | `size` | The size of the badge. Relevant if badge has no content. | `"medium" \| "small"` | `'small'` |
| `textColor` | `text-color` | Badge number color. The value should be a valid value of the palette color | `string` | `'text--inverse'` |


Expand Down

0 comments on commit 99c8c42

Please sign in to comment.