diff --git a/src/dev/pages/banner/banner.html b/src/dev/pages/banner/banner.html index f1ace2d3a..2fc3b1fe2 100644 --- a/src/dev/pages/banner/banner.html +++ b/src/dev/pages/banner/banner.html @@ -7,20 +7,23 @@ { type: 'select', label: 'Theme', - id: 'theme-select', + defaultValue: 'info', + id: 'opt-theme', options: [ - { label: 'Default (none)', value: '' }, - { label: 'Danger', value: 'danger' }, - { label: 'Warning', value: 'warning' }, + { label: 'Primary', value: 'primary' }, + { label: 'Secondary', value: 'secondary' }, + { label: 'Tertiary', value: 'tertiary' }, { label: 'Success', value: 'success' }, - { label: 'Info (Primary)', value: 'info-primary' }, - { label: 'Info (Secondary)', value: 'info-secondary' }, + { label: 'Error', value: 'error' }, + { label: 'Warning', value: 'warning' }, + { label: 'Info (default)', value: 'info' }, + { label: 'Info Secondary', value: 'info-secondary' }, ] }, - { type: 'switch', label: 'Dismissed', id: 'dismissed-toggle' }, - { type: 'switch', label: 'Show leading icon', id: 'show-leading-icon-toggle', defaultValue: true }, - { type: 'switch', label: 'Show dismiss', id: 'show-dismiss-toggle', defaultValue: true }, - { type: 'switch', label: 'Use more text', id: 'use-more-text-toggle' }, + { type: 'switch', label: 'Dismissed', id: 'opt-dismissed' }, + { type: 'switch', label: 'Persistent', id: 'opt-persistent' }, + { type: 'switch', label: 'Show icon', id: 'opt-show-icon', defaultValue: true }, + { type: 'switch', label: 'Use more text', id: 'opt-more-text' }, ] } }) diff --git a/src/dev/pages/banner/banner.ts b/src/dev/pages/banner/banner.ts index fc91b1bb8..16ae5c2b9 100644 --- a/src/dev/pages/banner/banner.ts +++ b/src/dev/pages/banner/banner.ts @@ -3,7 +3,7 @@ import { IconRegistry } from '@tylertech/forge/icon'; import '@tylertech/forge/banner'; import '@tylertech/forge/button'; import { tylIconAddAlert } from '@tylertech/tyler-icons/standard'; -import type { IBannerComponent, ISelectComponent, ISwitchComponent } from '@tylertech/forge'; +import { BANNER_CONSTANTS, IBannerComponent, ISelectComponent, ISwitchComponent } from '@tylertech/forge'; IconRegistry.define([ tylIconAddAlert @@ -13,31 +13,30 @@ const banner = document.querySelector('#banner') as IBannerComponent; const leadingIcon = document.querySelector('#leading-icon') as HTMLElement; const textEl = document.querySelector('#text') as HTMLElement; -const themeSelect = document.querySelector('#theme-select') as ISelectComponent; -themeSelect.addEventListener('change', ({ detail: theme }) => { - if (theme) { - banner.setAttribute('theme', theme); - } else { - banner.removeAttribute('theme'); - } +banner.addEventListener(BANNER_CONSTANTS.events.DISMISSED, evt => { + console.log(evt); + dismissedToggle.on = true; }); -const dismissToggle = document.querySelector('#dismissed-toggle') as ISwitchComponent; -dismissToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { - banner.dismissed = selected; -}); +const themeSelect = document.querySelector('#opt-theme') as ISelectComponent; +themeSelect.addEventListener('change', ({ detail: theme }) => banner.theme = theme); -const showLeadingIconToggle = document.querySelector('#show-leading-icon-toggle') as ISwitchComponent; -showLeadingIconToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { - leadingIcon.style.display = selected ? 'block' : 'none'; -}); +const dismissedToggle = document.querySelector('#opt-dismissed') as ISwitchComponent; +dismissedToggle.addEventListener('forge-switch-change', ({ detail: selected }) => banner.dismissed = selected); -const showDismissToggle = document.querySelector('#show-dismiss-toggle') as ISwitchComponent; -showDismissToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { - banner.canDismiss = selected; +const showIconToggle = document.querySelector('#opt-show-icon') as ISwitchComponent; +showIconToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { + if (selected) { + banner.appendChild(leadingIcon); + } else { + leadingIcon.remove(); + } }); -const useMoreTextToggle = document.querySelector('#use-more-text-toggle') as ISwitchComponent; +const persistentToggle = document.querySelector('#opt-persistent') as ISwitchComponent; +persistentToggle.addEventListener('forge-switch-change', ({ detail: selected }) => banner.persistent = selected); + +const useMoreTextToggle = document.querySelector('#opt-more-text') as ISwitchComponent; useMoreTextToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { textEl.textContent = selected ? 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Reprehenderit explicabo labore soluta ex culpa, consectetur possimus quidem ullam voluptas est facilis quasi enim error doloribus omnis recusandae! Dolore, eaque ipsa!' : diff --git a/src/lib/badge/_mixins.scss b/src/lib/badge/_mixins.scss deleted file mode 100644 index a18d1ca65..000000000 --- a/src/lib/badge/_mixins.scss +++ /dev/null @@ -1,120 +0,0 @@ -@use 'sass:map'; -@use '@material/elevation/elevation-theme' as mdc-elevation-theme; -@use '@material/typography/typography' as mdc-typography; -@use './variables'; -@use '../theme'; - -@mixin provide-theme($theme) { - @include theme.theme-properties(badge, $theme, variables.$theme-values); -} - -@mixin core-styles() { - .forge-badge { - @include base; - - &--elevated { - @include elevated; - } - - &--open { - @include open; - } - - &--dot { - @include dot; - } - } -} - -@mixin host() { - --forge-icon-font-size: 14px; - - display: flex; - box-sizing: border-box; -} - -@mixin host-positioned() { - position: absolute; - top: 0; - left: 100%; - transform: translateX(-1.25rem); -} - -@mixin base() { - @include mdc-typography.typography(caption); - @include theme.css-custom-property(height, --forge-badge-height, variables.$height); - @include theme.css-custom-property(line-height, --forge-badge-line-height, variables.$line-height); - @include theme.css-custom-property(min-width, --forge-badge-min-width, variables.$min-width); - @include theme.css-custom-property(max-width, --forge-badge-max-width, variables.$max-width); - @include theme.css-custom-property(border, --forge-badge-border, none); - - display: inline-flex; - align-items: center; - gap: 4px; - transition: transform 200ms ease-in-out; - transform: scale(0); - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - pointer-events: none; - border-radius: 16px; - padding: 0 8px; - font-weight: 700; -} - -@mixin theme-property($property, $theme, $theme-mod) { - $combined-theme: $theme + '-' + $theme-mod + '-' + $property; - $light-theme-value: map.get(variables.$theme-values, $combined-theme); - $dark-theme-css-custom-property: --forge-badge-theme-#{$theme}-#{$theme-mod}-#{$property}; - @include theme.css-custom-property($property, $dark-theme-css-custom-property, $light-theme-value); -} - -@mixin theme-base($theme, $theme-mod) { - .forge-badge { - @include theme-property('background', $theme, $theme-mod); - @include theme-property('color', $theme, $theme-mod); - } -} - -@mixin default-theme() { - :host(:not([theme])), - :host([theme='']) { - @include theme-base('default', 'muted'); - } - - :host(:not([theme])[strong]), - :host([theme=''][strong]) { - @include theme-base('default', 'strong'); - } - - @include theme('default'); -} - -@mixin theme($theme: 'danger') { - :host([theme=#{$theme}]) { - @include theme-base($theme, 'muted'); - } - - :host([theme=#{$theme}][strong]) { - @include theme-base($theme, 'strong'); - } -} - -@mixin static() { - position: static; -} - -@mixin elevated() { - @include mdc-elevation-theme.elevation(1); -} - -@mixin open() { - transform: scale(1); -} - -@mixin dot() { - height: 0.5rem; - width: 0.5rem; - min-width: auto; - padding: 0; -} diff --git a/src/lib/badge/_variables.scss b/src/lib/badge/_variables.scss deleted file mode 100644 index d2c6e9474..000000000 --- a/src/lib/badge/_variables.scss +++ /dev/null @@ -1,82 +0,0 @@ -@use '../theme/theme-values'; - -$height: 1.25rem !default; -$line-height: 1.25rem !default; -$min-width: 0 !default; -$max-width: auto !default; - -$theme-values: ( - // default theme - default-muted-background: theme-values.$secondary, - default-muted-color: rgba(0, 0, 0, 0.87), - default-strong-background: theme-values.$tertiary, - default-strong-color: theme-values.$on-tertiary, - - // success theme - success-muted-background: #a5d6a7, - success-muted-color: #1b5e20, - success-strong-background: #2e7d32, - success-strong-color: #ffffff, - - // warning theme - warning-muted-background: #ffdba6, - warning-muted-color: #a03a03, - warning-strong-background: #d14900, - warning-strong-color: #ffffff, - - // danger theme - danger-muted-background: #ffcdd2, - danger-muted-color: #a22d0e, - danger-strong-background: #b00020, - danger-strong-color: #ffffff, - - // info-primary theme - info-primary-muted-background: #bbdefb, - info-primary-strong-background: #1a237e, - info-primary-muted-color: #1a237e, - info-primary-strong-color: #ffffff, - - // info-secondary theme - info-secondary-muted-background: #e0e0e0, - info-secondary-muted-color: #000000, - info-secondary-strong-background: #000000, - info-secondary-strong-color: #ffffff -) !default; - -$theme-values-dark: ( - // default theme - default-muted-background: theme-values.$secondary, - default-strong-background: theme-values.$primary, - default-muted-color: #000000, - default-strong-color: rgba(255, 255, 255, 0.87), - - // success theme - success-muted-background: #175a2a, - success-strong-background: #007425, - success-muted-color: #ffffff, - success-strong-color: #ffffff, - - // warning theme - warning-muted-background: #933600, - warning-strong-background: #cc3900, - warning-muted-color: #ffffff, - warning-strong-color: #ffffff, - - // danger theme - danger-muted-background: #810022, - danger-strong-background: #ae0019, - danger-muted-color: #ffffff, - danger-strong-color: #ffffff, - - // info-primary theme - info-primary-muted-background: #bbdefb, - info-primary-muted-color: #1a237e, - info-primary-strong-background: #1a237e, - info-primary-strong-color: #ffffff, - - // info-secondary theme - info-secondary-muted-background: #424242, - info-secondary-strong-background: #5a5a5a, - info-secondary-muted-color: #ffffff, - info-secondary-strong-color: #ffffff -) !default; diff --git a/src/lib/badge/badge.test.ts b/src/lib/badge/badge.test.ts index a451e7307..6d5223704 100644 --- a/src/lib/badge/badge.test.ts +++ b/src/lib/badge/badge.test.ts @@ -5,7 +5,7 @@ import { BadgeTheme, BADGE_CONSTANTS } from './badge-constants'; import './badge'; -describe('Inline Message', () => { +describe('Badge', () => { it('should contain shadow root', async () => { const el = await fixture(html`Test`); expect(el.shadowRoot).not.to.be.null; diff --git a/src/lib/banner/_core.scss b/src/lib/banner/_core.scss new file mode 100644 index 000000000..65812bb5b --- /dev/null +++ b/src/lib/banner/_core.scss @@ -0,0 +1,48 @@ +@use './token-utils' as *; + +@forward './token-utils'; + +@mixin host { + display: block; + container-type: inline-size; +} + +@mixin base { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr; + overflow: hidden; + box-sizing: border-box; + + transition-property: opacity, grid-template-rows, min-height; + transition-duration: #{token(transition-duration)}; + transition-timing-function: #{token(transition-easing)}; + + background: #{token(background)}; + color: #{token(color)}; + + opacity: 1; +} + +@mixin inner { + display: grid; + grid-template-columns: 1fr auto; + place-items: center; + gap: #{token(gap)}; + overflow: hidden; + padding-inline: #{token(padding-inline)}; +} + +@mixin container { + display: flex; + justify-content: center; + align-items: center; + gap: #{token(gap)}; + flex: 1 1 auto; + padding-block: #{token(padding-block)}; +} + +@mixin dismissed { + grid-template-rows: 0fr; + opacity: 0; +} diff --git a/src/lib/banner/_mixins.scss b/src/lib/banner/_mixins.scss deleted file mode 100644 index 89a712c66..000000000 --- a/src/lib/banner/_mixins.scss +++ /dev/null @@ -1,214 +0,0 @@ -@use 'sass:map'; -@use '../theme'; -@use './variables'; - -@mixin provide-theme($theme) { - @include theme.theme-properties(banner, $theme, variables.$theme-values); -} - -@mixin width-above-breakpoint { - @media (min-width: variables.$width-breakpoint) { - @content; - } -} - -@mixin width-below-breakpoint { - @media (max-width: variables.$width-breakpoint - 1) { - @content; - } -} - -@mixin height-transition($new-height) { - transition: variables.$transition; - overflow: hidden; - max-height: $new-height; -} - -@mixin theme-property($property, $theme, $overriding-css-custom-property) { - $light-theme-value: map.get(variables.$theme-values, $theme + '-' + $property); - $dark-theme-css-custom-property: --forge-banner-theme-#{$theme}-#{$property}; - - // map the properties for 'color' back to 'color'. The differentiation between icon and font is required for the theme handling above. - $styleName: $property; - @if $styleName == 'font-color' or $styleName == 'icon-color' { - $styleName: color; - } - - $primary-override: $overriding-css-custom-property; - $secondary-override: $dark-theme-css-custom-property; - $default: $light-theme-value; - - #{$styleName}: $default; - #{$styleName}: var($primary-override, var($secondary-override, $default)); -} - -@mixin theme-base($theme) { - .forge-banner { - @include theme-property('background', $theme, --forge-banner-theme-background); - @include theme-property('font-color', $theme, --forge-banner-theme-on-background); - - .forge-banner__container-wrapper-icon { - @include theme-property('icon-color', $theme, --forge-banner-theme-icon); - } - - .forge-banner__container-dismiss { - @include theme-property('icon-color', $theme, --forge-banner-theme-icon); - } - } -} - -@mixin default-theme() { - :host(:not([theme])), - :host([theme='']), - :host([theme=default]) { - @include theme-base('default'); - } -} - -@mixin theme($theme) { - :host([theme=#{$theme}]) { - @include theme-base($theme); - } -} - -@mixin base { - @include height-transition(variables.$max-height); - - max-height: variables.$max-height; -} - -@mixin container-base { - @include theme.property(border-bottom-color, border-color); - - display: flex; - flex-direction: row; - border-bottom-width: 1px; - border-bottom-style: solid; - min-height: 48px; -} - -@mixin container-wrapper-base { - flex: 1 1 auto; - display: flex; - flex-direction: row; -} - -@mixin container-wrapper-icon-base { - margin-right: 16px; -} - -@mixin container-wrapper-subsection-base { - display: flex; - justify-content: center; -} - -@mixin container-dismiss-base { - &[hidden] { - display: none; - } -} - -@mixin wide-styles { - @include width-above-breakpoint { - .forge-banner { - @include base; - - &.forge-banner--dismissed { - @include height-transition(0); - } - - &__container { - @include container-base; - - align-items: center; - padding: 0 variables.$padding-right 0 variables.$padding-left; - - &-wrapper { - @include container-wrapper-base; - - justify-content: center; - align-items: center; - - &-icon { - @include container-wrapper-icon-base; - } - - &-subsection { - @include container-wrapper-subsection-base; - - flex-direction: row; - align-items: center; - - &-text { - padding: variables.$padding-top 0 variables.$padding-bottom 0; - margin-right: 24px; - } - - &-action { - ::slotted(forge-button) { - margin-right: 16px; - } - } - } - } - - &-dismiss { - @include container-dismiss-base; - } - } - } - } -} - -@mixin narrow-styles { - @include width-below-breakpoint { - .forge-banner { - @include base; - - &.forge-banner--dismissed { - @include height-transition(0); - } - - &__container { - @include container-base; - - padding: variables.$padding-top-narrow - variables.$padding-right-narrow - variables.$padding-bottom-narrow - variables.$padding-left-narrow; - - &-wrapper { - @include container-wrapper-base; - - &-icon { - @include container-wrapper-icon-base; - } - - &-subsection { - @include container-wrapper-subsection-base; - - flex-direction: column; - - &-action { - ::slotted(forge-button) { - margin-top: 12px; - } - } - } - } - - &-dismiss { - @include container-dismiss-base; - - margin-top: -12px; - margin-right: -12px; - } - } - } - } -} - -@mixin core-styles() { - @include wide-styles; - @include narrow-styles; -} diff --git a/src/lib/banner/_token-utils.scss b/src/lib/banner/_token-utils.scss new file mode 100644 index 000000000..f7df54ae0 --- /dev/null +++ b/src/lib/banner/_token-utils.scss @@ -0,0 +1,25 @@ +@use '../core/styles/tokens/banner/tokens'; +@use '../core/styles/tokens/token-utils'; + +$_module: banner; +$_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/banner/_variables.scss b/src/lib/banner/_variables.scss deleted file mode 100644 index 616e128a6..000000000 --- a/src/lib/banner/_variables.scss +++ /dev/null @@ -1,83 +0,0 @@ -@use '@material/theme/color-palette' as mdc-color-palette; -@use '../theme/theme-values'; - -// There must be an explicit max height in order for the animation to function. -// 600px _should_ be more than plenty. -$max-height: 600px !default; - -$padding-top: 14px !default; -$padding-bottom: $padding-top !default; -$padding-right: 24px !default; -$padding-left: $padding-right !default; - -$padding-top-narrow: 16px !default; -$padding-bottom-narrow: $padding-top-narrow !default; -$padding-right-narrow: 12px !default; -$padding-left-narrow: $padding-right-narrow !default; - -$width-breakpoint: 600px !default; -$transition: max-height 300ms ease-in-out !default; - -$theme-values: ( - // default theme - default-background: mdc-color-palette.$indigo-50, - default-font-color: theme-values.$text-primary-color, - default-icon-color: theme-values.$text-secondary-color, - - // success theme - success-background: #a5d6a7, - success-font-color: theme-values.$text-primary-color, - success-icon-color: theme-values.$text-secondary-color, - - // warning theme - warning-background: #ffdba6, - warning-font-color: theme-values.$text-primary-color, - warning-icon-color: theme-values.$text-secondary-color, - - // danger theme - danger-background: #ffcdd2, - danger-font-color: theme-values.$text-primary-color, - danger-icon-color: theme-values.$text-secondary-color, - - // info-primary theme - info-primary-background: #bbdefb, - info-primary-font-color: theme-values.$text-primary-color, - info-primary-icon-color: theme-values.$text-secondary-color, - - // info-secondary theme - info-secondary-background: #e0e0e0, - info-secondary-font-color: theme-values.$text-primary-color, - info-secondary-icon-color: theme-values.$text-secondary-color -) !default; - -$theme-values-dark: ( - // default theme - default-background: #3f51b5, - default-font-color: #ffffff, - default-icon-color: #ffffff, - - // success theme - success-background: #175a2a, - success-font-color: #ffffff, - success-icon-color: #ffffff, - - // warning theme - warning-background: #933600, - warning-font-color: #ffffff, - warning-icon-color: #ffffff, - - // danger theme - danger-background: #810022, - danger-font-color: #ffffff, - danger-icon-color: #ffffff, - - // info-primary theme - info-primary-background: #0a3b71, - info-primary-font-color: #ffffff, - info-primary-icon-color: #ffffff, - - // info-secondary theme - info-secondary-background: #424242, - info-secondary-font-color: #ffffff, - info-secondary-icon-color: #ffffff -) !default; diff --git a/src/lib/banner/banner-adapter.ts b/src/lib/banner/banner-adapter.ts index cd1159b47..4f3d02212 100644 --- a/src/lib/banner/banner-adapter.ts +++ b/src/lib/banner/banner-adapter.ts @@ -1,66 +1,50 @@ import { getShadowElement } from '@tylertech/forge-core'; - import { BaseAdapter, IBaseAdapter } from '../core/base/base-adapter'; -import { IIconButtonComponent } from '../icon-button'; -import { IBannerComponent } from './banner'; +import type { IBannerComponent } from './banner'; import { BANNER_CONSTANTS } from './banner-constants'; export interface IBannerAdapter extends IBaseAdapter { - addRootClass(className: string): void; - removeRootClass(className: string): void; - addDismissButtonAttribute(name: string, value?: string): void; - removeDismissButtonAttribute(name: string): void; - addDismissEventListener(event: string, callback: (event: Event) => void): void; - removeDismissEventListener(event: string, callback: (event: Event) => void): void; + initialize(): void; + setDismissButtonVisibility(visible: boolean): void; + addDismissListener(callback: EventListener): void; + removeDismissListener(callback: EventListener): void; + setDismissed(value: boolean): void; } export class BannerAdapter extends BaseAdapter implements IBannerAdapter { - private _rootElement: HTMLElement; - private _forgeIconButtonDismiss: IIconButtonComponent; - private _buttonDismiss: HTMLButtonElement; + private _rootElement: HTMLElement = this._component; + private _dismissButtonElement: HTMLButtonElement; + private _iconSlotElement: HTMLSlotElement; constructor(component: IBannerComponent) { super(component); - this._rootElement = getShadowElement(component, BANNER_CONSTANTS.selectors.BANNER); - } - - public addRootClass(name: string): void { - this._rootElement.classList.add(name); - } - - public removeRootClass(name: string): void { - this._rootElement.classList.remove(name); + this._rootElement = getShadowElement(component, '.forge-banner'); + this._dismissButtonElement = getShadowElement(component, BANNER_CONSTANTS.selectors.DISMISS_BUTTON) as HTMLButtonElement; + this._iconSlotElement = getShadowElement(component, BANNER_CONSTANTS.selectors.ICON_SLOT) as HTMLSlotElement; } - - public addDismissButtonAttribute(name: string, value = ''): void { - this.forgeIconButtonDismiss.setAttribute(name, value); + + public initialize(): void { + this._iconSlotElement.addEventListener('slotchange', this._onIconSlotChange.bind(this)); + this._onIconSlotChange(); } - public removeDismissButtonAttribute(name: string): void { - this.forgeIconButtonDismiss.removeAttribute(name); + public setDismissButtonVisibility(visible: boolean): void { + this._dismissButtonElement.hidden = !visible; } - public addDismissEventListener(event: string, callback: (event: Event) => void): void { - this.buttonDismiss.addEventListener(event, callback); + public addDismissListener(callback: EventListener): void { + this._dismissButtonElement.addEventListener('click', callback); } - public removeDismissEventListener(event: string, callback: (event: Event) => void): void { - this.buttonDismiss.removeEventListener(event, callback); + public removeDismissListener(callback: EventListener): void { + this._dismissButtonElement.removeEventListener('click', callback); } - public get forgeIconButtonDismiss(): IIconButtonComponent { - if (!this._forgeIconButtonDismiss) { - this._forgeIconButtonDismiss = getShadowElement(this._component, BANNER_CONSTANTS.selectors.FORGE_DISMISS_BUTTON) as IIconButtonComponent; - } - - return this._forgeIconButtonDismiss; + public setDismissed(value: boolean): void { + this._rootElement.inert = value; } - public get buttonDismiss(): HTMLButtonElement { - if (!this._buttonDismiss) { - this._buttonDismiss = getShadowElement(this._component, BANNER_CONSTANTS.selectors.DISMISS_BUTTON) as HTMLButtonElement; - } - - return this._buttonDismiss; + private _onIconSlotChange(): void { + this._rootElement.classList.toggle(BANNER_CONSTANTS.classes.HAS_ICON, this._iconSlotElement.assignedNodes().length > 0); } } diff --git a/src/lib/banner/banner-constants.ts b/src/lib/banner/banner-constants.ts index a8ae765f4..8eaf324dd 100644 --- a/src/lib/banner/banner-constants.ts +++ b/src/lib/banner/banner-constants.ts @@ -1,43 +1,43 @@ -import { COMPONENT_NAME_PREFIX } from '../constants'; +import { COMPONENT_NAME_PREFIX, Theme } from '../constants'; const elementName: keyof HTMLElementTagNameMap = `${COMPONENT_NAME_PREFIX}banner`; -const classes = { - BANNER: 'forge-banner', - DISMISSED: 'forge-banner--dismissed' +const observedAttributes = { + DISMISSED: 'dismissed', + PERSISTENT: 'persistent', + CAN_DISMISS: 'can-dismiss', + THEME: 'theme' }; -const selectors = { - BANNER: '.forge-banner', - FORGE_DISMISS_BUTTON: 'forge-icon-button.forge-banner__container-dismiss', - DISMISS_BUTTON: '.forge-banner__container-dismiss', - ICON: 'i', - FORGE_BUTTON: 'forge-button' +const attributes = { + ...observedAttributes }; -const attributes = { - SLOT: 'slot', - DISMISSED: 'dismissed', - CAN_DISMISS: 'can-dismiss', - HIDDEN: 'hidden' +const classes = { + HAS_ICON: 'has-icon' +}; + +const selectors = { + DISMISS_BUTTON: '[part=dismiss-button]', + ICON_SLOT: 'slot[name=icon]' }; -const slots = { - ICON: 'icon', - TEXT: 'text', - BUTTON: 'button' +const defaults = { + THEME: 'info' as BannerTheme }; const events = { - DISMISSED: `${elementName}-dismissed`, - UNDISMISSED: `${elementName}-undismissed` + DISMISSED: `${elementName}-dismissed` }; export const BANNER_CONSTANTS = { elementName, + observedAttributes, + attributes, classes, selectors, - attributes, - slots, + defaults, events }; + +export type BannerTheme = Theme | 'danger' | 'info-secondary'; diff --git a/src/lib/banner/banner-foundation.ts b/src/lib/banner/banner-foundation.ts index 6999e92f7..7c59e634a 100644 --- a/src/lib/banner/banner-foundation.ts +++ b/src/lib/banner/banner-foundation.ts @@ -1,89 +1,92 @@ import { ICustomElementFoundation } from '@tylertech/forge-core'; - import { IBannerAdapter } from './banner-adapter'; -import { BANNER_CONSTANTS } from './banner-constants'; +import { BannerTheme, BANNER_CONSTANTS } from './banner-constants'; export interface IBannerFoundation extends ICustomElementFoundation { dismissed: boolean; - canDismiss: boolean; + persistent: boolean; + theme: BannerTheme; } export class BannerFoundation implements IBannerFoundation { private _dismissed = false; - private _canDismiss = true; - private _dismissBanner: () => void; + private _persistent = false; + private _theme: BannerTheme = BANNER_CONSTANTS.defaults.THEME; + private _dismissListener: EventListener = this._onDismiss.bind(this); - constructor(private _adapter: IBannerAdapter) { - this._dismissBanner = () => this._dismiss(); - } + constructor(private _adapter: IBannerAdapter) {} - public connect(): void { - this._addDismissEventListener(); - } + public initialize(): void { + this._adapter.initialize(); - public disconnect(): void { - this._removeDismissEventListener(); + if (!this._persistent) { + this._addDismissListener(); + } } - private _dismiss(): void { - this.dismissed = true; - } + private async _onDismiss(): Promise { + const originalDismissed = this._dismissed; + this._dismissed = !this._dismissed; - private _syncDismissedState(): void { - if (this.dismissed) { - this._setDismissedClass(); - this._adapter.emitHostEvent(BANNER_CONSTANTS.events.DISMISSED); - } else { - this._setUndissmissedClass(); - this._adapter.emitHostEvent(BANNER_CONSTANTS.events.UNDISMISSED); + const evt = new CustomEvent(BANNER_CONSTANTS.events.DISMISSED, { bubbles: true, composed: true, cancelable: true }); + this._adapter.dispatchHostEvent(evt); + this._dismissed = originalDismissed; + + if (evt.defaultPrevented) { + return; } + + this.dismissed = !this._dismissed; } - private _setUndissmissedClass(): void { - this._adapter.removeRootClass(BANNER_CONSTANTS.classes.DISMISSED); + private _addDismissListener(): void { + this._adapter.addDismissListener(this._dismissListener); } - private _setDismissedClass(): void { - this._adapter.addRootClass(BANNER_CONSTANTS.classes.DISMISSED); + private _removeDismissEventListener(): void { + this._adapter.removeDismissListener(this._dismissListener); } - private _syncCanDismissState(): void { - if (this.canDismiss) { - this._adapter.removeDismissButtonAttribute(BANNER_CONSTANTS.attributes.HIDDEN); + private _applyPersistent(): void { + this._adapter.setDismissButtonVisibility(!this._persistent); + if (this._persistent) { + this._removeDismissEventListener(); } else { - this._adapter.addDismissButtonAttribute(BANNER_CONSTANTS.attributes.HIDDEN); + this._addDismissListener(); } } - private _addDismissEventListener(): void { - this._adapter.addDismissEventListener('click', this._dismissBanner); + public get dismissed(): boolean { + return this._dismissed; } - - private _removeDismissEventListener(): void { - this._adapter.removeDismissEventListener('click', this._dismissBanner); + public set dismissed(value: boolean) { + value = Boolean(value); + if (this.dismissed !== value) { + this._dismissed = value; + this._adapter.setDismissed(this._dismissed); + this._adapter.toggleHostAttribute(BANNER_CONSTANTS.attributes.DISMISSED, this.dismissed); + } } - public get dismissed(): boolean { - return !!this._dismissed; + public get persistent(): boolean { + return this._persistent; } - public set dismissed(val: boolean) { - if (this.dismissed === !!val) { - return; + public set persistent(value: boolean) { + value = Boolean(value); + if (this._persistent !== value) { + this._persistent = value; + this._applyPersistent(); + this._adapter.toggleHostAttribute(BANNER_CONSTANTS.attributes.PERSISTENT, this._persistent); } - - this._dismissed = !!val; - this._syncDismissedState(); } - public get canDismiss(): boolean { - return !!this._canDismiss; + public get theme(): BannerTheme { + return this._theme; } - public set canDismiss(val: boolean) { - if (this._canDismiss === !!val) { - return; + public set theme(value: BannerTheme) { + if (this._theme !== value) { + this._theme = value ?? BANNER_CONSTANTS.defaults.THEME; + this._adapter.setHostAttribute(BANNER_CONSTANTS.attributes.THEME, this._theme); } - - this._canDismiss = !!val; - this._syncCanDismissState(); } } diff --git a/src/lib/banner/banner.html b/src/lib/banner/banner.html index 855cf78a0..d1e67defb 100644 --- a/src/lib/banner/banner.html +++ b/src/lib/banner/banner.html @@ -1,23 +1,21 @@ \ No newline at end of file diff --git a/src/lib/banner/banner.scss b/src/lib/banner/banner.scss index cbbdfc2ee..3c8076de8 100644 --- a/src/lib/banner/banner.scss +++ b/src/lib/banner/banner.scss @@ -1,18 +1,118 @@ -@use './mixins'; +@use './core' as *; +@use '../button'; +@use '../icon-button'; +@use '../core/styles/theme'; -@include mixins.core-styles; +// +// Host +// :host { - display: block; + @include host; } :host([hidden]) { display: none; } -@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'); +// +// Base +// + +.forge-banner { + @include tokens; +} + +.forge-banner { + @include base; + + .inner { + @include inner; + } + + .container { + @include container; + } + + forge-icon-button { + @include icon-button.provide-theme(( + focus-indicator-color: #{token(color)} + )); + } +} + +// +// Slotted - button +// + +::slotted(forge-button[slot=button]) { + @include button.provide-theme(( + primary-color: #{token(color)} + )); +} + +// +// Dismissed +// + +:host([dismissed]) { + .forge-banner { + @include dismissed; + } +} + +// +// Theme +// + +@each $theme in [primary secondary tertiary success error warning] { + :host([theme=#{$theme}]) { + .forge-banner { + @include override(background, theme.variable(#{$theme}-container), value); + } + } +} + +// Backwards compatibility with the new error theme +:host([theme=danger]) { + .forge-banner { + @include override(background, theme.variable(error-container), value); + } +} + +// Use surface theme for info-secondary +:host([theme=info-secondary]) { + .forge-banner { + @include override(background, theme.variable(surface-container), value); + } +} + +// +// Responsive +// + +@container (max-width: 600px) { + .forge-banner { + .container { + display: grid; + grid-template-rows: [content] 1fr [button] auto; + grid-template-columns: [content] 1fr; + place-items: normal; + } + + &.has-icon { + .container { + grid-template-columns: [icon] auto [content] 1fr; + } + } + + .inner { + place-items: normal; + } + + .button-container { + grid-row: button; + grid-column: content; + } + } +} diff --git a/src/lib/banner/banner.test.ts b/src/lib/banner/banner.test.ts new file mode 100644 index 000000000..1c34398a6 --- /dev/null +++ b/src/lib/banner/banner.test.ts @@ -0,0 +1,190 @@ +import { expect } from '@esm-bundle/chai'; +import { spy } from 'sinon'; +import { elementUpdated, fixture, html } from '@open-wc/testing'; +import { IBannerComponent } from './banner'; +import { BannerTheme, BANNER_CONSTANTS } from './banner-constants'; + +import './banner'; + +describe('Banner', () => { + it('should contain shadow root', async () => { + const el = await fixture(html`Test`); + expect(el.shadowRoot).not.to.be.null; + }); + + it('should add and remove class when icon slot is populated', async () => { + const el = await fixture(html`iconTest`); + const iconEl = el.querySelector('[slot=icon]') as HTMLSpanElement; + const rootEl = el.shadowRoot?.querySelector('.forge-banner'); + + expect(rootEl?.classList.contains(BANNER_CONSTANTS.classes.HAS_ICON)).to.be.true; + + iconEl.remove(); + await elementUpdated(el); + expect(rootEl?.classList.contains(BANNER_CONSTANTS.classes.HAS_ICON)).to.be.false; + + el.appendChild(iconEl); + await elementUpdated(el); + expect(rootEl?.classList.contains(BANNER_CONSTANTS.classes.HAS_ICON)).to.be.true; + }); + + describe('accessibility', () => { + 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: BannerTheme[] = ['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(); + } + }); + }); + + describe('dismissed', () => { + it('should set dismissed attribute when dismissed property is set', async () => { + const el = await fixture(html``); + el.dismissed = true; + + expect(el.hasAttribute(BANNER_CONSTANTS.attributes.DISMISSED)).to.be.true; + expect(el.offsetHeight).to.equal(0); + }); + + it('should set dismissed property when dismissed attribute is set', async () => { + const el = await fixture(html``); + + expect(el.dismissed).to.be.true; + expect(el.offsetHeight).to.equal(0); + }); + + it('should set inert attribute when dismissed', async () => { + const el = await fixture(html``); + const rootEl = el.shadowRoot?.querySelector('.forge-banner') as HTMLElement; + el.dismissed = true; + + expect(rootEl.hasAttribute('inert')).to.be.true; + }); + }); + + describe('dismissible/persistent', () => { + it('should be dismissible (not peristent) by default', async () => { + const el = await fixture(html``); + + const dismissButton = el.shadowRoot?.querySelector(BANNER_CONSTANTS.selectors.DISMISS_BUTTON); + expect(el.persistent).to.be.false; + expect(dismissButton).not.to.be.null; + }); + + it('should hide dismiss button when persistent', async () => { + const el = await fixture(html``); + el.persistent = true; + + const dismissButton = el.shadowRoot?.querySelector(BANNER_CONSTANTS.selectors.DISMISS_BUTTON) as HTMLElement; + expect(dismissButton.hidden).to.be.true; + }); + + it('should dispatch dismissed event when toggling persistent', async () => { + const el = await fixture(html``); + const dismissButton = el.shadowRoot?.querySelector(BANNER_CONSTANTS.selectors.DISMISS_BUTTON) as HTMLButtonElement; + + const dismissSpy = spy(); + el.addEventListener(BANNER_CONSTANTS.events.DISMISSED, dismissSpy); + + el.persistent = true; + dismissButton.click(); + expect(dismissButton.hidden).to.be.true; + expect(dismissSpy.calledOnce).to.be.false; + + el.persistent = false; + dismissButton.click(); + expect(dismissButton.hidden).to.be.false; + expect(dismissSpy.calledOnce).to.be.true; + }); + + it('should dispatch dismissed event when dismiss button is clicked', async () => { + const el = await fixture(html``); + const dismissButton = el.shadowRoot?.querySelector(BANNER_CONSTANTS.selectors.DISMISS_BUTTON) as HTMLButtonElement; + + const dismissSpy = spy(); + el.addEventListener(BANNER_CONSTANTS.events.DISMISSED, dismissSpy); + + dismissButton.click(); + + expect(el.dismissed).to.be.true; + expect(dismissSpy.calledOnce).to.be.true; + }); + + it('should not dismiss when dismissed event is prevented', async () => { + const el = await fixture(html``); + const dismissButton = el.shadowRoot?.querySelector(BANNER_CONSTANTS.selectors.DISMISS_BUTTON) as HTMLButtonElement; + + el.addEventListener(BANNER_CONSTANTS.events.DISMISSED, (evt: CustomEvent) => evt.preventDefault()); + + dismissButton.click(); + + expect(el.dismissed).to.be.false; + }); + + it('should not dispatch dismissed event when persistent', async () => { + const el = await fixture(html``); + const dismissButton = el.shadowRoot?.querySelector(BANNER_CONSTANTS.selectors.DISMISS_BUTTON) as HTMLButtonElement; + + expect(dismissButton.hidden).to.be.true; + + const dismissSpy = spy(); + el.addEventListener(BANNER_CONSTANTS.events.DISMISSED, dismissSpy); + + dismissButton.click(); + + expect(dismissSpy.called).to.be.false; + }); + }); + + describe('theme', () => { + it('should set theme attribute when theme property is set', async () => { + const el = await fixture(html``); + el.theme = 'error'; + expect(el.getAttribute(BANNER_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 use default theme when set to null', async () => { + const el = await fixture(html``); + + expect(el.theme).to.equal('error'); + + el.theme = null as any; + + expect(el.theme).to.equal(BANNER_CONSTANTS.defaults.THEME); + }); + }); + + describe('deprecated', () => { + it('should proxy deprecated can-dismiss attribute to persistent property', async () => { + const el = await fixture(html``); + expect(el.persistent).to.be.true; + + el.setAttribute('can-dismiss', ''); + expect(el.persistent).to.be.false; + }); + + it('should proxy deprecated canDismiss property to persistent property', async () => { + const el = await fixture(html``); + + el.canDismiss = false; + expect(el.persistent).to.be.true; + + el.canDismiss = true; + expect(el.persistent).to.be.false; + }); + }); +}); diff --git a/src/lib/banner/banner.ts b/src/lib/banner/banner.ts index e75ecb17a..fac871677 100644 --- a/src/lib/banner/banner.ts +++ b/src/lib/banner/banner.ts @@ -1,11 +1,11 @@ import { attachShadowTemplate, coerceBoolean, CustomElement, FoundationProperty } from '@tylertech/forge-core'; import { tylIconCancel } from '@tylertech/tyler-icons/standard'; import { BaseComponent, IBaseComponent } from '../core/base/base-component'; -import { IconComponent, IconRegistry } from '../icon'; +import { IconRegistry } from '../icon'; import { IconButtonComponent } from '../icon-button'; import { TooltipComponent } from '../tooltip'; import { BannerAdapter } from './banner-adapter'; -import { BANNER_CONSTANTS } from './banner-constants'; +import { BannerTheme, BANNER_CONSTANTS } from './banner-constants'; import { BannerFoundation } from './banner-foundation'; import template from './banner.html'; @@ -13,6 +13,9 @@ import styles from './banner.scss'; export interface IBannerComponent extends IBaseComponent { dismissed: boolean; + persistent: boolean; + theme: BannerTheme; + /** @deprecated Use `persistent` instead. */ canDismiss: boolean; } @@ -23,29 +26,46 @@ declare global { interface HTMLElementEventMap { 'forge-banner-dismissed': CustomEvent; - 'forge-banner-undismissed': CustomEvent; } } /** - * The custom element class behind the `` element. - * * @tag forge-banner + * + * @summary Banners are used to inform users of important information, such as errors, warnings, or success messages. + * + * @property {boolean} dismissed - Controls the visibility of the banner. + * @property {boolean} persistent - Controls the visibility of the built-in dismiss button. + * @property {BannerTheme} theme - The theme of the banner. + * + * @attribute {boolean} dismissed - Controls the visibility of the banner. + * @attribute {boolean} persistent - Controls the visibility of the built-in dismiss button. + * @attribute {BannerTheme} theme - The theme of the banner. + * + * @event {CustomEvent} forge-banner-dismissed - Dispatched when the banner is dismissed. + * + * @cssproperty --forge-banner-background - The background color of the banner. + * @cssproperty --forge-banner-color - The text color of the banner. + * @cssproperty --forge-banner-gap - The gap between the contents. + * @cssproperty --forge-banner-padding-inline - The inline padding. + * @cssproperty --forge-banner-padding-block - The block padding. + * @cssproperty --forge-banner-transition-duration - The transition duration. + * @cssproperty --forge-banner-transition-easing - The transition easing function. + * + * @slot - The content of the banner. + * @slot icon - The icon to display. + * @slot button - The optional button to display. */ @CustomElement({ name: BANNER_CONSTANTS.elementName, dependencies: [ IconButtonComponent, - IconComponent, TooltipComponent ] }) export class BannerComponent extends BaseComponent implements IBannerComponent { public static get observedAttributes(): string[] { - return [ - BANNER_CONSTANTS.attributes.DISMISSED, - BANNER_CONSTANTS.attributes.CAN_DISMISS - ]; + return Object.values(BANNER_CONSTANTS.observedAttributes); } protected _foundation: BannerFoundation; @@ -58,29 +78,40 @@ export class BannerComponent extends BaseComponent implements IBannerComponent { } public connectedCallback(): void { - this._foundation.connect(); - } - - public disconnectedCallback(): void { - this._foundation.disconnect(); + this._foundation.initialize(); } public attributeChangedCallback(name: string, oldValue: string, newValue: string): void { switch (name) { - case BANNER_CONSTANTS.attributes.DISMISSED: + case BANNER_CONSTANTS.observedAttributes.DISMISSED: this.dismissed = coerceBoolean(newValue); break; - case BANNER_CONSTANTS.attributes.CAN_DISMISS: - this.canDismiss = coerceBoolean(newValue); + case BANNER_CONSTANTS.observedAttributes.PERSISTENT: + this.persistent = coerceBoolean(newValue); + break; + case BANNER_CONSTANTS.observedAttributes.CAN_DISMISS: + this.persistent = coerceBoolean(newValue) === false; + break; + case BANNER_CONSTANTS.observedAttributes.THEME: + this.theme = newValue as BannerTheme; break; } } - /** Controls whether the component is dismissed (hidden) or not. */ @FoundationProperty() public declare dismissed: boolean; - /** Controls the visibility of the dismiss button. */ @FoundationProperty() - public declare canDismiss: boolean; + public declare persistent: boolean; + + @FoundationProperty() + public declare theme: BannerTheme; + + /** @deprecated Use `persistent` instead. */ + public get canDismiss(): boolean { + return this.persistent; + } + public set canDismiss(value: boolean) { + this.persistent = !value; + } } diff --git a/src/lib/banner/build.json b/src/lib/banner/build.json deleted file mode 100644 index 07c79a465..000000000 --- a/src/lib/banner/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/banner/index.scss b/src/lib/banner/index.scss new file mode 100644 index 000000000..98a38c0d9 --- /dev/null +++ b/src/lib/banner/index.scss @@ -0,0 +1 @@ +@forward './core'; diff --git a/src/lib/core/styles/tokens/banner/_tokens.scss b/src/lib/core/styles/tokens/banner/_tokens.scss new file mode 100644 index 000000000..014096994 --- /dev/null +++ b/src/lib/core/styles/tokens/banner/_tokens.scss @@ -0,0 +1,20 @@ +@use 'sass:map'; +@use '../../animation'; +@use '../../spacing'; +@use '../../theme'; +@use '../../border'; +@use '../../utils'; + +$tokens: ( + background: utils.module-val(banner, background, theme.variable(info-container)), + color: utils.module-val(banner, color, theme.variable(text-high)), + gap: utils.module-val(banner, gap, spacing.variable(small)), + padding-inline: utils.module-val(banner, padding-inline, spacing.variable(large)), + padding-block: utils.module-val(banner, padding-block, spacing.variable(small)), + transition-duration: utils.module-val(banner, transition-duration, animation.variable(duration-short4)), + transition-easing: utils.module-val(banner, transition-easing, animation.variable(easing-standard)) +) !default; + +@function get($key) { + @return map.get($tokens, $key); +} diff --git a/src/lib/theme/_theme-dark.scss b/src/lib/theme/_theme-dark.scss index 3fd67c572..598264af6 100644 --- a/src/lib/theme/_theme-dark.scss +++ b/src/lib/theme/_theme-dark.scss @@ -10,10 +10,6 @@ @use '../skeleton/variables' as skeleton-variables; @use '../toast/mixins' as toast-mixins; @use '../toast/variables' as toast-variables; -@use '../badge/mixins' as badge-mixins; -@use '../badge/variables' as badge-variables; -@use '../banner/mixins' as banner-mixins; -@use '../banner/variables' as banner-variables; @use '../table/mixins' as table-mixins; @use '../table/variables' as table-variables; @@ -40,6 +36,4 @@ @include skeleton-mixins.provide-theme(skeleton-variables.$theme-values-dark); @include toast-mixins.provide-theme(toast-variables.$theme-values-dark); @include table-mixins.provide-theme(table-variables.$theme-values-dark); - @include badge-mixins.provide-theme(badge-variables.$theme-values-dark); - @include banner-mixins.provide-theme(banner-variables.$theme-values-dark); } diff --git a/src/stories/src/components/banner/banner-args.ts b/src/stories/src/components/banner/banner-args.ts index 4fecfd714..e8e7c1f3b 100644 --- a/src/stories/src/components/banner/banner-args.ts +++ b/src/stories/src/components/banner/banner-args.ts @@ -1,6 +1,6 @@ export interface IBannerProps { dismissed: boolean; - canDismiss: boolean; + persistent: boolean; theme: string; hasIcon: boolean; hasButton: boolean; @@ -13,7 +13,7 @@ export const argTypes = { category: 'Properties' }, }, - canDismiss: { + persistent: { control: 'boolean', table: { category: 'Properties' @@ -23,16 +23,18 @@ export const argTypes = { control: { type: 'select', labels: { - 'default': 'Default', - 'danger': 'Danger', - 'warning': 'Warning', + 'primary': 'Primary', + 'secondary': 'Secondary', + 'tertiary': 'Tertiary', 'success': 'Success', - 'info-primary': 'Info primary', + 'error': 'Error', + 'warning': 'Warning', + 'info': 'Info (default)', 'info-secondary': 'Info secondary', }, }, description: 'Use theme to change the color of the banner', - options: ['default', 'danger', 'warning', 'success', 'info-primary', 'info-secondary'], + options: ['primary', 'secondary', 'tertiary', 'success', 'error', 'warning', 'info', 'info-secondary'], table: { category: 'Attributes' }, diff --git a/src/stories/src/components/banner/banner.mdx b/src/stories/src/components/banner/banner.mdx index b4c39c0fe..34bc5ec1f 100644 --- a/src/stories/src/components/banner/banner.mdx +++ b/src/stories/src/components/banner/banner.mdx @@ -68,9 +68,9 @@ Sets the banner to either the dismissed state or the undismissed state. - + -Controls the visibility of the dismiss icon-button. +Controls the visibility of the dismiss button. diff --git a/src/stories/src/components/banner/banner.stories.tsx b/src/stories/src/components/banner/banner.stories.tsx index 03a303a93..1eab8acbe 100644 --- a/src/stories/src/components/banner/banner.stories.tsx +++ b/src/stories/src/components/banner/banner.stories.tsx @@ -20,7 +20,7 @@ export default { export const Default: Story = ({ dismissed = false, - canDismiss = true, + persistent = false, theme = 'default', hasIcon = false, hasButton = false @@ -30,7 +30,7 @@ export const Default: Story = ({ }, []); return ( - + {hasIcon && }
Minim sunt eu laborum labore minim.
{hasButton && Learn more...} @@ -39,7 +39,7 @@ export const Default: Story = ({ }; Default.args = { dismissed: false, - canDismiss: true, + persistent: false, theme: 'default', hasIcon: true, hasButton: false, diff --git a/src/test/spec/banner/banner.fixture.html b/src/test/spec/banner/banner.fixture.html deleted file mode 100644 index ad7968f90..000000000 --- a/src/test/spec/banner/banner.fixture.html +++ /dev/null @@ -1,7 +0,0 @@ -
- - add_alert -
Laboris cillum dolore id incididunt consequat nisi nisi. Consequat cillum officia quis quis.
- Learn more... -
-
\ No newline at end of file diff --git a/src/test/spec/banner/banner.spec.ts b/src/test/spec/banner/banner.spec.ts deleted file mode 100644 index bc9d3271c..000000000 --- a/src/test/spec/banner/banner.spec.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { removeElement, getShadowElement } from '@tylertech/forge-core'; -import { defineBannerComponent, IBannerComponent, BANNER_CONSTANTS, BannerComponent, BUTTON_CONSTANTS, IButtonComponent } from '@tylertech/forge'; -import { tick } from '@tylertech/forge-testing'; - -const DEFAULT_TEXT_CONTENT = 'Laboris cillum dolore id incididunt consequat nisi nisi. Consequat cillum officia quis quis.'; - -interface ITestContext { - context: ITestBannerContext; -} - -interface ITestBannerContext { - component: IBannerComponent; - destroy(): void; -} - -describe('BannerComponent', function(this: ITestContext) { - - let component: IBannerComponent; - - beforeAll(function(this: ITestContext) { - defineBannerComponent(); - }); - - beforeEach(function(this: ITestContext) { - this.context = setupTestContext(); - }); - - afterEach(function(this: ITestContext) { - this.context.destroy(); - }); - - describe('Instantiation', function(this: ITestContext) { - it('should be connected', function(this: ITestContext) { - expect(this.context.component.isConnected).toBe(true); - }); - - it('should instantiate component with shadow dom', function(this: ITestContext) { - expect(this.context.component.shadowRoot).toBeDefined(); - }); - - it('should instantiate component as BannerComponent', function(this: ITestContext) { - expect(this.context.component instanceof BannerComponent).toBe(true, 'Component should be an instance of BannerComponent.'); - }); - - it('should initially be in undismissed state by default', function(this: ITestContext) { - expect(isDismissedClassApplied(this.context.component)).toBe(false, 'dismissed class should not be applied'); - expect(this.context.component.dismissed).toBe(false, 'dismissed property should be false'); - }); - - it('should be shown by default', function(this: ITestContext) { - expect(this.context.component.canDismiss === true).toBe(true, 'canDismiss property should be true'); - expect(dismissButtonIsVisible(this.context.component)).toBe(true, 'dismiss button should be visible'); - }); - }); - - describe('API (Properties) - dismissed', function(this: ITestContext) { - it('should set dismissed state when dismissed property is set to true', function(this: ITestContext) { - this.context.component.dismissed = true; - expect(isDismissedClassApplied(this.context.component)).toBe(true, 'dismissed class should be applied'); - expect(this.context.component.dismissed).toBe(true, 'dismissed property should be true'); - }); - - it('should set undismissed state when dismissed property is set to false from true', async function(this: ITestContext) { - this.context.component.dismissed = true; - await tick(); - - this.context.component.dismissed = false; - expect(isDismissedClassApplied(this.context.component)).toBe(false, 'dismissed class should not be applied'); - expect(this.context.component.dismissed).toBe(false, 'dismissed property should be false'); - }); - }); - - describe('API (Attributes) - dismissed', function(this: ITestContext) { - it('should set dismissed state when dismissed attribute is set to true', function(this: ITestContext) { - this.context.component.setAttribute(BANNER_CONSTANTS.attributes.DISMISSED, 'true'); - expect(isDismissedClassApplied(this.context.component)).toBe(true, 'dismissed class should be applied'); - expect(this.context.component.dismissed).toBe(true, 'dismissed property should be true'); - }); - - it('should set undismissed state when dismissed attribute is set to true from false', async function(this: ITestContext) { - this.context.component.setAttribute(BANNER_CONSTANTS.attributes.DISMISSED, 'true'); - await tick(); - - this.context.component.setAttribute(BANNER_CONSTANTS.attributes.DISMISSED, 'false'); - expect(isDismissedClassApplied(this.context.component)).toBe(false, 'dismissed class should not be applied'); - expect(this.context.component.dismissed).toBe(false, 'dismissed property should be false'); - }); - }); - - describe('API (Properties) - canDismiss', function(this: ITestContext) { - it('should not be shown when canDismiss is false', function(this: ITestContext) { - this.context.component.canDismiss = false; - expect(this.context.component.canDismiss === false).toBe(true, 'canDismiss property should be false'); - expect(dismissButtonIsVisible(this.context.component)).toBe(false, 'dismiss button should not be visible'); - }); - - it('should be shown when canDismiss flips from false to true', async function(this: ITestContext) { - this.context.component.canDismiss = false; - await tick(); - this.context.component.canDismiss = true; - expect(this.context.component.canDismiss === true).toBe(true, 'canDismiss property should be true'); - expect(dismissButtonIsVisible(this.context.component)).toBe(true, 'dismiss button should be visible'); - }); - }); - - describe('API (Attributes) - can-dismiss', function(this: ITestContext) { - it('should not be shown when can-dismiss is false', function(this: ITestContext) { - this.context.component.setAttribute(BANNER_CONSTANTS.attributes.CAN_DISMISS, 'false'); - expect(this.context.component.canDismiss === false).toBe(true, 'canDismiss property should be false'); - expect(dismissButtonIsVisible(this.context.component)).toBe(false, 'dismiss button should not be visible'); - }); - - it('should be shown when can-dismiss flips from false to true', async function(this: ITestContext) { - this.context.component.setAttribute(BANNER_CONSTANTS.attributes.CAN_DISMISS, 'false'); - await tick(); - this.context.component.setAttribute(BANNER_CONSTANTS.attributes.CAN_DISMISS, 'true'); - expect(this.context.component.canDismiss === true).toBe(true, 'canDismiss property should be true'); - expect(dismissButtonIsVisible(this.context.component)).toBe(true, 'dismiss button should be visible'); - }); - }); - - describe('Events', function(this: ITestContext) { - it('should emit dismissed event when banner is dismissed', async function(this: ITestContext) { - this.context.component.dismissed = false; - const dismissedSpy = jasmine.createSpy('dismissed event'); - this.context.component.addEventListener(BANNER_CONSTANTS.events.DISMISSED, dismissedSpy) - this.context.component.dismissed = true; - await tick(); - expect(dismissedSpy).toHaveBeenCalled(); - }); - - it('should emit undismissed event when banner is undismissed', async function(this: ITestContext) { - this.context.component.dismissed = true; - const undismissedSpy = jasmine.createSpy('undismissed event'); - this.context.component.addEventListener(BANNER_CONSTANTS.events.UNDISMISSED, undismissedSpy) - this.context.component.dismissed = false; - await tick(); - expect(undismissedSpy).toHaveBeenCalled(); - }); - }); - - describe('Internal dismiss', function(this: ITestContext) { - it('should emit dismiss event when banner is dismissed from internal icon button', function(this: ITestContext) { - this.context.component.dismissed = false; - const dismissBtn = getShadowElement(this.context.component, BANNER_CONSTANTS.selectors.DISMISS_BUTTON); - dismissBtn.click(); - expect(isDismissedClassApplied(this.context.component)).toBe(true, 'dismissed class should be applied'); - expect(this.context.component.dismissed).toBe(true, 'dismissed should be true'); - }); - }); - - function setupTestContext(): ITestBannerContext { - const fixture = document.createElement('div'); - fixture.id = 'banner-test-fixture'; - const component = document.createElement(BANNER_CONSTANTS.elementName) as IBannerComponent; - const icon = document.createElement('i') as HTMLElement; - icon.textContent = 'add_alert'; - icon.classList.add('tyler-icons'); - component.appendChild(icon); - const text = document.createElement('div') as HTMLElement; - text.slot = 'text'; - text.textContent = DEFAULT_TEXT_CONTENT; - component.appendChild(text); - const buttonComponent = document.createElement(BUTTON_CONSTANTS.elementName) as IButtonComponent; - buttonComponent.setAttribute('variant', 'outlined'); - buttonComponent.type = 'button'; - buttonComponent.textContent = 'Learn more...'; - component.appendChild(buttonComponent); - fixture.appendChild(component); - document.body.appendChild(fixture); - return { - component, - destroy: () => removeElement(fixture) - }; - } - - function isDismissedClassApplied(component: IBannerComponent): boolean { - const rootElement = getShadowElement(component, BANNER_CONSTANTS.selectors.BANNER); - return rootElement.classList.contains(BANNER_CONSTANTS.classes.DISMISSED); - - } - - function dismissButtonIsVisible(component: IBannerComponent): boolean { - const dismissBtn = getShadowElement(component, BANNER_CONSTANTS.selectors.FORGE_DISMISS_BUTTON); - return !dismissBtn.hasAttribute('hidden'); - } -});