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

feature(avatar): add badge #267

Merged
merged 10 commits into from
Jun 30, 2023
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)];
}
}
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