From ba9d7a11c8cc1fd66836ea42bcc5971f1eae02af Mon Sep 17 00:00:00 2001 From: Kieran Nichols Date: Thu, 1 Feb 2024 13:36:38 -0500 Subject: [PATCH] [@next][badge] refactor to use design tokens (#463) --- src/dev/pages/badge/badge.ejs | 53 +++-- src/dev/pages/badge/badge.ts | 5 + .../app-bar-notification-button-adapter.ts | 2 +- .../app-bar-notification-button.test.ts | 6 +- src/lib/badge/_core.scss | 53 +++++ src/lib/badge/_token-utils.scss | 25 +++ src/lib/badge/badge-adapter.ts | 26 --- src/lib/badge/badge-component-delegate.ts | 17 -- src/lib/badge/badge-constants.ts | 16 +- src/lib/badge/badge-foundation.ts | 61 ------ src/lib/badge/badge.html | 6 +- src/lib/badge/badge.scss | 116 ++++++++-- src/lib/badge/badge.test.ts | 85 ++++++++ src/lib/badge/badge.ts | 89 +++++--- src/lib/badge/build.json | 4 - src/lib/badge/index.scss | 1 + src/lib/badge/index.ts | 3 - .../circular-progress-foundation.ts | 3 + src/lib/core/styles/tokens/badge/_tokens.scss | 37 ++++ .../src/components/badge/badge-arg-types.ts | 22 +- .../src/components/badge/badge.stories.tsx | 48 ++--- src/test/spec/badge/badge.spec.ts | 199 ------------------ 22 files changed, 445 insertions(+), 432 deletions(-) create mode 100644 src/lib/badge/_core.scss create mode 100644 src/lib/badge/_token-utils.scss delete mode 100644 src/lib/badge/badge-adapter.ts delete mode 100644 src/lib/badge/badge-component-delegate.ts delete mode 100644 src/lib/badge/badge-foundation.ts create mode 100644 src/lib/badge/badge.test.ts delete mode 100644 src/lib/badge/build.json create mode 100644 src/lib/badge/index.scss create mode 100644 src/lib/core/styles/tokens/badge/_tokens.scss delete mode 100644 src/test/spec/badge/badge.spec.ts diff --git a/src/dev/pages/badge/badge.ejs b/src/dev/pages/badge/badge.ejs index a7fe656be..7bbf6001c 100644 --- a/src/dev/pages/badge/badge.ejs +++ b/src/dev/pages/badge/badge.ejs @@ -7,19 +7,30 @@
- - - + + + w/Start icon + + + w/End icon + + +
+ +
+ + + - - - 99 + + + 99 - - - 999999+ + + + 999999+
@@ -27,20 +38,26 @@

Theme

Default - Danger - Warning + Primary + Secondary + Tertiary Success - Info (primary) + Error + Warning + Info Info (secondary)
-

Strong theme

+

Theme (strong)

Default - Danger - Warning - Success - Info (primary) - Info (secondary) + Primary + Secondary + Tertiary + Success + Error + Warning + Info + Info (secondary)
diff --git a/src/dev/pages/badge/badge.ts b/src/dev/pages/badge/badge.ts index 1ef6d88b5..9552801e5 100644 --- a/src/dev/pages/badge/badge.ts +++ b/src/dev/pages/badge/badge.ts @@ -1,3 +1,8 @@ import '$src/shared'; import '@tylertech/forge/badge'; import '@tylertech/forge/icon-button'; +import { tylIconNotifications } from '@tylertech/tyler-icons/standard'; +import { IconRegistry } from '@tylertech/forge/icon'; +import { tylIconHeart } from '@tylertech/tyler-icons/extended'; + +IconRegistry.define([tylIconHeart, tylIconNotifications]); diff --git a/src/lib/app-bar/notification-button/app-bar-notification-button-adapter.ts b/src/lib/app-bar/notification-button/app-bar-notification-button-adapter.ts index 8f2f2f368..abb280a37 100644 --- a/src/lib/app-bar/notification-button/app-bar-notification-button-adapter.ts +++ b/src/lib/app-bar/notification-button/app-bar-notification-button-adapter.ts @@ -65,6 +65,6 @@ export class AppBarNotificationButtonAdapter extends BaseAdapter { const badgeEl = el.querySelector('forge-badge') as IBadgeComponent; expect(badgeEl).to.be.ok; - expect(badgeEl.open).to.be.true; + expect(badgeEl.hide).to.be.false; expect(badgeEl.innerText).to.equal('0'); }); @@ -78,7 +78,7 @@ describe('App Bar Notification Button', () => { el.showBadge = true; expect(badgeEl).to.be.ok; - expect(badgeEl.open).to.be.true; + expect(badgeEl.hide).to.be.false; expect(badgeEl.innerText).to.equal('0'); }); @@ -89,7 +89,7 @@ describe('App Bar Notification Button', () => { const badgeEl = el.querySelector('forge-badge') as IBadgeComponent; expect(badgeEl).to.be.ok; - expect(badgeEl.open).to.be.false; + expect(badgeEl.hide).to.be.true; }); it('should show badge with count', async () => { diff --git a/src/lib/badge/_core.scss b/src/lib/badge/_core.scss new file mode 100644 index 000000000..cedb0584a --- /dev/null +++ b/src/lib/badge/_core.scss @@ -0,0 +1,53 @@ +@use './token-utils' as *; +@use '../core/styles/typography'; + +@forward './token-utils'; + +@mixin host { + display: flex; + box-sizing: border-box; +} + +@mixin base { + @include typography.style(label); + + background: #{token(background)}; + color: #{token(color)}; + + height: #{token(height)}; + min-width: #{token(min-width)}; + max-width: #{token(max-width)}; + + border-width: #{token(border-width)}; + border-style: #{token(border-style)}; + border-color: #{token(border-color)}; + + display: inline-flex; + align-items: center; + gap: #{token(gap)}; + border-radius: #{token(shape)}; + padding-inline: #{token(padding-inline)}; + padding-block: #{token(padding-block)}; + overflow: hidden; + box-sizing: border-box; + + pointer-events: none; + + transition: transform #{token(transition-duration)} #{token(transition-easing)}; + + font-weight: #{token(font-weight)}; + text-overflow: ellipsis; + white-space: nowrap; +} + +@mixin dot { + @include override(height, dot-height); + @include override(min-width, auto, value); + + padding: #{token(dot-padding)}; + width: #{token(dot-width)}; +} + +@mixin hide { + transform: scale(0); +} diff --git a/src/lib/badge/_token-utils.scss b/src/lib/badge/_token-utils.scss new file mode 100644 index 000000000..76ecd48ab --- /dev/null +++ b/src/lib/badge/_token-utils.scss @@ -0,0 +1,25 @@ +@use '../core/styles/tokens/badge/tokens'; +@use '../core/styles/tokens/token-utils'; + +$_module: badge; +$_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/badge/badge-adapter.ts b/src/lib/badge/badge-adapter.ts deleted file mode 100644 index ca69f4d88..000000000 --- a/src/lib/badge/badge-adapter.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { addClass, getShadowElement, removeClass } from '@tylertech/forge-core'; -import { BaseAdapter, IBaseAdapter } from '../core/base/base-adapter'; -import { IBadgeComponent } from './badge'; -import { BADGE_CONSTANTS } from './badge-constants'; - -export interface IBadgeAdapter extends IBaseAdapter { - setRootClass(classes: string | string[]): void; - removeRootClass(classes: string | string[]): void; -} - -export class BadgeAdapter extends BaseAdapter implements IBadgeAdapter { - private _rootElement: HTMLElement; - - constructor(component: IBadgeComponent) { - super(component); - this._rootElement = getShadowElement(component, BADGE_CONSTANTS.selectors.ROOT); - } - - public setRootClass(classes: string | string[]): void { - addClass(classes, this._rootElement); - } - - public removeRootClass(classes: string | string[]): void { - removeClass(classes, this._rootElement); - } -} diff --git a/src/lib/badge/badge-component-delegate.ts b/src/lib/badge/badge-component-delegate.ts deleted file mode 100644 index 478c77729..000000000 --- a/src/lib/badge/badge-component-delegate.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { BaseComponentDelegate, IBaseComponentDelegateConfig, IBaseComponentDelegateOptions } from '../core/delegates/base-component-delegate'; -import { IBadgeComponent } from './badge'; -import { BADGE_CONSTANTS } from './badge-constants'; - -export type BadgeComponentDelegateProps = Partial; -export interface IBadgeComponentDelegateOptions extends IBaseComponentDelegateOptions {} -export interface IBadgeComponentDelegateConfig extends IBaseComponentDelegateConfig {} - -export class BadgeComponentDelegate extends BaseComponentDelegate { - constructor(config?: IBadgeComponentDelegateConfig) { - super(config); - } - - protected _build(): IBadgeComponent { - return document.createElement(BADGE_CONSTANTS.elementName); - } -} diff --git a/src/lib/badge/badge-constants.ts b/src/lib/badge/badge-constants.ts index 2b0a3f925..b66781a08 100644 --- a/src/lib/badge/badge-constants.ts +++ b/src/lib/badge/badge-constants.ts @@ -1,26 +1,32 @@ -import { COMPONENT_NAME_PREFIX } from '../constants'; +import { COMPONENT_NAME_PREFIX, Theme } from '../constants'; const elementName: keyof HTMLElementTagNameMap = `${COMPONENT_NAME_PREFIX}badge`; const attributes = { DOT: 'dot', - OPEN: 'open', + HIDE: 'hide', THEME: 'theme', STRONG: 'strong' }; const classes = { - DOT: 'forge-badge--dot', - OPEN: 'forge-badge--open' + OPEN: 'open' }; const selectors = { ROOT: '.forge-badge' }; +const defaults = { + THEME: 'default' as BadgeTheme +}; + export const BADGE_CONSTANTS = { elementName, attributes, + selectors, classes, - selectors + defaults }; + +export type BadgeTheme = Theme | 'default' | 'info-primary' | 'info-secondary' | 'danger'; diff --git a/src/lib/badge/badge-foundation.ts b/src/lib/badge/badge-foundation.ts deleted file mode 100644 index 11aa0bdcb..000000000 --- a/src/lib/badge/badge-foundation.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { ICustomElementFoundation } from '@tylertech/forge-core'; - -import { IBadgeAdapter } from './badge-adapter'; -import { BADGE_CONSTANTS } from './badge-constants'; - -export interface IBadgeFoundation extends ICustomElementFoundation { - dot: boolean; - open: boolean; -} - -export class BadgeFoundation implements IBadgeFoundation { - private _dot = false; - private _open = true; - - constructor(private _adapter: IBadgeAdapter) {} - - public initialize(): void { - this._applyDot(); - this._setOpen(); - } - - private _applyDot(): void { - if (this._dot) { - this._adapter.setRootClass(BADGE_CONSTANTS.classes.DOT); - this._adapter.setHostAttribute(BADGE_CONSTANTS.attributes.DOT); - } else { - this._adapter.removeRootClass(BADGE_CONSTANTS.classes.DOT); - this._adapter.removeHostAttribute(BADGE_CONSTANTS.attributes.DOT); - } - } - - private _setOpen(): void { - if (this._open) { - this._adapter.setRootClass(BADGE_CONSTANTS.classes.OPEN); - this._adapter.setHostAttribute(BADGE_CONSTANTS.attributes.OPEN); - } else { - this._adapter.removeRootClass(BADGE_CONSTANTS.classes.OPEN); - this._adapter.removeHostAttribute(BADGE_CONSTANTS.attributes.OPEN); - } - } - - public get dot(): boolean { - return this._dot; - } - public set dot(value: boolean) { - if (this._dot !== value) { - this._dot = value; - this._applyDot(); - } - } - - public get open(): boolean { - return this._open; - } - public set open(value: boolean) { - if (this._open !== value) { - this._open = value; - this._setOpen(); - } - } -} diff --git a/src/lib/badge/badge.html b/src/lib/badge/badge.html index d618420bd..ac7f70f9d 100644 --- a/src/lib/badge/badge.html +++ b/src/lib/badge/badge.html @@ -1,7 +1,7 @@ \ No newline at end of file diff --git a/src/lib/badge/badge.scss b/src/lib/badge/badge.scss index 04e54f646..9d5aaf8dd 100644 --- a/src/lib/badge/badge.scss +++ b/src/lib/badge/badge.scss @@ -1,35 +1,121 @@ -@use '../theme'; -@use './mixins'; +@use './core' as *; +@use '../core/styles/theme'; -@include mixins.core-styles; +// +// Host +// :host { - @include mixins.host; + @include host; } :host([hidden]) { display: none; } -:host([positioned]) { - @include mixins.host-positioned; +// +// Base +// + +.forge-badge { + @include tokens; +} + +.forge-badge { + @include base; +} + +// +// Start/End slots +// + +::slotted(:is([slot=start],[slot=end])) { + font-size: inherit; } +// +// Hide +// + +:host([hide]) { + .forge-badge { + @include hide; + } +} + +// +// Dot +// + :host([dot]) { - top: 0.5rem; + .forge-badge { + @include dot; + } .forge-badge > slot { display: none; } } -:host([static]) { - @include mixins.static; +// +// Themes +// + +// General theme overrides +@each $theme in [primary secondary tertiary success warning] { + :host([theme=#{$theme}]) { + .forge-badge { + @include override(background, theme.variable(#{$theme}-container), value); + @include override(color, theme.variable(on-#{$theme}-container), value); + } + } + + :host([strong][theme=#{$theme}]) { + .forge-badge { + @include override(background, theme.variable($theme), value); + @include override(color, theme.variable(on-#{$theme}), value); + } + } +} + +// Map both "error" and "danger" for backwards compatibility with legacy "danger" theme +:host(:not([strong]):is([theme=error],[theme=danger])) { + .forge-badge { + @include override(background, theme.variable(error-container), value); + @include override(color, theme.variable(error), value); + } +} +:host([strong]:is([theme=error],[theme=danger])) { + .forge-badge { + @include override(background, theme.variable(error), value); + @include override(color, theme.variable(on-error), value); + } } -@include mixins.default-theme(); -@include mixins.theme('danger'); -@include mixins.theme('warning'); -@include mixins.theme('success'); -@include mixins.theme('info-primary'); -@include mixins.theme('info-secondary'); +// Map both "info" and "info-primary" for backwards compatibility with "info" theme +:host(:not([strong]):is([theme=info],[theme=info-primary])) { + .forge-badge { + @include override(background, theme.variable(info-container), value); + @include override(color, theme.variable(on-info-container), value); + } +} +:host([strong]:is([theme=info],[theme=info-primary])) { + .forge-badge { + @include override(background, theme.variable(info), value); + @include override(color, theme.variable(on-info), value); + } +} + +// Support "info-secondary" separately since it is not a core theme +:host(:not([strong])[theme=info-secondary]) { + .forge-badge { + @include override(background, theme.variable(surface-container-low), value); + @include override(color, theme.variable(on-surface-container-low), value); + } +} +:host([strong][theme=info-secondary]) { + .forge-badge { + @include override(background, theme.variable(surface-inverse), value); + @include override(color, theme.variable(on-surface-inverse), value); + } +} diff --git a/src/lib/badge/badge.test.ts b/src/lib/badge/badge.test.ts new file mode 100644 index 000000000..a451e7307 --- /dev/null +++ b/src/lib/badge/badge.test.ts @@ -0,0 +1,85 @@ +import { expect } from '@esm-bundle/chai'; +import { elementUpdated, fixture, html } from '@open-wc/testing'; +import { IBadgeComponent } from './badge'; +import { BadgeTheme, BADGE_CONSTANTS } from './badge-constants'; + +import './badge'; + +describe('Inline Message', () => { + it('should contain shadow root', async () => { + const el = await fixture(html`Test`); + expect(el.shadowRoot).not.to.be.null; + }); + + it('should be accessible', async () => { + const el = await fixture(html`Test`); + await expect(el).to.be.accessible(); + }); + + it('should be accessible in all theme colors', async () => { + const el = await fixture(html`Test`); + + const themes: BadgeTheme[] = ['primary', 'secondary', 'tertiary', 'success', 'error', 'warning', 'info', 'info-secondary']; + for (const theme of themes) { + el.theme = theme; + await elementUpdated(el); + await expect(el).to.be.accessible(); + } + }); + + it('should set theme attribute when theme property is set', async () => { + const el = await fixture(html``); + el.theme = 'error'; + expect(el.getAttribute(BADGE_CONSTANTS.attributes.THEME)).to.equal('error'); + }); + + it('should set theme property when theme attribute is set', async () => { + const el = await fixture(html``); + expect(el.theme).to.equal('error'); + }); + + it('should get default theme when no theme attribute is set', async () => { + const el = await fixture(html``); + expect(el.theme).to.equal(BADGE_CONSTANTS.defaults.THEME); + }); + + it('should set dot attribute when dot property is set', async () => { + const el = await fixture(html``); + el.dot = true; + expect(el.hasAttribute(BADGE_CONSTANTS.attributes.DOT)).to.be.true; + }); + + it('should set dot property when dot attribute is set', async () => { + const el = await fixture(html``); + expect(el.dot).to.be.true; + }); + + it('should set strong attribute when strong property is set', async () => { + const el = await fixture(html``); + el.strong = true; + expect(el.hasAttribute(BADGE_CONSTANTS.attributes.STRONG)).to.be.true; + }); + + it('should set strong property when strong attribute is set', async () => { + const el = await fixture(html``); + expect(el.strong).to.be.true; + }); + + it('should set hide attribute when hide property is set', async () => { + const el = await fixture(html``); + el.hide = true; + expect(el.hasAttribute(BADGE_CONSTANTS.attributes.HIDE)).to.be.true; + }); + + it('should set hide property when hide attribute is set', async () => { + const el = await fixture(html``); + expect(el.hide).to.be.true; + }); + + it('should set transform when hide attribute is set', async () => { + const el = await fixture(html``); + const rootEl = el.shadowRoot?.querySelector(BADGE_CONSTANTS.selectors.ROOT) as HTMLElement; + + expect(getComputedStyle(rootEl).transform).to.equal('matrix(0, 0, 0, 0, 0, 0)'); + }); +}); diff --git a/src/lib/badge/badge.ts b/src/lib/badge/badge.ts index 1534252e2..5e43108d9 100644 --- a/src/lib/badge/badge.ts +++ b/src/lib/badge/badge.ts @@ -1,15 +1,15 @@ -import { attachShadowTemplate, coerceBoolean, CustomElement, FoundationProperty } from '@tylertech/forge-core'; +import { attachShadowTemplate, coerceBoolean, CustomElement } from '@tylertech/forge-core'; import { BaseComponent, IBaseComponent } from '../core/base/base-component'; -import { BadgeAdapter } from './badge-adapter'; -import { BADGE_CONSTANTS } from './badge-constants'; -import { BadgeFoundation } from './badge-foundation'; +import { BadgeTheme, BADGE_CONSTANTS } from './badge-constants'; import template from './badge.html'; import styles from './badge.scss'; export interface IBadgeComponent extends IBaseComponent { dot: boolean; - open: boolean; + theme: BadgeTheme; + strong: boolean; + hide: boolean; } declare global { @@ -19,49 +19,70 @@ declare global { } /** - * The web component class behind the `` custom element. - * * @tag forge-badge + * + * @summary Badges are non-interactive components used to inform status, counts, or as a descriptive label. + * + * @property {boolean} dot - Controls whether the badge will be a small dot without any content visible. + * @property {BadgeTheme} theme - The theme of the badge. + * @property {boolean} strong - Controls whether the badge will have a stronger visual appearance. + * @property {boolean} hide - Controls whether the badge is visible. + * + * @attribute {boolean} dot - When present, the badge will be a small dot without any content visible. + * @attribute {BadgeTheme} theme - The theme of the badge. + * @attribute {boolean} strong - Controls whether the badge will have a stronger visual appearance. + * @attribute {boolean} hide - Controls whether the badge is visible. + * + * @cssproperty --forge-badge-background - The background color. + * @cssproperty --forge-badge-color - The text color. + * @cssproperty --forge-badge-shape - The shape radius. + * @cssproperty --forge-badge-padding-inline - The inline padding. + * @cssproperty --forge-badge-padding-block - The block padding. + * @cssproperty --forge-badge-border-width - The border width. + * @cssproperty --forge-badge-border-color - The border color. + * @cssproperty --forge-badge-border-style - The border style. + * @cssproperty --forge-badge-gap - The spacing between the content within the badge. + * + * @slot - Default content placed inside the badge. + * @slot start - Content placed before the default content. + * @slot end - Content placed after the default content. */ @CustomElement({ name: BADGE_CONSTANTS.elementName }) export class BadgeComponent extends BaseComponent implements IBadgeComponent { - public static get observedAttributes(): string[] { - return [ - BADGE_CONSTANTS.attributes.DOT, - BADGE_CONSTANTS.attributes.OPEN - ]; - } - - private _foundation: BadgeFoundation; - constructor() { super(); attachShadowTemplate(this, template, styles); - this._foundation = new BadgeFoundation(new BadgeAdapter(this)); } - public connectedCallback(): void { - this._foundation.initialize(); + public get dot(): boolean { + return this.hasAttribute(BADGE_CONSTANTS.attributes.DOT); + } + public set dot(value: boolean) { + this.toggleAttribute(BADGE_CONSTANTS.attributes.DOT, value); } - public attributeChangedCallback(name: string, oldValue: string, newValue: string): void { - switch (name) { - case BADGE_CONSTANTS.attributes.DOT: - this.dot = coerceBoolean(newValue); - break; - case BADGE_CONSTANTS.attributes.OPEN: - this.open = coerceBoolean(newValue); - break; - } + public get theme(): BadgeTheme { + return this.getAttribute(BADGE_CONSTANTS.attributes.THEME) as BadgeTheme ?? BADGE_CONSTANTS.defaults.THEME; + } + public set theme(value: BadgeTheme) { + this.setAttribute(BADGE_CONSTANTS.attributes.THEME, value); } - /** Controls whether the component renders a simple dot/circle, or allows for content. */ - @FoundationProperty() - public declare dot: boolean; + public get strong(): boolean { + return this.hasAttribute(BADGE_CONSTANTS.attributes.STRONG); + } + public set strong(value: boolean) { + this.toggleAttribute(BADGE_CONSTANTS.attributes.STRONG, value); + } - /** Controls the visibility state. */ - @FoundationProperty() - public declare open: boolean; + public get hide(): boolean { + return this.hasAttribute(BADGE_CONSTANTS.attributes.HIDE); + } + public set hide(value: boolean) { + if (this.hasAttribute(BADGE_CONSTANTS.attributes.HIDE) !== value) { + this.toggleAttribute(BADGE_CONSTANTS.attributes.HIDE, value); + } + } } diff --git a/src/lib/badge/build.json b/src/lib/badge/build.json deleted file mode 100644 index 07c79a465..000000000 --- a/src/lib/badge/build.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "../../../node_modules/@tylertech/forge-cli/config/build-schema.json", - "extends": "../build.json" -} \ No newline at end of file diff --git a/src/lib/badge/index.scss b/src/lib/badge/index.scss new file mode 100644 index 000000000..98a38c0d9 --- /dev/null +++ b/src/lib/badge/index.scss @@ -0,0 +1 @@ +@forward './core'; diff --git a/src/lib/badge/index.ts b/src/lib/badge/index.ts index bb7498fa0..859998ee8 100644 --- a/src/lib/badge/index.ts +++ b/src/lib/badge/index.ts @@ -2,11 +2,8 @@ import { defineCustomElement } from '@tylertech/forge-core'; import { BadgeComponent } from './badge'; -export * from './badge-adapter'; export * from './badge-constants'; -export * from './badge-foundation'; export * from './badge'; -export * from './badge-component-delegate'; export function defineBadgeComponent(): void { defineCustomElement(BadgeComponent); diff --git a/src/lib/circular-progress/circular-progress-foundation.ts b/src/lib/circular-progress/circular-progress-foundation.ts index 903f46d00..32b35b109 100644 --- a/src/lib/circular-progress/circular-progress-foundation.ts +++ b/src/lib/circular-progress/circular-progress-foundation.ts @@ -47,6 +47,9 @@ export class CircularProgressFoundation implements ICircularProgressFoundation { return this._progress; } public set progress(value: number) { + if (isNaN(value)) { + value = 0; + } if (this._progress !== value) { this._progress = value; if (this._determinate) { diff --git a/src/lib/core/styles/tokens/badge/_tokens.scss b/src/lib/core/styles/tokens/badge/_tokens.scss new file mode 100644 index 000000000..7e38f25f9 --- /dev/null +++ b/src/lib/core/styles/tokens/badge/_tokens.scss @@ -0,0 +1,37 @@ +@use 'sass:map'; +@use '../../theme'; +@use '../../shape'; +@use '../../spacing'; +@use '../../border'; +@use '../../animation'; +@use '../../utils'; + +$tokens: ( + background: utils.module-val(badge, background, theme.variable(secondary)), + color: utils.module-val(badge, color, theme.variable(on-secondary)), + shape: utils.module-val(badge, shape, shape.variable(full)), + height: utils.module-val(badge, height, 20px), + min-width: utils.module-val(badge, min-width, 0), + max-width: utils.module-val(badge, max-width, auto), + padding-inline: utils.module-val(badge, padding-inline, spacing.variable(xsmall)), + padding-block: utils.module-val(badge, padding-block, 0), + border-width: utils.module-val(badge, border-width, border.variable(thin)), + border-style: utils.module-val(badge, border-style, none), + border-color: utils.module-ref(badge, border-color, color), + gap: utils.module-val(badge, gap, spacing.variable(xsmall)), + font-weight: utils.module-val(badge, font-weight, bold), + + // Dot + dot-size: utils.module-val(badge, dot-size, 8px), + dot-height: utils.module-ref(badge, dot-height, dot-size), + dot-width: utils.module-ref(badge, dot-width, dot-size), + dot-padding: utils.module-val(badge, dot-padding, 0), + + // Transition + transition-duration: utils.module-val(badge, transition-duration, animation.variable(duration-short4)), + transition-easing: utils.module-val(badge, transition-easing, animation.variable(easing-decelerate)), +) !default; + +@function get($key) { + @return map.get($tokens, $key); +} diff --git a/src/stories/src/components/badge/badge-arg-types.ts b/src/stories/src/components/badge/badge-arg-types.ts index e6f6814bd..e4b650630 100644 --- a/src/stories/src/components/badge/badge-arg-types.ts +++ b/src/stories/src/components/badge/badge-arg-types.ts @@ -1,12 +1,11 @@ export interface IBadgeProps { dot: boolean; - open: boolean; + hide: boolean; theme: string; - positioned: boolean; strong: boolean; text: string; - hasLeadingIcon: boolean; - hasTrailingIcon: boolean; + hasStartIcon: boolean; + hasEndIcon: boolean; } export const argTypes = { @@ -17,9 +16,9 @@ export const argTypes = { category: 'Properties' }, }, - open: { + hide: { control: 'boolean', - description: 'Use open to show or hide the badge', + description: 'Use the visibility of the badge', table: { category: 'Properties' }, @@ -42,13 +41,6 @@ export const argTypes = { category: 'Attributes' }, }, - positioned: { - control: 'boolean', - description: 'Use positioned to place the badge relative to an icon such as a notification icon', - table: { - category: 'Attributes' - }, - }, strong: { control: 'boolean', description: 'Use muted badges by default. In cases where more visual emphasis is needed, use strong badges instead. In general, only pages where just a few badges are used should use the strong style.', @@ -63,14 +55,14 @@ export const argTypes = { category: 'Slots' }, }, - hasLeadingIcon: { + hasStartIcon: { control: 'boolean', description: 'Use an icon to visually reinforce a badge\'s meaning.', table: { category: 'Slots' }, }, - hasTrailingIcon: { + hasEndIcon: { control: 'boolean', description: 'Use an icon to visually reinforce a badge\'s meaning.', table: { diff --git a/src/stories/src/components/badge/badge.stories.tsx b/src/stories/src/components/badge/badge.stories.tsx index 859f905ce..a0b91b1dd 100644 --- a/src/stories/src/components/badge/badge.stories.tsx +++ b/src/stories/src/components/badge/badge.stories.tsx @@ -21,75 +21,67 @@ export default { export const Default: Story = ({ text = '', - open = true, + hide = false, strong = false, dot = false, - positioned = false, theme = 'default', - hasLeadingIcon = false, - hasTrailingIcon = false + hasStartIcon = false, + hasEndIcon = false }) => { useEffect(() => { IconRegistry.define([tylIconFace, tylIconStar]); }, []); return ( - - {hasLeadingIcon && } + + {hasStartIcon && } {text} - {hasTrailingIcon && } + {hasEndIcon && } ) }; Default.args = { dot: false, - open: true, + hide: false, theme: 'default', - positioned: false, strong: false, - text: 'Default', - hasLeadingIcon: false, - hasTrailingIcon: false + text: 'Status', + hasStartIcon: false, + hasEndIcon: false } as IBadgeProps; export const WithIconButton: Story = ({ text = '', - open = true, + hide = false, strong = false, dot = false, - positioned = false, theme = 'default', - hasLeadingIcon = false, - hasTrailingIcon = false + hasStartIcon = false, + hasEndIcon = false }) => { useEffect(() => { IconRegistry.define([tylIconFace, tylIconNotifications, tylIconStar]); }, []); - const demoIconButtonStyles = { - color: 'var(--mdc-theme-on-surface, #000000)', - }; - return ( - + - - {hasLeadingIcon && } + + {hasStartIcon && } {text} - {hasTrailingIcon && } + {hasEndIcon && } ) }; WithIconButton.args = { dot: false, - open: true, + hide: false, theme: 'default', - positioned: true, strong: false, text: '3', - hasLeadingIcon: false, - hasTrailingIcon: false + hasStartIcon: false, + hasEndIcon: false } as IBadgeProps; diff --git a/src/test/spec/badge/badge.spec.ts b/src/test/spec/badge/badge.spec.ts deleted file mode 100644 index f8bd45f9e..000000000 --- a/src/test/spec/badge/badge.spec.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { defineBadgeComponent, BADGE_CONSTANTS, IBadgeComponent, BadgeComponent } from '@tylertech/forge/badge'; -import { removeElement, getShadowElement } from '@tylertech/forge-core'; - -interface ITestContext { - context: ITestBadgeContext; -} - -interface ITestBadgeContext { - component: IBadgeComponent; - appendToBody(): void; - appendToFixture(): void; - destroy(): void; -} - -describe('BadgeComponent', function(this: ITestContext) { - const bogusAttribute = 'BOGUS_ATTRIBUTE'; - - beforeAll(function(this: ITestContext) { - defineBadgeComponent(); - }); - - afterEach(function(this: ITestContext) { - this.context.destroy(); - }); - - it('should instantiate component instance', function(this: ITestContext) { - this.context = setupTestContext(); - expect(this.context.component.shadowRoot).not.toBeNull(); - }); - - describe('with default values', function(this: ITestContext) { - it('should instantiate dot to false', function(this: ITestContext) { - this.context = setupTestContext(); - expect(this.context.component.dot).toBeFalse(); - }); - it('should instantiate open to true', function(this: ITestContext) { - this.context = setupTestContext(); - expect(this.context.component.open).toBeTrue(); - }); - }); - - describe('with dot set to true', function(this: ITestContext) { - it('should add the dot class to the root element', function(this: ITestContext) { - this.context = setupTestContext(); - this.context.component.dot = true; - const rootElement = getShadowElement(this.context.component, BADGE_CONSTANTS.selectors.ROOT); - - const thatTheElementContainmentOfTheDotClass = rootElement.classList.contains( - BADGE_CONSTANTS.classes.DOT - ); - - expect(thatTheElementContainmentOfTheDotClass).toBe(true); - }); - - it('should add the dot attriute to the component', function(this: ITestContext) { - this.context = setupTestContext(); - this.context.component.dot = true; - - const thatTheDotAttributeDefinition = this.context.component.getAttribute( - BADGE_CONSTANTS.attributes.DOT - ); - expect(thatTheDotAttributeDefinition).not.toBeNull(); - }); - }); - - describe('with dot set to false', function(this: ITestContext) { - it('should not add the dot class to the root element', function(this: ITestContext) { - this.context = setupTestContext(); - this.context.component.dot = false; - const rootElement = getShadowElement(this.context.component, BADGE_CONSTANTS.selectors.ROOT); - - const theClassContainsTheDotClass = rootElement.classList.contains( - BADGE_CONSTANTS.classes.DOT - ); - - expect(theClassContainsTheDotClass).toBe(false); - }); - - it('should not add the dot attribute to the component', function(this: ITestContext) { - this.context = setupTestContext(); - this.context.component.dot = false; - - const theDotAttributeIsDefined = this.context.component.getAttribute( - BADGE_CONSTANTS.attributes.DOT - ); - expect(theDotAttributeIsDefined).toBeNull(); - }); - }); - - describe('with open set to true', function(this: ITestContext) { - it('should add the open class to the root element', function(this: ITestContext) { - this.context = setupTestContext(false, false); - this.context.component.open = true; - this.context.appendToFixture(); - const rootElement = getShadowElement(this.context.component, BADGE_CONSTANTS.selectors.ROOT); - - const theClassContainsTheOpenClass = rootElement.classList.contains( - BADGE_CONSTANTS.classes.OPEN - ); - - expect(theClassContainsTheOpenClass).toBe(true); - }); - - it('should add the open attribute to the component', function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.open = true; - - const theOpenAttributeIsDefined = this.context.component.getAttribute( - BADGE_CONSTANTS.attributes.OPEN - ); - - expect(theOpenAttributeIsDefined).not.toBeNull(); - }); - }); - - describe('with open set to false', function(this: ITestContext) { - it('should not add the open class to the root element', function(this: ITestContext) { - this.context = setupTestContext(false, false); - this.context.component.open = false; - this.context.appendToFixture(); - const rootElement = getShadowElement(this.context.component, BADGE_CONSTANTS.selectors.ROOT); - - const theClassContainsTheOpenClass = rootElement.classList.contains( - BADGE_CONSTANTS.classes.OPEN - ); - - expect(theClassContainsTheOpenClass).toBe(false); - }); - - it('should not add the open attribute to the component', function(this: ITestContext) { - this.context = setupTestContext(); - this.context.component.open = false; - this.context.appendToFixture(); - - const theOpenAttributeIsDefined = this.context.component.getAttribute( - BADGE_CONSTANTS.attributes.OPEN - ); - - expect(theOpenAttributeIsDefined).toBeNull(); - }); - }); - - it('should observe the dot attribute', function(this: ITestContext) { - this.context = setupTestContext(); - const componentDefinedAttributes = BadgeComponent.observedAttributes; - const dotAttributeName = componentDefinedAttributes.find( - (a) => a === BADGE_CONSTANTS.attributes.DOT - ); - - expect(dotAttributeName).toEqual(BADGE_CONSTANTS.attributes.DOT); - }); - - it('should observe the open attribute', function(this: ITestContext) { - this.context = setupTestContext(); - const componentDefinedAttributes = BadgeComponent.observedAttributes; - - const openAttributeName = componentDefinedAttributes.find( - (a) => a === BADGE_CONSTANTS.attributes.OPEN - ); - - expect(openAttributeName).toEqual(BADGE_CONSTANTS.attributes.OPEN); - }); - - describe('with non-observed attribute set', function(this: ITestContext) { - it('should have no change to the open property', function(this: ITestContext) { - this.context = setupTestContext(); - const open = this.context.component.open; - this.context.component.setAttribute(bogusAttribute, bogusAttribute); - - expect(open).toEqual(this.context.component.open); - }); - - it('should have no change to dot property', function(this: ITestContext) { - this.context = setupTestContext(); - const dot = this.context.component.dot; - this.context.component.setAttribute(bogusAttribute, bogusAttribute); - - expect(dot).toEqual(this.context.component.dot); - }); - }); - - function setupTestContext(appendToBody = false, appendToFixture = true): ITestBadgeContext { - const fixture = document.createElement('div'); - fixture.id = 'badge-test-fixture'; - const component = document.createElement(BADGE_CONSTANTS.elementName) as IBadgeComponent; - if (appendToFixture) { - fixture.appendChild(component); - } - if (appendToBody) { - document.body.appendChild(fixture); - } - return { - component, - appendToFixture: () => document.body.appendChild(fixture), - appendToBody: () => fixture.appendChild(component), - destroy: () => removeElement(fixture) - }; - } -});