diff --git a/src/dev/pages/avatar/avatar.ejs b/src/dev/pages/avatar/avatar.ejs index 77eec2c6b..1a8c646e2 100644 --- a/src/dev/pages/avatar/avatar.ejs +++ b/src/dev/pages/avatar/avatar.ejs @@ -16,14 +16,14 @@

w/Custom Radius (10px)

- +

w/Icon

- +
@@ -32,7 +32,7 @@

w/Custom Slotted Content and CSS Variable

- A + A
diff --git a/src/dev/pages/avatar/avatar.html b/src/dev/pages/avatar/avatar.html index 37054fba9..11282eec1 100644 --- a/src/dev/pages/avatar/avatar.html +++ b/src/dev/pages/avatar/avatar.html @@ -3,9 +3,7 @@ page: { title: 'Avatar', includePath: './pages/avatar/avatar.ejs', - options: [ - { type: 'switch', label: 'Auto color', id: 'auto-color-checkbox' } - ] + options: [] } }) %> diff --git a/src/dev/pages/avatar/avatar.ts b/src/dev/pages/avatar/avatar.ts index 939fd1baa..e15f41266 100644 --- a/src/dev/pages/avatar/avatar.ts +++ b/src/dev/pages/avatar/avatar.ts @@ -1,15 +1,3 @@ // Components import '@tylertech/forge/avatar'; -import type { AvatarComponent } from '@tylertech/forge/avatar'; -import type { ISwitchComponent } from '@tylertech/forge/switch'; import '$src/shared'; - -function getAvatarElements(): NodeListOf { - return document.querySelectorAll('.content forge-avatar'); -} - -const autoColorToggle = document.querySelector('#auto-color-checkbox') as ISwitchComponent; -autoColorToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { - const avatars = getAvatarElements(); - avatars.forEach(avatar => avatar.autoColor = selected); -}); diff --git a/src/lib/avatar/_core.scss b/src/lib/avatar/_core.scss new file mode 100644 index 000000000..aa0e95162 --- /dev/null +++ b/src/lib/avatar/_core.scss @@ -0,0 +1,38 @@ +@use '../core/styles/typography'; +@use './token-utils' as *; + +@forward './token-utils'; + +@mixin host() { + contain: content; + + display: inline-block; +} + +@mixin base() { + display: flex; + align-items: center; + justify-content: center; + + overflow: hidden; + + transition: height #{token(transition-duration)} #{token(transition-timing)}, + width #{token(transition-duration)} #{token(transition-timing)}; + + border-radius: #{token(shape)}; + box-sizing: border-box; + width: #{token(size)}; + height: #{token(size)}; + + background-color: #{token(background)}; + background-position: center; + background-repeat: no-repeat; + background-size: cover; + + @include typography.style(subheading2); + color: #{token(color)}; +} + +@mixin base-with-image() { + background-color: inherit; +} diff --git a/src/lib/avatar/_mixins.scss b/src/lib/avatar/_mixins.scss deleted file mode 100644 index 4c3942877..000000000 --- a/src/lib/avatar/_mixins.scss +++ /dev/null @@ -1,41 +0,0 @@ -@use '@material/animation/variables' as animation-variables; -@use '../theme'; -@use '../typography/mixins' as typography-mixins; -@use './variables'; - -@mixin provide-theme($theme) { - @include theme.theme-properties(avatar, $theme, variables.$theme-values); -} - -@mixin core-styles() { - .forge-avatar { - @include base; - } -} - -@mixin host() { - display: inline-block; - contain: content; -} - -@mixin base() { - @include typography-mixins.typography(title); - @include theme.css-custom-property(font-size, --forge-avatar-font-size, variables.$font-size); - @include theme.css-custom-property(font-weight, --forge-avatar-font-weight, variables.$font-weight); - @include theme.css-custom-property(background-color, --forge-avatar-theme-background, variables.$background); - @include theme.css-custom-property(height, --forge-avatar-size, variables.$size); - @include theme.css-custom-property(width, --forge-avatar-size, variables.$size); - @include theme.css-custom-property(color, --forge-avatar-theme-on-background, variables.$on-background); - @include theme.css-custom-property(border-radius, --forge-avatar-radius, variables.$border-radius); - - display: flex; - justify-content: center; - align-items: center; - box-sizing: border-box; - background-position: center; - background-repeat: no-repeat; - background-size: cover; - overflow: hidden; - transition: height variables.$transition-duration animation-variables.$standard-curve-timing-function, - width variables.$transition-duration animation-variables.$standard-curve-timing-function; -} diff --git a/src/lib/avatar/_token-utils.scss b/src/lib/avatar/_token-utils.scss new file mode 100644 index 000000000..b7404f8c5 --- /dev/null +++ b/src/lib/avatar/_token-utils.scss @@ -0,0 +1,25 @@ +@use '../core/styles/tokens/avatar/tokens'; +@use '../core/styles/tokens/token-utils'; + +$_module: avatar; +$_tokens: tokens.$tokens; + +@mixin provide-theme($theme) { + @include token-utils.provide-theme($_module, $_tokens, $theme); +} + +@function token($name, $type: token) { + @return token-utils.token($_module, $_tokens, $name, $type); +} + +@function declare($token) { + @return token-utils.declare($_module, $token); +} + +@mixin override($token, $token-or-value, $type: token) { + @include token-utils.override($_module, $_tokens, $token, $token-or-value, $type); +} + +@mixin tokens($includes: null, $excludes: null) { + @include token-utils.tokens($_module, $_tokens, $includes, $excludes); +} diff --git a/src/lib/avatar/_variables.scss b/src/lib/avatar/_variables.scss deleted file mode 100644 index 1361181ee..000000000 --- a/src/lib/avatar/_variables.scss +++ /dev/null @@ -1,11 +0,0 @@ -$size: 40px !default; -$font-size: 1rem !default; -$font-weight: 400 !default; -$background: none; -$on-background: #ffffff !default; -$transition-duration: 200ms !default; -$border-radius: 50% !default; -$theme-values: ( - background: $background, - on-background: $on-background -); diff --git a/src/lib/avatar/avatar-adapter.ts b/src/lib/avatar/avatar-adapter.ts index d7d3b2460..cb1a3cb07 100644 --- a/src/lib/avatar/avatar-adapter.ts +++ b/src/lib/avatar/avatar-adapter.ts @@ -4,7 +4,6 @@ import { IAvatarComponent } from './avatar'; import { AVATAR_CONSTANTS } from './avatar-constants'; export interface IAvatarAdapter extends IBaseAdapter { - setBackgroundColor(color: string): void; setBackgroundImageUrl(url: string): Promise; removeBackgroundImage(): void; setText(value: string): void; @@ -24,24 +23,14 @@ export class AvatarAdapter extends BaseAdapter implements IAva this._defaultSlot = getShadowElement(this._component, AVATAR_CONSTANTS.selectors.DEFAULT_SLOT) as HTMLSlotElement; } - /** - * Sets the `backgroundColor` style on the content element. - * @param {string} value The background color. - */ - public setBackgroundColor(value: string): void { - this._root.style.backgroundColor = `var(${AVATAR_CONSTANTS.strings.BACKGROUND_VARNAME}, ${value})`; - } - /** * Sets the background image URL. * @param url The URL. */ - public setBackgroundImageUrl(url: string): Promise { - const backgroundColor = this._root.style.backgroundColor; - // doing his before the promise so it doesn't flash a color before loading - this._root.style.backgroundColor = 'inherit'; - - const loadResult = new Promise(resolve => { + public async setBackgroundImageUrl(url: string): Promise { + // Set before loading image to prevent a flash of background color + this._root.classList.add('forge-avatar--image'); + return new Promise(resolve => { const image = new Image(); image.onload = () => { this._root.style.backgroundImage = `url(${image.src})`; @@ -49,14 +38,12 @@ export class AvatarAdapter extends BaseAdapter implements IAva }; image.onerror = () => { - this._root.style.backgroundColor = backgroundColor; + this._root.classList.remove('forge-avatar--image'); resolve(false); }; image.src = url; }); - - return loadResult; } /** @@ -64,6 +51,7 @@ export class AvatarAdapter extends BaseAdapter implements IAva */ public removeBackgroundImage(): void { this._root.style.removeProperty('background-image'); + this._root.classList.remove('forge-avatar--image'); } /** diff --git a/src/lib/avatar/avatar-component-delegate.ts b/src/lib/avatar/avatar-component-delegate.ts deleted file mode 100644 index 5b99a94bb..000000000 --- a/src/lib/avatar/avatar-component-delegate.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { BaseComponentDelegate, IBaseComponentDelegateConfig, IBaseComponentDelegateOptions } from '../core/delegates/base-component-delegate'; -import { IAvatarComponent } from './avatar'; -import { AVATAR_CONSTANTS } from './avatar-constants'; - -export type AvatarComponentDelegateProps = Partial; -export interface IAvatarComponentDelegateOptions extends IBaseComponentDelegateOptions {} -export interface IAvatarComponentDelegateConfig extends IBaseComponentDelegateConfig {} - -export class AvatarComponentDelegate extends BaseComponentDelegate { - constructor(config?: IAvatarComponentDelegateConfig) { - super(config); - } - - protected _build(): IAvatarComponent { - return document.createElement(AVATAR_CONSTANTS.elementName); - } -} diff --git a/src/lib/avatar/avatar-constants.ts b/src/lib/avatar/avatar-constants.ts index 764b745df..58b4aa1eb 100644 --- a/src/lib/avatar/avatar-constants.ts +++ b/src/lib/avatar/avatar-constants.ts @@ -6,8 +6,7 @@ const elementName: keyof HTMLElementTagNameMap = `${COMPONENT_NAME_PREFIX}avatar const attributes = { IMAGE_URL: 'image-url', TEXT: 'text', - LETTER_COUNT: 'letter-count', - AUTO_COLOR: 'auto-color' + LETTER_COUNT: 'letter-count' }; const numbers = { @@ -21,7 +20,7 @@ const selectors = { const strings = { DEFAULT_COLOR: COLOR_CONSTANTS.themeColors.tertiary, - BACKGROUND_VARNAME: '--forge-avatar-theme-background' + BACKGROUND_VARNAME: '--forge-avatar-background' }; export const AVATAR_CONSTANTS = { diff --git a/src/lib/avatar/avatar-foundation.ts b/src/lib/avatar/avatar-foundation.ts index d3f4801e7..46be957ea 100644 --- a/src/lib/avatar/avatar-foundation.ts +++ b/src/lib/avatar/avatar-foundation.ts @@ -1,15 +1,11 @@ import { ICustomElementFoundation, isDefined, isString } from '@tylertech/forge-core'; -import { getTextColor } from '../utils/color-utils'; import { IAvatarAdapter } from './avatar-adapter'; import { AVATAR_CONSTANTS } from './avatar-constants'; - - export interface IAvatarFoundation extends ICustomElementFoundation { imageUrl: string; text: string; letterCount: number; - autoColor: boolean; } /** @@ -19,7 +15,6 @@ export class AvatarFoundation implements IAvatarFoundation { private _imageUrl: string; private _text = ''; private _letterCount = AVATAR_CONSTANTS.numbers.DEFAULT_LETTER_COUNT; - private _autoColor = false; private _initialized = false; constructor(private _adapter: IAvatarAdapter) {} @@ -56,9 +51,6 @@ export class AvatarFoundation implements IAvatarFoundation { } else { this._adapter.clearText(); } - - const color = this._autoColor ? getTextColor(data) : AVATAR_CONSTANTS.strings.DEFAULT_COLOR; - this._adapter.setBackgroundColor(color); } /** @@ -122,16 +114,4 @@ export class AvatarFoundation implements IAvatarFoundation { } } } - - /** Controls whether the background color set automatically based on the text value. Does not have any effect when an image URL is specified. */ - public get autoColor(): boolean { - return this._autoColor; - } - public set autoColor(value: boolean) { - if (this._autoColor !== value) { - this._autoColor = value; - this._setText(); - this._adapter.setHostAttribute(AVATAR_CONSTANTS.attributes.AUTO_COLOR, isDefined(this._autoColor) ? this._autoColor.toString() : ''); - } - } } diff --git a/src/lib/avatar/avatar.scss b/src/lib/avatar/avatar.scss index bef4cc3c9..6137b1f8b 100644 --- a/src/lib/avatar/avatar.scss +++ b/src/lib/avatar/avatar.scss @@ -1,11 +1,29 @@ -@use './mixins'; +@use './core' as *; + +// +// Host +// :host { - @include mixins.host; + @include host; } :host([hidden]) { display: none; } -@include mixins.core-styles; +// +// Base +// + +.forge-avatar { + @include tokens; +} + +.forge-avatar { + @include base; + + &--image { + @include base-with-image; + } +} diff --git a/src/lib/avatar/avatar.test.ts b/src/lib/avatar/avatar.test.ts new file mode 100644 index 000000000..ce602bcb2 --- /dev/null +++ b/src/lib/avatar/avatar.test.ts @@ -0,0 +1,122 @@ +import { expect } from '@esm-bundle/chai'; +import { fixture, html } from '@open-wc/testing'; +import { timer } from '@tylertech/forge-testing'; +import { getShadowElement } from '@tylertech/forge-core'; +import { IAvatarComponent } from './avatar'; +import { AVATAR_CONSTANTS } from './avatar-constants'; + +import './avatar' + +describe('Avatar', () => { + it('should initialize', async () => { + const el = await fixture(html``); + + expect(el.shadowRoot).not.to.be.null; + }); + + it('should should be accessible', async () => { + const el = await fixture(html``); + + await expect(el).to.be.accessible(); + }); + + it('should set slot text content to first characters of text attribute', async () => { + const el = await fixture(html``); + + expect(el.text).to.equal('Tyler Forge'); + expect(getDefaultSlotEl(el).textContent).to.equal('TF'); + }); + + it('should set restrict letter count when attribute set', async () => { + const el = await fixture(html``); + + el.setAttribute(AVATAR_CONSTANTS.attributes.LETTER_COUNT, '1'); + + expect(getDefaultSlotEl(el).textContent).to.equal('T'); + }); + + it('should update the letter count attribute when the property is set ', async () => { + const el = await fixture(html``); + + el.letterCount = 3; + + expect(el.getAttribute(AVATAR_CONSTANTS.attributes.LETTER_COUNT)).to.equal('3'); + }); + + it('should change background image when set via attribute', async () => { + const el = await fixture(html``); + + const url = 'https://empower.tylertech.com/rs/015-NUU-525/images/tyler-logo-color.svg'; + el.setAttribute(AVATAR_CONSTANTS.attributes.IMAGE_URL, url); + const root = getRootEl(el); + // Give enough time for the image to load + await timer(1800); + + expect(root.hasAttribute('style')).to.be.true; + expect(root.style.backgroundImage).to.equal(`url("${url}")`); + }); + + it('should change text when set via attribute', async () => { + const el = await fixture(html``); + + el.setAttribute(AVATAR_CONSTANTS.attributes.TEXT, 'New Value'); + + expect(getDefaultSlotEl(el).textContent).to.equal('NV'); + }); + + it('should render text when image url fails to load an image with 500 error', async () => { + const el = await fixture(html``); + const root = getRootEl(el); + + await timer(300); + + expect(getDefaultSlotEl(el).textContent).to.equal('IU'); + expect(root.hasAttribute('style')).to.be.false; + }); + + it('should render text when image url fails to load an image with 404 error', async () => { + const el = await fixture(html``); + const root = getRootEl(el); + + await timer(300); + + expect(getDefaultSlotEl(el).textContent).to.equal('IU'); + expect(root.hasAttribute('style')).to.be.false; + }); + + it('should have proper default values', async () => { + const el = await fixture(html``); + + expect(el.text).to.equal(''); + expect(el.letterCount).to.equal(AVATAR_CONSTANTS.numbers.DEFAULT_LETTER_COUNT); + expect(el.imageUrl).to.be.undefined; + }); + + it('should have not have any content by default', async () => { + const el = await fixture(html``); + + expect(getDefaultSlotEl(el).textContent).to.equal(''); + }); + + it('should render slotted content in place of text content', async () => { + const el = await fixture(html`Slotted Content`); + const defaultSlot = getDefaultSlotEl(el); + + expect(defaultSlot.textContent).to.equal('AT'); + expect(defaultSlot.assignedNodes().length).to.equal(1); + }); + + it('should render first character of each word when letter count is greater than number of words', async () => { + const el = await fixture(html`Slotted Content`); + const defaultSlot = getDefaultSlotEl(el); + + expect(defaultSlot.textContent).to.equal('SLSWS'); + }); + + function getRootEl(el: IAvatarComponent): HTMLElement { + return el.shadowRoot?.firstElementChild as HTMLElement; + } + function getDefaultSlotEl(el: IAvatarComponent): HTMLSlotElement { + return getShadowElement(el, AVATAR_CONSTANTS.selectors.DEFAULT_SLOT) as HTMLSlotElement; + } +}); \ No newline at end of file diff --git a/src/lib/avatar/avatar.ts b/src/lib/avatar/avatar.ts index a4cfd64fc..e5aa737b7 100644 --- a/src/lib/avatar/avatar.ts +++ b/src/lib/avatar/avatar.ts @@ -1,4 +1,4 @@ -import { CustomElement, attachShadowTemplate, coerceNumber, coerceBoolean, FoundationProperty } from '@tylertech/forge-core'; +import { CustomElement, attachShadowTemplate, coerceNumber, FoundationProperty } from '@tylertech/forge-core'; import { AvatarAdapter } from './avatar-adapter'; import { AvatarFoundation } from './avatar-foundation'; @@ -12,7 +12,6 @@ export interface IAvatarComponent extends IBaseComponent { imageUrl: string; text: string; letterCount: number; - autoColor: boolean; } declare global { @@ -22,9 +21,33 @@ declare global { } /** - * The custom element class behind the `` element. - * * @tag forge-avatar + * + * @summary Avatars represent an entity via text or image. + * + * @description + * The avatar component allows you to provide text or images to display that represent an entity. By default, the + * avatar will display textual content as single characters (character count is configurable), or display an image or + * icon based on the URL provided to it. + * + * @property {string} text - The text to display in the avatar. + * @property {number} letterCount - Controls the number of letters to display from the text. By default the text is split on spaces and the first character of each word is used. + * @property {string} imageUrl - The background image URL to use. + * + * @attribute {string} text - The text to display in the avatar. + * @attribute {number} letter-count - Controls the number of letters to display from the text. By default the text is split on spaces and the first character of each word is used. + * @attribute {string} image-url - The background image URL to use. + * + * @cssproperty --forge-avatar-background - The background color of the avatar. + * @cssproperty --forge-avatar-shape - The border radius of the avatar, defaults to 50%. + * @cssproperty --forge-avatar-color - The text color of the avatar. + * @cssproperty --forge-avatar-size - The height and width of the avatar. + * @cssproperty --forge-avatar-transition-duration - The transition duration for animations. + * @cssproperty --forge-avatar-transition-timing - The transition timing function for animations. + * + * @csspart root - The root container element. + * + * @slot - The default/unnamed slot for avatar content if not provided via text/imageUrl. */ @CustomElement({ name: AVATAR_CONSTANTS.elementName @@ -34,8 +57,7 @@ export class AvatarComponent extends BaseComponent implements IAvatarComponent { return [ AVATAR_CONSTANTS.attributes.TEXT, AVATAR_CONSTANTS.attributes.LETTER_COUNT, - AVATAR_CONSTANTS.attributes.IMAGE_URL, - AVATAR_CONSTANTS.attributes.AUTO_COLOR + AVATAR_CONSTANTS.attributes.IMAGE_URL ]; } @@ -66,13 +88,10 @@ export class AvatarComponent extends BaseComponent implements IAvatarComponent { case AVATAR_CONSTANTS.attributes.IMAGE_URL: this.imageUrl = newValue; break; - case AVATAR_CONSTANTS.attributes.AUTO_COLOR: - this.autoColor = coerceBoolean(newValue); - break; } } - /** Gets/sets the text to display. */ + /** The text to display in the avatar. */ @FoundationProperty() public declare text: string; @@ -80,11 +99,7 @@ export class AvatarComponent extends BaseComponent implements IAvatarComponent { @FoundationProperty() public declare letterCount: number; - /** Sets the background image URL to use. */ + /** The background image URL to use. */ @FoundationProperty() public declare imageUrl: string; - - /** Controls whether the background color is set automatically based on the text value. Does not have any effect when an image URL is specified. */ - @FoundationProperty() - public declare autoColor: boolean; } diff --git a/src/lib/avatar/index.ts b/src/lib/avatar/index.ts index 9ddf95094..8e9b8b867 100644 --- a/src/lib/avatar/index.ts +++ b/src/lib/avatar/index.ts @@ -6,7 +6,6 @@ export * from './avatar-adapter'; export * from './avatar-constants'; export * from './avatar-foundation'; export * from './avatar'; -export * from './avatar-component-delegate'; export function defineAvatarComponent(): void { defineCustomElement(AvatarComponent); diff --git a/src/lib/core/styles/tokens/avatar/_tokens.scss b/src/lib/core/styles/tokens/avatar/_tokens.scss new file mode 100644 index 000000000..aca79e869 --- /dev/null +++ b/src/lib/core/styles/tokens/avatar/_tokens.scss @@ -0,0 +1,18 @@ +@use 'sass:map'; +@use '../../animation'; +@use '../../shape'; +@use '../../theme'; +@use '../../utils'; + +$tokens: ( + size: utils.module-val(avatar, size, 40px), + background: utils.module-val(avatar, background, theme.variable(tertiary)), + color: utils.module-val(avatar, color, theme.variable(on-tertiary)), + transition-duration: utils.module-val(avatar, transition-duration, animation.variable(duration-short4)), + transition-timing: utils.module-val(avatar, transition-timing, animation.variable(easing-standard)), + shape: utils.module-val(avatar, shape, shape.variable(round)) +) !default; + +@function get($key) { + @return map.get($tokens, $key); +} diff --git a/src/stories/src/components/avatar/avatar-args.ts b/src/stories/src/components/avatar/avatar-args.ts index 282c7c2f9..b2770bad4 100644 --- a/src/stories/src/components/avatar/avatar-args.ts +++ b/src/stories/src/components/avatar/avatar-args.ts @@ -1,5 +1,4 @@ export interface IAvatarProps { - autoColor: boolean; imageUrl: string; useIcon: boolean; letterCount: number; @@ -7,12 +6,6 @@ export interface IAvatarProps { } export const argTypes = { - autoColor: { - control: 'boolean', - table: { - category: 'Properties' - } - }, imageUrl: { control: 'text', table: { diff --git a/src/stories/src/components/avatar/avatar.mdx b/src/stories/src/components/avatar/avatar.mdx index f06fc3a33..0020e0a7f 100644 --- a/src/stories/src/components/avatar/avatar.mdx +++ b/src/stories/src/components/avatar/avatar.mdx @@ -49,12 +49,6 @@ When using with an image, the avatar will apply your image as that background of ## Properties/Attributes - - -Controls whether the background color is set automatically based on the text value. Does not have any effect when an image URL is specified. - - - Sets the background image URL to use in place of the text representation. @@ -103,12 +97,12 @@ Gets/set the text to display as characters. If spaces exist, the first letter of | Name | Description | :-----------------------------------------------| :-------------------- -| `--forge-avatar-theme-background` | Controls the `background-color` style. -| `--forge-avatar-font-size` | Controls the `font-size` style. -| `--forge-avatar-font-weight` | Controls the `font-weight` style. -| `--forge-avatar-theme-on-background` | Controls the `color` of the font. -| `--forge-avatar-radius` | Controls the `border-radius` style. Can be useful for custom avatar shapes. -| `--forge-avatar-size` | Controls the `height` and `width` styles together. +| `--forge-avatar-background` | Controls the `background-color` style. +| `--forge-avatar-color` | Controls the `color` of the font. +| `--forge-avatar-shape` | Controls the `border-radius` style. Can be useful for custom avatar shapes. +| `--forge-avatar-size` | Controls the `height` and `width` styles together. +| `--forge-avatar-transition-duration` | The transition duration for animations. +| `--forge-avatar-transition-timing` | The transition timing function for animations. diff --git a/src/stories/src/components/avatar/avatar.stories.tsx b/src/stories/src/components/avatar/avatar.stories.tsx index cda465b61..437865cad 100644 --- a/src/stories/src/components/avatar/avatar.stories.tsx +++ b/src/stories/src/components/avatar/avatar.stories.tsx @@ -19,7 +19,6 @@ export default { } as Meta; export const Default: Story = ({ - autoColor = false, imageUrl = '', useIcon = false, letterCount = 2, @@ -31,7 +30,6 @@ export const Default: Story = ({ return ( @@ -40,7 +38,6 @@ export const Default: Story = ({ ); }; Default.args = { - autoColor: false, imageUrl: '', useIcon: false, letterCount: 2, diff --git a/src/test/spec/avatar/avatar.spec.ts b/src/test/spec/avatar/avatar.spec.ts deleted file mode 100644 index 666176771..000000000 --- a/src/test/spec/avatar/avatar.spec.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { IAvatarComponent, AVATAR_CONSTANTS, defineAvatarComponent } from '@tylertech/forge/avatar'; -import { tick, appendElement, timer } from '@tylertech/forge-testing'; -import { removeElement, getShadowElement } from '@tylertech/forge-core'; - -const DEFAULT_TEXT = 'Tom Brady'; -const DEFAULT_LETTER_COUNT = 1; - -interface ITestContext { - context: ITestAvatarContext; -} - -interface ITestAvatarContext { - component: IAvatarComponent; - destroy(): void; -} - -describe('AvatarComponent', function(this: ITestContext) { - beforeAll(function(this: ITestContext) { - defineAvatarComponent(); - }); - - afterEach(function(this: ITestContext) { - this.context.destroy(); - }); - - describe('with default attribute values', function(this: ITestContext) { - it('should set slot text to first character of text', function(this: ITestContext) { - this.context = setupTestContext(true); - const defaultSlot = getShadowElement(this.context.component, AVATAR_CONSTANTS.selectors.DEFAULT_SLOT); - expect(defaultSlot.textContent).toBe('T'); - }); - - it('should change background image when set via attribute', async function(this: ITestContext) { - this.context = setupTestContext(true); - await tick(); - - const url = 'https://empower.tylertech.com/rs/015-NUU-525/images/tyler-logo-color.svg'; - this.context.component.setAttribute(AVATAR_CONSTANTS.attributes.IMAGE_URL, url); - const root = getShadowElement(this.context.component, AVATAR_CONSTANTS.selectors.ROOT); - // Give enough time for the image to load - await timer(2000); - expect(root.hasAttribute('style')).toBe(true); - expect(root.getAttribute('style')).toContain('background-image'); - expect(root.getAttribute('style')).toContain(url); - }); - - it('should change text content when set via attribute', async function(this: ITestContext) { - this.context = setupTestContext(true); - await tick(); - this.context.component.setAttribute(AVATAR_CONSTANTS.attributes.TEXT, 'New Text'); - const defaultSlot = getShadowElement(this.context.component, AVATAR_CONSTANTS.selectors.DEFAULT_SLOT); - expect(defaultSlot.textContent).toBe('N'); - }); - - it('should change text content when letter count is set via attribute', async function(this: ITestContext) { - this.context = setupTestContext(true); - await tick(); - this.context.component.setAttribute(AVATAR_CONSTANTS.attributes.LETTER_COUNT, '3'); - const defaultSlot = getShadowElement(this.context.component, AVATAR_CONSTANTS.selectors.DEFAULT_SLOT); - expect(defaultSlot.textContent).toBe('TB'); - }); - }); - - describe('without default values', function(this: ITestContext) { - it('should have proper default values', function(this: ITestContext) { - this.context = setupTestContext(true, false); - expect(this.context.component.text).toBe(''); - expect(this.context.component.letterCount).toBe(AVATAR_CONSTANTS.numbers.DEFAULT_LETTER_COUNT); - expect(this.context.component.imageUrl).toBeUndefined(); - expect(this.context.component.autoColor).toBeFalse(); - }); - - it('should have not have any content by default', function(this: ITestContext) { - this.context = setupTestContext(true, false); - const defaultSlot = getShadowElement(this.context.component, AVATAR_CONSTANTS.selectors.DEFAULT_SLOT); - expect(defaultSlot.textContent).toBe(''); - }); - - it('should not set background-color when text is not set', async function(this: ITestContext) { - this.context = setupTestContext(true, false); - const root = getShadowElement(this.context.component, AVATAR_CONSTANTS.selectors.ROOT); - expect(root.style.backgroundColor).toBe(`var(${AVATAR_CONSTANTS.strings.BACKGROUND_VARNAME}, ${AVATAR_CONSTANTS.strings.DEFAULT_COLOR})`); - }); - - it('should set text content properly using default letter count', function(this: ITestContext) { - this.context = setupTestContext(true, false); - const defaultSlot = getShadowElement(this.context.component, AVATAR_CONSTANTS.selectors.DEFAULT_SLOT); - this.context.component.text = DEFAULT_TEXT; - expect(defaultSlot.innerText.length).toBe(AVATAR_CONSTANTS.numbers.DEFAULT_LETTER_COUNT); - expect(defaultSlot.textContent).toBe('TB'); - }); - - it('should set background-color when text is set', function(this: ITestContext) { - this.context = setupTestContext(true, false); - const root = getShadowElement(this.context.component, AVATAR_CONSTANTS.selectors.ROOT); - this.context.component.text = DEFAULT_TEXT; - - expect(root.hasAttribute('style')).toBe(true); - expect(root.getAttribute('style')).toContain('background-color'); - expect(root.style.backgroundColor).not.toBeNull(); - expect(root.style.backgroundColor).not.toBe(''); - expect(root.style.backgroundColor!.length).toBeGreaterThan(0); - }); - - it('should set background-color when text is set and autoColor is false', function(this: ITestContext) { - this.context = setupTestContext(true, false); - this.context.component.autoColor = false; - - const root = getShadowElement(this.context.component, AVATAR_CONSTANTS.selectors.ROOT); - this.context.component.text = DEFAULT_TEXT; - - expect(root.style.backgroundColor).toBe(`var(${AVATAR_CONSTANTS.strings.BACKGROUND_VARNAME}, ${AVATAR_CONSTANTS.strings.DEFAULT_COLOR})`); - }); - - it('should render slotted content in place of text content', function(this: ITestContext) { - this.context = setupTestContext(true, false); - const defaultSlot = getShadowElement(this.context.component, AVATAR_CONSTANTS.selectors.DEFAULT_SLOT) as HTMLSlotElement; - - this.context.component.text = DEFAULT_TEXT; - - const node = document.createTextNode('Test Text'); - this.context.component.appendChild(node); - - expect(defaultSlot.textContent).toBe('TB'); - expect(defaultSlot.assignedNodes().length).toBe(1); - }); - - xit('should render background-image when image URL is set', async function(this: ITestContext) { - this.context = setupTestContext(true, false); - const url = 'https://empower.tylertech.com/rs/015-NUU-525/images/tyler-logo-color.svg'; - this.context.component.imageUrl = url; - - const root = getShadowElement(this.context.component, AVATAR_CONSTANTS.selectors.ROOT); - - // Give enough time for the image to load - await timer(2000); - - expect(root.hasAttribute('style')).toBe(true); - expect(root.getAttribute('style')).toContain('background-image'); - expect(root.getAttribute('style')).toContain(url); - }); - - it('should update text content when letter count changes', async function(this: ITestContext) { - this.context = setupTestContext(true, false); - const defaultSlot = getShadowElement(this.context.component, AVATAR_CONSTANTS.selectors.DEFAULT_SLOT) as HTMLSlotElement; - this.context.component.text = DEFAULT_TEXT; - await tick(); - expect(defaultSlot.textContent).toBe('TB'); - await tick(); - this.context.component.letterCount = 1; - expect(defaultSlot.textContent).toBe('T'); - }); - - it('should render first character of each word when letter count is greater than number of words', function(this: ITestContext) { - this.context = setupTestContext(true, false); - const defaultSlot = getShadowElement(this.context.component, AVATAR_CONSTANTS.selectors.DEFAULT_SLOT) as HTMLSlotElement; - this.context.component.text = 'some long string with spaces'; - this.context.component.letterCount = 6; // One greater than the number of words in the string - expect(defaultSlot.textContent).toBe('SLSWS'); - }); - - it('should render text when image url fails to load an image with 500 error', async function(this: ITestContext) { - this.context = setupTestContext(true, false); - const defaultSlot = getShadowElement(this.context.component, AVATAR_CONSTANTS.selectors.DEFAULT_SLOT) as HTMLSlotElement; - - this.context.component.text = 'Invalid Url'; - // Url that will return a 500 error - this.context.component.imageUrl = 'https://httpstat.us/500'; - await timer(300); - expect(defaultSlot.textContent).toBe('IU'); - }); - - it('should render text when image url fails to load an image', async function(this: ITestContext) { - this.context = setupTestContext(true, false); - const defaultSlot = getShadowElement(this.context.component, AVATAR_CONSTANTS.selectors.DEFAULT_SLOT) as HTMLSlotElement; - const root = getShadowElement(this.context.component, AVATAR_CONSTANTS.selectors.ROOT); - - this.context.component.text = 'Invalid Url'; - // Url that will return a 404 error - this.context.component.imageUrl = 'https://httpstat.us/404'; - await timer(300); - expect(defaultSlot.textContent).toBe('IU'); - expect(root.getAttribute('style')).not.toContain('background-image'); - }); - }); - - function setupTestContext(append = false, useDefaultValues = true): ITestAvatarContext { - const fixture = document.createElement('div'); - fixture.id = 'avatar-test-fixture'; - const component = document.createElement(AVATAR_CONSTANTS.elementName) as IAvatarComponent; - if (useDefaultValues) { - component.text = DEFAULT_TEXT; - component.letterCount = DEFAULT_LETTER_COUNT; - } - fixture.appendChild(component); - if (append) { - document.body.appendChild(fixture); - } - return { - component, - destroy: () => removeElement(fixture) - }; - } -});