From a3933e1c800f51ec431a338325a397cb8e2dd138 Mon Sep 17 00:00:00 2001 From: "Nichols, Kieran" Date: Wed, 22 Nov 2023 11:19:43 -0500 Subject: [PATCH 1/5] feat(app-bar): refactor to use tokens and new patterns --- package-lock.json | 33 --- package.json | 1 - src/dev/pages/app-bar/app-bar.ejs | 9 +- src/dev/pages/app-bar/app-bar.html | 20 +- src/dev/pages/app-bar/app-bar.ts | 65 +++-- src/dev/pages/menu/menu.ejs | 1 - src/dev/src/partials/header.ejs | 6 +- src/dev/src/styles/_header.scss | 4 +- src/lib/app-bar/_mixins.scss | 249 ------------------ src/lib/app-bar/_variables.scss | 29 -- src/lib/app-bar/app-bar-adapter.ts | 84 ------ src/lib/app-bar/app-bar-constants.ts | 37 --- src/lib/app-bar/app-bar-foundation.ts | 82 ------ src/lib/app-bar/app-bar.html | 22 -- src/lib/app-bar/app-bar.scss | 17 -- src/lib/app-bar/app-bar.ts | 89 ------- src/lib/app-bar/app-bar/_configuration.scss | 11 + src/lib/app-bar/app-bar/_core.scss | 90 +++++++ src/lib/app-bar/app-bar/_token-utils.scss | 25 ++ src/lib/app-bar/app-bar/app-bar-adapter.ts | 90 +++++++ src/lib/app-bar/app-bar/app-bar-constants.ts | 38 +++ src/lib/app-bar/app-bar/app-bar-foundation.ts | 82 ++++++ src/lib/app-bar/app-bar/app-bar.html | 24 ++ src/lib/app-bar/app-bar/app-bar.scss | 137 ++++++++++ src/lib/app-bar/app-bar/app-bar.test.ts | 155 +++++++++++ src/lib/app-bar/app-bar/app-bar.ts | 119 +++++++++ src/lib/app-bar/app-bar/index.ts | 11 + src/lib/app-bar/build.json | 4 - src/lib/app-bar/index.ts | 3 - src/lib/button/button.html | 2 +- src/lib/checkbox/checkbox.html | 2 +- src/lib/core/styles/elevation/index.scss | 10 + src/lib/core/styles/theme/index.scss | 2 + .../tokens/app-bar/app-bar/_tokens.scss | 30 +++ .../styles/tokens/theme/_tokens.brand.scss | 17 ++ src/lib/core/styles/tokens/theme/_tokens.scss | 3 + src/lib/core/utils/utils.ts | 2 +- .../floating-action-button.html | 2 +- .../floating-action-button.ts | 2 +- src/lib/focus-indicator/focus-indicator.html | 4 +- src/lib/focus-indicator/focus-indicator.scss | 39 +-- .../focus-indicator/focus-indicator.test.ts | 6 +- src/lib/icon-button/_core.scss | 4 - src/lib/icon-button/icon-button.html | 2 +- src/lib/icon-button/icon-button.scss | 6 - src/lib/list/list-item/list-item.html | 2 +- src/lib/list/list-item/list-item.scss | 2 + src/lib/menu/menu-constants.ts | 2 +- src/lib/slider/slider.html | 2 +- src/lib/switch/switch.html | 2 +- src/lib/tabs/tab/tab.html | 2 +- src/lib/theme/_theme-dark.scss | 3 - src/test/spec/app-bar/app-bar.spec.ts | 167 ------------ 53 files changed, 926 insertions(+), 926 deletions(-) delete mode 100644 src/lib/app-bar/_mixins.scss delete mode 100644 src/lib/app-bar/_variables.scss delete mode 100644 src/lib/app-bar/app-bar-adapter.ts delete mode 100644 src/lib/app-bar/app-bar-constants.ts delete mode 100644 src/lib/app-bar/app-bar-foundation.ts delete mode 100644 src/lib/app-bar/app-bar.html delete mode 100644 src/lib/app-bar/app-bar.scss delete mode 100644 src/lib/app-bar/app-bar.ts create mode 100644 src/lib/app-bar/app-bar/_configuration.scss create mode 100644 src/lib/app-bar/app-bar/_core.scss create mode 100644 src/lib/app-bar/app-bar/_token-utils.scss create mode 100644 src/lib/app-bar/app-bar/app-bar-adapter.ts create mode 100644 src/lib/app-bar/app-bar/app-bar-constants.ts create mode 100644 src/lib/app-bar/app-bar/app-bar-foundation.ts create mode 100644 src/lib/app-bar/app-bar/app-bar.html create mode 100644 src/lib/app-bar/app-bar/app-bar.scss create mode 100644 src/lib/app-bar/app-bar/app-bar.test.ts create mode 100644 src/lib/app-bar/app-bar/app-bar.ts create mode 100644 src/lib/app-bar/app-bar/index.ts delete mode 100644 src/lib/app-bar/build.json create mode 100644 src/lib/core/styles/tokens/app-bar/app-bar/_tokens.scss create mode 100644 src/lib/core/styles/tokens/theme/_tokens.brand.scss delete mode 100644 src/test/spec/app-bar/app-bar.spec.ts diff --git a/package-lock.json b/package-lock.json index ee39851f3..4e7d75e7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,6 @@ "@material/select": "^14.0.0", "@material/shape": "^14.0.0", "@material/theme": "^14.0.0", - "@material/top-app-bar": "^14.0.0", "@material/touch-target": "^14.0.0", "@material/typography": "^14.0.0", "@tylertech/forge-core": "^2.3.0", @@ -2286,22 +2285,6 @@ "@material/elevation": "^14.0.0" } }, - "node_modules/@material/top-app-bar": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@material/top-app-bar/-/top-app-bar-14.0.0.tgz", - "integrity": "sha512-uPej5vHgZnlSB1+koiA9FnabXrHh3O/Npl2ifpUgDVwHDSOxKvLp2LNjyCO71co1QLNnNHIU0xXv3B97Gb0rpA==", - "dependencies": { - "@material/animation": "^14.0.0", - "@material/base": "^14.0.0", - "@material/elevation": "^14.0.0", - "@material/ripple": "^14.0.0", - "@material/rtl": "^14.0.0", - "@material/shape": "^14.0.0", - "@material/theme": "^14.0.0", - "@material/typography": "^14.0.0", - "tslib": "^2.1.0" - } - }, "node_modules/@material/touch-target": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-14.0.0.tgz", @@ -22886,22 +22869,6 @@ "@material/elevation": "^14.0.0" } }, - "@material/top-app-bar": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@material/top-app-bar/-/top-app-bar-14.0.0.tgz", - "integrity": "sha512-uPej5vHgZnlSB1+koiA9FnabXrHh3O/Npl2ifpUgDVwHDSOxKvLp2LNjyCO71co1QLNnNHIU0xXv3B97Gb0rpA==", - "requires": { - "@material/animation": "^14.0.0", - "@material/base": "^14.0.0", - "@material/elevation": "^14.0.0", - "@material/ripple": "^14.0.0", - "@material/rtl": "^14.0.0", - "@material/shape": "^14.0.0", - "@material/theme": "^14.0.0", - "@material/typography": "^14.0.0", - "tslib": "^2.1.0" - } - }, "@material/touch-target": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-14.0.0.tgz", diff --git a/package.json b/package.json index 3ee33b48c..aecc66138 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "@material/select": "^14.0.0", "@material/shape": "^14.0.0", "@material/theme": "^14.0.0", - "@material/top-app-bar": "^14.0.0", "@material/touch-target": "^14.0.0", "@material/typography": "^14.0.0", "@tylertech/forge-core": "^2.3.0", diff --git a/src/dev/pages/app-bar/app-bar.ejs b/src/dev/pages/app-bar/app-bar.ejs index 62b0c8895..5ccd1f04e 100644 --- a/src/dev/pages/app-bar/app-bar.ejs +++ b/src/dev/pages/app-bar/app-bar.ejs @@ -1,19 +1,22 @@ - + - + + - + + Sign in + { document.body.appendChild(toast); }); -const useProfileCardBuilderToggle = document.querySelector('#app-bar-profile-card-builder-toggle') as ISwitchComponent; +const themeSelect = document.querySelector('#opt-theme') as ISelectComponent; +themeSelect.addEventListener('change', () => { + appBar.theme = themeSelect.value || ''; +}); + +const elevationSelect = document.querySelector('#opt-elevation') as ISelectComponent; +elevationSelect.addEventListener('change', ({ detail }) => { + appBar.elevation = detail as AppBarElevation; +}); + +const titleInput = document.querySelector('#opt-title-text') as HTMLInputElement; +titleInput.addEventListener('input', () => { + appBar.titleText = titleInput.value; +}); + +const hrefToggle = document.querySelector('#opt-href') as ISwitchComponent; +hrefToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { + appBar.href = selected ? 'javascript: void(0);' : undefined; +}); + +const showTabsToggle = document.querySelector('#opt-show-tabs') as ISwitchComponent; +showTabsToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { + if (selected) { + appBar.appendChild(appBarTabs); + } else { + appBarTabs.remove(); + } +}); + +const useProfileCardBuilderToggle = document.querySelector('#opt-profile-card-builder') as ISwitchComponent; useProfileCardBuilderToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { appBarProfileButton.profileCardBuilder = selected ? profileCardBuilder : undefined; }); @@ -113,35 +142,3 @@ function profileCardBuilder(): HTMLElement { listElement.appendChild(buildListItemElement('My Preferences', 'settings', 'preferences')); return listElement; } - -const appBarRaisedToggle = document.querySelector('#app-bar-raised-toggle') as ISwitchComponent; -appBarRaisedToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { - appBar.raised = selected; - pageAppBar.raised = selected; -}); - -const appBarTitleInput = document.querySelector('#app-bar-title-input') as HTMLInputElement; -appBarTitleInput.addEventListener('input', () => { - appBar.titleText = appBarTitleInput.value; -}); - -const appBarThemeSelect = document.querySelector('#app-bar-theme-select') as ISelectComponent; -appBarThemeSelect.addEventListener('change', () => { - if (appBarThemeSelect.value) { - appBar.setAttribute('theme', appBarThemeSelect.value); - pageAppBar.setAttribute('theme', appBarThemeSelect.value); - } else { - appBar.removeAttribute('theme'); - pageAppBar.removeAttribute('theme'); - } -}); - -const showAppBarTabsToggle = document.querySelector('#app-bar-show-tabs-toggle') as ISwitchComponent; -showAppBarTabsToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { - if (selected) { - appBarTabs.style.removeProperty('display'); - } else { - appBarTabs.style.display = 'none'; - } -}); - diff --git a/src/dev/pages/menu/menu.ejs b/src/dev/pages/menu/menu.ejs index 16022522f..fbdeed9d0 100644 --- a/src/dev/pages/menu/menu.ejs +++ b/src/dev/pages/menu/menu.ejs @@ -1,5 +1,4 @@ - Show menu diff --git a/src/dev/src/partials/header.ejs b/src/dev/src/partials/header.ejs index a415e4da5..f06e1f8a8 100644 --- a/src/dev/src/partials/header.ejs +++ b/src/dev/src/partials/header.ejs @@ -1,8 +1,4 @@ - - -

<%= site.title %><%= title ? ` - ${title}` : null %>

- -
+ diff --git a/src/dev/src/styles/_header.scss b/src/dev/src/styles/_header.scss index dd41b085e..758d8fb3c 100644 --- a/src/dev/src/styles/_header.scss +++ b/src/dev/src/styles/_header.scss @@ -1,6 +1,6 @@ #page-app-bar { - --forge-app-bar-theme-background: var(--forge-theme-secondary); - --forge-app-bar-theme-on-background: var(--forge-theme-on-secondary); + --forge-app-bar-background: var(--forge-theme-secondary); + --forge-app-bar-foreground: var(--forge-theme-on-secondary); a[slot=title] { text-decoration: none; diff --git a/src/lib/app-bar/_mixins.scss b/src/lib/app-bar/_mixins.scss deleted file mode 100644 index 89e59c6b0..000000000 --- a/src/lib/app-bar/_mixins.scss +++ /dev/null @@ -1,249 +0,0 @@ -@use 'sass:map'; -@use 'sass:string'; -@use '@material/typography/typography' as mdc-typography; -@use '@material/elevation/elevation-theme' as mdc-elevation-theme; -@use '@material/theme/theme' as mdc-theme; -@use '@material/theme/theme-color' as mdc-theme-color; -@use '@material/top-app-bar/mixins' as mdc-top-app-bar-mixins; -@use '@material/top-app-bar/variables' as mdc-top-app-bar-variables; -@use '../theme'; -@use './variables'; -@use '../tabs/tab-bar'; -@use '../tabs/tab'; -@use '../icon-button'; - -@mixin provide-theme($theme) { - @include theme.theme-properties(app-bar, $theme, variables.$theme-values); -} - -@mixin base() { - @include theme.css-custom-property(color, --forge-app-bar-theme-on-background, variables.$on-background); - @include theme.css-custom-property(background-color, --forge-app-bar-theme-background, variables.$background); - @include theme.z-index(header); - - background-size: cover; - background-position: center; - display: grid; - grid-template-rows: 1fr auto; - grid-template-columns: minmax(0, 1fr); - position: relative; - box-sizing: border-box; - width: 100%; - transition: box-shadow 200ms linear; -} - -@mixin row() { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - position: relative; - box-sizing: border-box; - width: 100%; - height: variables.$row-height; -} - -@mixin row-two-column() { - grid-template-columns: 1fr auto; -} - -@mixin section() { - display: inline-flex; - flex: 1 1 auto; - align-items: center; - min-width: 0; - padding: 0 4px; - height: variables.$row-height; - box-sizing: border-box; -} - -@mixin section-align-start() { - justify-content: flex-start; - order: -1; - grid-column: 1; -} - -@mixin section-align-center() { - grid-column: 2; - display: flex; - justify-content: center; - align-items: center; -} - -@mixin section-align-end() { - justify-content: flex-end; - order: 1; - grid-column: 3; -} - -@mixin title() { - @include mdc-typography.typography(headline5); - @include theme.css-custom-property(color, --forge-app-bar-theme-on-background, variables.$on-background); - - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - padding-left: 8px; - padding-right: 8px; -} - -@mixin fixed() { - transition: box-shadow 200ms linear; -} - -@mixin raised() { - @include mdc-elevation-theme.elevation(4); -} - -@mixin title-hover() { - background-color: rgba(255, 255, 255, 0.12); - border-radius: 4px; - cursor: pointer; -} - -@mixin title-active() { - background-color: rgba(255, 255, 255, 0.24); -} - -/// Creates styles for the app-bar host element. -@mixin host() { - @include theme.z-index(header); - - position: relative; - display: block; - contain: layout style; -} - -/// Creates the styles for the logo. -@mixin logo() { - @include theme.css-custom-property(color, --forge-app-bar-theme-on-background, variables.$on-background); - - display: flex; - justify-content: center; - align-items: center; - margin: 0 8px; - font-size: 3rem; - overflow: visible; -} - -@mixin bottom() { - padding: 0 4px; - display: flex; - align-items: center; - height: auto !important; - overflow: hidden; -} - -@mixin no-center-align-end() { - grid-column: 2; -} - -@mixin core-styles() { - .forge-app-bar { - @include base; - - &__row { - @include row; - - .forge-app-bar--no-center & { - @include row-two-column; - } - } - - &__section { - @include section; - - &--align-start { - @include section-align-start; - } - - &--align-center { - @include section-align-center; - } - - &--align-end { - @include section-align-end; - } - } - - &--no-center { - .forge-app-bar__section--align-end { - @include no-center-align-end; - } - } - - ::slotted([slot=logo]) { - @include logo; - } - - ::slotted([slot=profile]) { - margin-left: 4px; - } - - ::slotted([slot=title]) { - @include title; - } - - slot[name=user-action] { - display: inline-flex; - margin-right: 24px; - } - - &__title { - @include title; - - &--interactable { - &:hover { - @include title-hover; - } - - &:active { - @include title-active; - } - } - } - - &__bottom { - @include bottom; - - grid-row: 2; - } - - &--fixed { - @include fixed; - } - - &--raised { - @include raised; - } - } -} - -@mixin default-theme() { - ::slotted(*:not(forge-button)) { - --mdc-theme-on-primary: var(--forge-app-bar-theme-on-background, #{variables.$on-background}); - --mdc-theme-on-surface: var(--forge-app-bar-theme-on-background, #{variables.$on-background}); - } - - --mdc-theme-text-secondary-on-background: var(--forge-app-bar-theme-secondary-on-background, #{variables.$secondary-on-background}); - --mdc-theme-text-secondary-on-light: var(--forge-app-bar-theme-secondary-on-background, #{variables.$secondary-on-background}); - --mdc-theme-text-disabled-on-background: var(--forge-app-bar-theme-disabled-on-background, #{variables.$disabled-on-background}); - --mdc-theme-text-disabled-on-light: var(--forge-app-bar-theme-disabled-on-light, #{variables.$disabled-on-light}); - - ::slotted(forge-tab-bar) { - @include tab-bar.provide-theme(( divider-thickness: 0 )); - - @include tab.provide-theme(( - active-color: var(--forge-app-bar-theme-on-background, #{variables.$on-background}), - inactive-color: var(--forge-app-bar-theme-secondary-on-background, #{variables.$secondary-on-background}) - )); - } - - ::slotted(*) { - @include icon-button.provide-theme(( - icon-color: var(--forge-app-bar-theme-on-background, #{variables.$on-background}) - )); - } - - ::slotted(forge-button:is(:not([variant]),[variant=outlined])) { - --forge-theme-primary: var(--forge-app-bar-theme-on-background, rgba(255, 255, 255, 0.87)); - } -} diff --git a/src/lib/app-bar/_variables.scss b/src/lib/app-bar/_variables.scss deleted file mode 100644 index ccad346c4..000000000 --- a/src/lib/app-bar/_variables.scss +++ /dev/null @@ -1,29 +0,0 @@ -@use '@material/theme/color-palette' as mdc-color-palette; - -$background: mdc-color-palette.$indigo-800 !default; -$on-background: #ffffff !default; -$secondary-on-background: rgba(255, 255, 255, 0.54) !default; -$disabled-on-background: rgba(255, 255, 255, 0.38) !default; -$disabled-on-light: rgba(255, 255, 255, 0.12) !default; -$row-height: 56px !default; - -$theme-values: ( - background: $background, - on-background: $on-background, - secondary-on-background: $secondary-on-background, - disabled-on-background: $disabled-on-background, - disabled-on-light: $disabled-on-light -) !default; - -$theme-values-white: ( - background: #ffffff, - on-background: rgba(0, 0, 0, 0.87), - secondary-on-background: rgba(0, 0, 0, 0.54), - disabled-on-background: rgba(0, 0, 0, 0.38), - disabled-on-light: rgba(0, 0, 0, 0.12) -) !default; - -$theme-values-dark: ( - background: #{mdc-color-palette.$grey-900}, - on-background: #ffffff -) !default; diff --git a/src/lib/app-bar/app-bar-adapter.ts b/src/lib/app-bar/app-bar-adapter.ts deleted file mode 100644 index fa316b255..000000000 --- a/src/lib/app-bar/app-bar-adapter.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { getShadowElement, toggleClass } from '@tylertech/forge-core'; -import { BaseAdapter, IBaseAdapter } from '../core/base/base-adapter'; -import { IAppBarComponent } from './app-bar'; -import { APP_BAR_CONSTANTS } from './app-bar-constants'; - -export interface IAppBarAdapter extends IBaseAdapter { - setTitleText(value: string): void; - setDense(value: boolean): void; - setRaised(value: boolean): void; - setFixed(value: boolean): void; - addBottomClass(name: string): void; - removeBottomClass(name: string): void; - addBottomSlotListener(listener: (evt: Event) => void): void; - addCenterSlotListener(listener: (evt: Event) => void): void; - setCenterSlotVisibility(): void; -} - -/** - * Provides facilities for interacting with the internal DOM of `AppBarComponent`. - */ -export class AppBarAdapter extends BaseAdapter implements IAppBarAdapter { - private _rootElement: HTMLElement; - private _titleElement: HTMLElement; - private _bottomElement: HTMLElement; - private _bottomSlotElement: HTMLSlotElement; - private _centerSlotElement: HTMLSlotElement; - private _centerSectionElement: HTMLElement; - - constructor(component: IAppBarComponent) { - super(component); - this._rootElement = getShadowElement(component, APP_BAR_CONSTANTS.selectors.ROOT); - this._titleElement = getShadowElement(component, APP_BAR_CONSTANTS.selectors.TITLE); - this._bottomElement = getShadowElement(component, APP_BAR_CONSTANTS.selectors.BOTTOM); - this._bottomSlotElement = getShadowElement(component, APP_BAR_CONSTANTS.selectors.BOTTOM_SLOT) as HTMLSlotElement; - this._centerSlotElement = getShadowElement(component, APP_BAR_CONSTANTS.selectors.CENTER_SLOT) as HTMLSlotElement; - this._centerSectionElement = getShadowElement(component, APP_BAR_CONSTANTS.selectors.CENTER_SECTION); - } - - /** - * Sets the title text value. - * @param {string} value The title text. - */ - public setTitleText(value: string): void { - this._titleElement.textContent = value; - } - - public setDense(value: boolean): void { - toggleClass(this._rootElement, value, APP_BAR_CONSTANTS.classes.DENSE); - } - - public setRaised(value: boolean): void { - toggleClass(this._rootElement, value, APP_BAR_CONSTANTS.classes.RAISED); - } - - public setFixed(value: boolean): void { - toggleClass(this._rootElement, value, APP_BAR_CONSTANTS.classes.FIXED); - } - - public addBottomClass(name: string): void { - this._bottomElement.classList.add(name); - } - - public removeBottomClass(name: string): void { - this._bottomElement.classList.remove(name); - } - - public addBottomSlotListener(listener: (evt: Event) => void): void { - this._bottomSlotElement.addEventListener('slotchange', listener); - } - - public addCenterSlotListener(listener: (evt: Event) => void): void { - this._centerSlotElement.addEventListener('slotchange', listener); - } - - public setCenterSlotVisibility(): void { - if (this._centerSlotElement.assignedNodes().length) { - this._centerSectionElement.style.removeProperty('display'); - this._rootElement.classList.remove(APP_BAR_CONSTANTS.classes.NO_CENTER); - } else { - this._centerSectionElement.style.display = 'none'; - this._rootElement.classList.add(APP_BAR_CONSTANTS.classes.NO_CENTER); - } - } -} diff --git a/src/lib/app-bar/app-bar-constants.ts b/src/lib/app-bar/app-bar-constants.ts deleted file mode 100644 index 6bddad2fc..000000000 --- a/src/lib/app-bar/app-bar-constants.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { COMPONENT_NAME_PREFIX } from '../constants'; - -export const elementName: keyof HTMLElementTagNameMap = `${COMPONENT_NAME_PREFIX}app-bar`; - -const classes = { - DENSE: 'forge-app-bar--dense', - FIXED: 'forge-app-bar--fixed', - RAISED: 'forge-app-bar--raised', - ROW: 'forge-app-bar__row', - TITLE_INTERACTABLE: 'forge-app-bar__title--interactable', - NO_CENTER: 'forge-app-bar--no-center' -}; - -const selectors = { - ROOT: '.forge-app-bar', - TITLE: '.forge-app-bar__title', - BOTTOM: '.forge-app-bar__bottom', - BOTTOM_SLOT: 'slot[name=bottom]', - CENTER_SLOT: 'slot[name=center]', - CENTER_SECTION: '#center-section', - FIXED: `.${classes.FIXED}`, - RAISED: `.${classes.RAISED}` -}; - -const attributes = { - TITLE_TEXT: 'title-text', - FIXED: 'fixed', - RAISED: 'raised', - THEME: 'theme' -}; - -export const APP_BAR_CONSTANTS = { - elementName, - selectors, - attributes, - classes -}; diff --git a/src/lib/app-bar/app-bar-foundation.ts b/src/lib/app-bar/app-bar-foundation.ts deleted file mode 100644 index f49f1c253..000000000 --- a/src/lib/app-bar/app-bar-foundation.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { ICustomElementFoundation } from '@tylertech/forge-core'; -import { IAppBarAdapter } from './app-bar-adapter'; -import { APP_BAR_CONSTANTS } from './app-bar-constants'; - -export interface IAppBarFoundation extends ICustomElementFoundation { - titleText: string; - fixed: boolean; - raised: boolean; -} - -/** - * Provides facilities and helper methods for creating an app-bar component. - */ -export class AppBarFoundation implements IAppBarFoundation { - private _titleText: string; - private _fixed = false; - private _raised = true; - private _bottomSlotListener: (evt: Event) => void; - private _centerSlotListener: (evt: Event) => void; - - constructor(private _adapter: IAppBarAdapter) { - this._bottomSlotListener = evt => this._onBottomSlotChanged(evt); - this._centerSlotListener = evt => this._onCenterSlotChanged(evt); - } - - public initialize(): void { - this._adapter.setTitleText(this._titleText); - this._adapter.addBottomSlotListener(this._bottomSlotListener); - this._adapter.addCenterSlotListener(this._centerSlotListener); - this._adapter.setCenterSlotVisibility(); - this._adapter.setRaised(this._raised); - this._adapter.setFixed(this._fixed); - } - - private _onBottomSlotChanged(evt: Event): void { - const slotElement = evt.target as HTMLSlotElement; - if (slotElement.assignedNodes().length) { - this._adapter.addBottomClass(APP_BAR_CONSTANTS.classes.ROW); - } else { - this._adapter.removeBottomClass(APP_BAR_CONSTANTS.classes.ROW); - } - } - - private _onCenterSlotChanged(evt: Event): void { - this._adapter.setCenterSlotVisibility(); - } - - /** Gets/sets the title text. */ - public get titleText(): string { - return this._titleText; - } - public set titleText(value: string) { - if (this._titleText !== value) { - this._titleText = value; - this._adapter.setTitleText(this._titleText); - this._adapter.setHostAttribute(APP_BAR_CONSTANTS.attributes.TITLE_TEXT, value); - } - } - - /** Gets/sets the fixed state. */ - public get fixed(): boolean { - return this._fixed; - } - public set fixed(value: boolean) { - if (this._fixed !== value) { - this._fixed = value; - this._adapter.setFixed(this._fixed); - this._adapter.setHostAttribute(APP_BAR_CONSTANTS.attributes.FIXED, this._fixed.toString()); - } - } - - public get raised(): boolean { - return this._raised; - } - public set raised(value: boolean) { - if (this._raised !== value) { - this._raised = value; - this._adapter.setRaised(value); - this._adapter.setHostAttribute(APP_BAR_CONSTANTS.attributes.RAISED, this._raised.toString()); - } - } -} diff --git a/src/lib/app-bar/app-bar.html b/src/lib/app-bar/app-bar.html deleted file mode 100644 index bc66bf077..000000000 --- a/src/lib/app-bar/app-bar.html +++ /dev/null @@ -1,22 +0,0 @@ - diff --git a/src/lib/app-bar/app-bar.scss b/src/lib/app-bar/app-bar.scss deleted file mode 100644 index 5e5c0d612..000000000 --- a/src/lib/app-bar/app-bar.scss +++ /dev/null @@ -1,17 +0,0 @@ -@use './variables'; -@use './mixins'; - -@include mixins.core-styles; - -:host { - @include mixins.host; - @include mixins.default-theme; -} - -:host([hidden]) { - display: none; -} - -:host([theme=white]) { - @include mixins.provide-theme(variables.$theme-values-white); -} diff --git a/src/lib/app-bar/app-bar.ts b/src/lib/app-bar/app-bar.ts deleted file mode 100644 index e06e371fe..000000000 --- a/src/lib/app-bar/app-bar.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { CustomElement, attachShadowTemplate, coerceBoolean, FoundationProperty } from '@tylertech/forge-core'; -import { AppBarAdapter } from './app-bar-adapter'; -import { AppBarFoundation } from './app-bar-foundation'; -import { APP_BAR_CONSTANTS } from './app-bar-constants'; -import { BaseComponent, IBaseComponent } from '../core/base/base-component'; - -import template from './app-bar.html'; -import styles from './app-bar.scss'; - -export interface IAppBarComponent extends IBaseComponent { - titleText: string; - fixed: boolean; - raised: boolean; - theme: string | null | undefined; -} - -declare global { - interface HTMLElementTagNameMap { - 'forge-app-bar': IAppBarComponent; - } -} - -/** - * The web component class behind the `` custom element. - * - * @tag forge-app-bar - */ -@CustomElement({ - name: APP_BAR_CONSTANTS.elementName -}) -export class AppBarComponent extends BaseComponent implements IAppBarComponent { - public static get observedAttributes(): string[] { - return [ - APP_BAR_CONSTANTS.attributes.TITLE_TEXT, - APP_BAR_CONSTANTS.attributes.FIXED, - APP_BAR_CONSTANTS.attributes.RAISED - ]; - } - - private _foundation: AppBarFoundation; - - constructor() { - super(); - attachShadowTemplate(this, template, styles); - this._foundation = new AppBarFoundation(new AppBarAdapter(this)); - } - - public initializedCallback(): void { - this._foundation.initialize(); - } - - public attributeChangedCallback(name: string, oldValue: string, newValue: string): void { - switch (name) { - case APP_BAR_CONSTANTS.attributes.TITLE_TEXT: - this.titleText = newValue; - break; - case APP_BAR_CONSTANTS.attributes.FIXED: - this.fixed = coerceBoolean(newValue); - break; - case APP_BAR_CONSTANTS.attributes.RAISED: - this.raised = coerceBoolean(newValue); - break; - } - } - - /** Gets/sets the title text. */ - @FoundationProperty() - public declare titleText: string; - - /** Gets/sets the fixed variant. */ - @FoundationProperty() - public declare fixed: boolean; - - /** Gets/sets the raised state. */ - @FoundationProperty() - public declare raised: boolean; - - /** Convenience property to allow for easily getting/setting the theme color from JavaScript. */ - public get theme(): string | null | undefined { - return this.getAttribute(APP_BAR_CONSTANTS.attributes.THEME) || null; - } - public set theme(value: string | null | undefined) { - if (value) { - this.setAttribute(APP_BAR_CONSTANTS.attributes.THEME, value); - } else { - this.removeAttribute(APP_BAR_CONSTANTS.attributes.THEME); - } - } -} diff --git a/src/lib/app-bar/app-bar/_configuration.scss b/src/lib/app-bar/app-bar/_configuration.scss new file mode 100644 index 000000000..7d5072803 --- /dev/null +++ b/src/lib/app-bar/app-bar/_configuration.scss @@ -0,0 +1,11 @@ +@use './token-utils' as *; + +$host-tokens: [background foreground]; + +@mixin host-configuration { + @include tokens($includes: $host-tokens); +} + +@mixin configuration { + @include tokens($excludes: $host-tokens); +} diff --git a/src/lib/app-bar/app-bar/_core.scss b/src/lib/app-bar/app-bar/_core.scss new file mode 100644 index 000000000..c1dd2ddd7 --- /dev/null +++ b/src/lib/app-bar/app-bar/_core.scss @@ -0,0 +1,90 @@ +@use '../../core/styles/typography'; +@use './token-utils' as *; + +@mixin host { + display: block; +} + +@mixin base { + background: #{token(background)}; + color: #{token(foreground)}; + + position: relative; + z-index: #{token(z-index)}; + + display: grid; + grid-template-rows: 1fr auto; + + transition-property: box-shadow, background-color; + transition-duration: #{token(transition-duration)}; + transition-timing-function: #{token(transition-timing)}; + + box-sizing: border-box; + width: 100%; +} + +@mixin row { + display: grid; + align-items: center; + grid-template-columns: 1fr 1fr 1fr; + + height: #{token(height)}; + padding-inline: #{token(row-padding)}; +} + +@mixin row-no-center { + grid-template-columns: 1fr auto; +} + +@mixin section { + display: inline-flex; + flex: 1 1 auto; + align-items: center; + + box-sizing: border-box; + min-width: 0; + height: 100%; +} + +@mixin section-end { + justify-self: end; +} + +@mixin title { + @include typography.style(heading4); + + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +@mixin anchor-base { + outline: none; + text-decoration: none; + color: inherit; + + box-sizing: border-box; + height: 100%; +} + +@mixin logo-title-container { + position: relative; + + display: flex; + align-items: center; + gap: #{token(logo-gap)}; + + min-width: 0; + padding-inline: #{token(title-padding)}; +} + +@mixin logo { + font-size: #{token(logo-font-size)}; + height: 1em; + width: 1em; +} + +@mixin elevation-raised { + box-shadow: #{token(elevation)}; +} diff --git a/src/lib/app-bar/app-bar/_token-utils.scss b/src/lib/app-bar/app-bar/_token-utils.scss new file mode 100644 index 000000000..eac333c63 --- /dev/null +++ b/src/lib/app-bar/app-bar/_token-utils.scss @@ -0,0 +1,25 @@ +@use '../../core/styles/tokens/app-bar/app-bar/tokens'; +@use '../../core/styles/tokens/token-utils'; + +$_module: app-bar; +$_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/app-bar/app-bar/app-bar-adapter.ts b/src/lib/app-bar/app-bar/app-bar-adapter.ts new file mode 100644 index 000000000..ad3c1dc36 --- /dev/null +++ b/src/lib/app-bar/app-bar/app-bar-adapter.ts @@ -0,0 +1,90 @@ +import { getShadowElement } from '@tylertech/forge-core'; +import { replaceElement } from '../../core/utils/utils'; +import { BaseAdapter, IBaseAdapter } from '../../core/base/base-adapter'; +import { IAppBarComponent } from './app-bar'; +import { APP_BAR_CONSTANTS } from './app-bar-constants'; +import { STATE_LAYER_CONSTANTS } from '../../state-layer'; +import { FOCUS_INDICATOR_CONSTANTS } from '../../focus-indicator'; + +export interface IAppBarAdapter extends IBaseAdapter { + initialize(): void; + setTitleText(value: string): void; + setHref(value: string): void; + setTarget(value: string): void; + addCenterSlotListener(listener: (evt: Event) => void): void; + setCenterSlotVisibility(): void; +} + +export class AppBarAdapter extends BaseAdapter implements IAppBarAdapter { + private _rootElement: HTMLElement; + private _titleElement: HTMLElement; + private _logoTitleContainerElement: HTMLElement | HTMLAnchorElement; + private _centerSectionElement: HTMLElement; + private _centerSlotElement: HTMLSlotElement; + + constructor(component: IAppBarComponent) { + super(component); + this._rootElement = getShadowElement(component, APP_BAR_CONSTANTS.selectors.ROOT); + this._titleElement = getShadowElement(component, APP_BAR_CONSTANTS.selectors.TITLE); + this._logoTitleContainerElement = getShadowElement(component, APP_BAR_CONSTANTS.selectors.LOGO_TITLE_CONTAINER); + this._centerSectionElement = getShadowElement(component, APP_BAR_CONSTANTS.selectors.CENTER_SECTION); + this._centerSlotElement = getShadowElement(component, APP_BAR_CONSTANTS.selectors.CENTER_SLOT) as HTMLSlotElement; + } + + public initialize(): void { + if (!this._component.hasAttribute('role')) { + this._component.setAttribute('role', 'banner'); + } + } + + public setTitleText(value: string): void { + this._titleElement.textContent = value; + } + + public setHref(value: string): void { + const isAnchor = this._logoTitleContainerElement.tagName === 'A'; + if (value) { + if (!isAnchor) { + this._createAnchorElement(); + } + (this._logoTitleContainerElement as HTMLAnchorElement).href = value; + } else if (isAnchor) { + this._logoTitleContainerElement = replaceElement(this._logoTitleContainerElement, document.createElement('div')); + this._logoTitleContainerElement.classList.add(APP_BAR_CONSTANTS.classes.LOGO_TITLE_CONTAINER); + + this._logoTitleContainerElement.querySelector(STATE_LAYER_CONSTANTS.elementName)?.remove(); + this._logoTitleContainerElement.querySelector(FOCUS_INDICATOR_CONSTANTS.elementName)?.remove(); + } + } + + public setTarget(value: string): void { + if (this._logoTitleContainerElement.tagName === 'A') { + (this._logoTitleContainerElement as HTMLAnchorElement).target = value; + } + } + + public addCenterSlotListener(listener: (evt: Event) => void): void { + this._centerSlotElement.addEventListener('slotchange', listener); + } + + public setCenterSlotVisibility(): void { + if (this._centerSlotElement.assignedNodes().length) { + this._centerSectionElement.style.removeProperty('display'); + this._rootElement.classList.remove(APP_BAR_CONSTANTS.classes.NO_CENTER); + } else { + this._centerSectionElement.style.display = 'none'; + this._rootElement.classList.add(APP_BAR_CONSTANTS.classes.NO_CENTER); + } + } + + private _createAnchorElement(): void { + this._logoTitleContainerElement = replaceElement(this._logoTitleContainerElement, document.createElement('a')); + this._logoTitleContainerElement.classList.add(APP_BAR_CONSTANTS.classes.LOGO_TITLE_CONTAINER); + + const stateLayer = document.createElement('forge-state-layer'); + const focusIndicator = document.createElement('forge-focus-indicator'); + focusIndicator.inward = true; + + this._logoTitleContainerElement.append(stateLayer, focusIndicator); + } +} diff --git a/src/lib/app-bar/app-bar/app-bar-constants.ts b/src/lib/app-bar/app-bar/app-bar-constants.ts new file mode 100644 index 000000000..7a62aa262 --- /dev/null +++ b/src/lib/app-bar/app-bar/app-bar-constants.ts @@ -0,0 +1,38 @@ +import { COMPONENT_NAME_PREFIX } from '../../constants'; + +export const elementName: keyof HTMLElementTagNameMap = `${COMPONENT_NAME_PREFIX}app-bar`; + +const observedAttributes = { + TITLE_TEXT: 'title-text', + ELEVATION: 'elevation', + THEME: 'theme', + HREF: 'href', + TARGET: 'target' +}; + +const attributes = { + ...observedAttributes +}; + +const classes = { + NO_CENTER: 'no-center', + LOGO_TITLE_CONTAINER: 'logo-title-container' +}; + +const selectors = { + ROOT: '.forge-app-bar', + TITLE: '.title', + LOGO_TITLE_CONTAINER: '.logo-title-container', + CENTER_SLOT: 'slot[name=center]', + CENTER_SECTION: '#center-section' +}; + +export const APP_BAR_CONSTANTS = { + elementName, + selectors, + attributes, + classes +}; + +export type AppBarElevation = 'none' | 'raised'; +export type AppBarTheme = 'white' | ''; diff --git a/src/lib/app-bar/app-bar/app-bar-foundation.ts b/src/lib/app-bar/app-bar/app-bar-foundation.ts new file mode 100644 index 000000000..f5af03710 --- /dev/null +++ b/src/lib/app-bar/app-bar/app-bar-foundation.ts @@ -0,0 +1,82 @@ +import { ICustomElementFoundation } from '@tylertech/forge-core'; +import { IAppBarAdapter } from './app-bar-adapter'; +import { AppBarElevation, AppBarTheme, APP_BAR_CONSTANTS } from './app-bar-constants'; + +export interface IAppBarFoundation extends ICustomElementFoundation { + titleText: string; + elevation: AppBarElevation; + theme: string; + href: string; + target: string; +} + +export class AppBarFoundation implements IAppBarFoundation { + private _titleText = ''; + private _elevation: AppBarElevation = 'raised'; + private _theme: AppBarTheme; + private _href = ''; + private _target = ''; + + private _centerSlotListener = (_evt: Event): void => this._adapter.setCenterSlotVisibility(); + + constructor(private _adapter: IAppBarAdapter) {} + + public initialize(): void { + this._adapter.initialize(); + this._adapter.addCenterSlotListener(this._centerSlotListener); + this._adapter.setCenterSlotVisibility(); + } + + public get titleText(): string { + return this._titleText; + } + public set titleText(value: string) { + if (this._titleText !== value) { + this._titleText = value ?? ''; + this._adapter.setTitleText(this._titleText); + this._adapter.setHostAttribute(APP_BAR_CONSTANTS.attributes.TITLE_TEXT, value); + } + } + + public get elevation(): AppBarElevation { + return this._elevation; + } + public set elevation(value: AppBarElevation) { + if (this._elevation !== value) { + this._elevation = value; + this._adapter.setHostAttribute(APP_BAR_CONSTANTS.attributes.ELEVATION, value); + } + } + + public get theme(): AppBarTheme { + return this._theme; + } + public set theme(value: AppBarTheme) { + if (this._theme !== value) { + this._theme = value ?? ''; + this._adapter.toggleHostAttribute(APP_BAR_CONSTANTS.attributes.THEME, !!this._theme, this._theme); + } + } + + public get href(): string { + return this._href ?? ''; + } + public set href(value: string) { + if (this._href !== value) { + this._href = value?.trim().length ? value.trim() : ''; + this._adapter.setHref(this._href); + this._adapter.toggleHostAttribute(APP_BAR_CONSTANTS.attributes.HREF, !!this._href, this._href); + } + } + + public get target(): string { + return this._target ?? ''; + } + public set target(value: string) { + if (this._target !== value) { + this._target = value?.trim().length ? value.trim() : ''; + this._adapter.setTarget(this._target); + this._adapter.toggleHostAttribute(APP_BAR_CONSTANTS.attributes.TARGET, !!this._target, this._target); + } + } +} diff --git a/src/lib/app-bar/app-bar/app-bar.html b/src/lib/app-bar/app-bar/app-bar.html new file mode 100644 index 000000000..e342a228a --- /dev/null +++ b/src/lib/app-bar/app-bar/app-bar.html @@ -0,0 +1,24 @@ + diff --git a/src/lib/app-bar/app-bar/app-bar.scss b/src/lib/app-bar/app-bar/app-bar.scss new file mode 100644 index 000000000..83d022734 --- /dev/null +++ b/src/lib/app-bar/app-bar/app-bar.scss @@ -0,0 +1,137 @@ +@use 'sass:map'; +@use './configuration' as *; +@use './token-utils' as *; +@use './core'; +@use '../../focus-indicator'; +@use '../../state-layer'; +@use '../../tabs/tab-bar'; +@use '../../tabs/tab'; +@use '../../core/styles/theme'; +@use '../../core/styles/tokens/theme/color-emphasis'; + +// +// Host +// + +:host { + @include host-configuration; +} + +:host { + @include core.host; +} + +:host([hidden]) { + display: none; +} + +// +// Default theme +// + +:host(:not(theme)) { + @include theme.provide(( + primary: #{token(foreground)}, + on-primary: rgba(black, color-emphasis.value(highest)), + text-medium: rgba(white, color-emphasis.value(medium)) + )); +} + +// +// Base +// + +.forge-app-bar { + @include configuration; +} + +.forge-app-bar { + @include core.base; + + .row { + @include core.row; + } + + &.no-center { + .row { + @include core.row-no-center; + } + } + + .section { + @include core.section; + + &:last-child { + @include core.section-end; + } + } + + h1 { + @include core.title; + } + + a { + @include core.anchor-base; + + forge-state-layer { + @include state-layer.provide-theme(( + color: #{token(foreground)} + )); + } + + forge-focus-indicator { + @include focus-indicator.provide-theme(( + color: #{token(foreground)}, + shape: 4px, + offset-block: 4px + )); + } + } + + .logo-title-container { + @include core.logo-title-container; + } +} + +// +// Elevation +// + +:host(:is(:not([elevation]),[elevation=raised])) { + .forge-app-bar { + @include core.elevation-raised; + } +} + +// +// Slotted - Tab bar +// + +::slotted(forge-tab-bar) { + @include tab-bar.provide-theme(( divider-thickness: 0 )); +} + +// +// Slotted - Logo +// + +::slotted([slot=logo]) { + @include core.logo; +} + +// +// White theme +// + +:host([theme=white]) { + @include theme.provide(( + primary: black, + on-primary: rgba(white, color-emphasis.value(highest)), + text-medium: rgba(black, color-emphasis.value(medium)) + )); + + .forge-app-bar { + @include override(background, white, value); + @include override(foreground, black, value); + } +} diff --git a/src/lib/app-bar/app-bar/app-bar.test.ts b/src/lib/app-bar/app-bar/app-bar.test.ts new file mode 100644 index 000000000..47c2255ca --- /dev/null +++ b/src/lib/app-bar/app-bar/app-bar.test.ts @@ -0,0 +1,155 @@ +import { expect } from '@esm-bundle/chai'; +import { elementUpdated, fixture, html } from '@open-wc/testing'; +import { sendMouse } from '@web/test-runner-commands'; +import { APP_BAR_CONSTANTS } from './app-bar-constants'; +import type { IStateLayerComponent } from '../../state-layer'; +import type { IFocusIndicatorComponent } from '../../focus-indicator'; +import type { IAppBarComponent } from './app-bar'; + +import './app-bar'; + +describe('App Bar', () => { + it('should initialize', async () => { + const el = await fixture(html``); + + expect(el.shadowRoot).not.to.be.null; + }); + + it('should be accessible', async () => { + const el = await fixture(html``); + + await expect(el).to.be.accessible(); + }); + + it('should set title', async () => { + const el = await fixture(html``); + + const titleEl = getTitleEl(el); + expect(el.titleText).to.equal('Test'); + expect(titleEl.innerText).to.equal('Test'); + }); + + it('should set title as slot', async () => { + const el = await fixture(html`

Test

`); + + const titleEl = getTitleEl(el); + + expect(el.titleText).to.equal(''); + expect(titleEl.innerText).to.equal(''); + await expect(el).to.be.accessible(); + }); + + it('should set elevation', async () => { + const el = await fixture(html``); + + expect(el.elevation).to.equal('raised'); + expect(el.getAttribute(APP_BAR_CONSTANTS.attributes.ELEVATION)).to.equal('raised'); + + el.elevation = 'none'; + + expect(el.elevation).to.equal('none'); + expect(el.getAttribute(APP_BAR_CONSTANTS.attributes.ELEVATION)).to.equal('none'); + }); + + it('should set theme', async () => { + const el = await fixture(html``); + + expect(el.theme).to.equal('white'); + expect(el.getAttribute(APP_BAR_CONSTANTS.attributes.THEME)).to.equal('white'); + + el.theme = ''; + + expect(el.theme).to.equal(''); + expect(el.hasAttribute(APP_BAR_CONSTANTS.attributes.THEME)).to.be.false; + }); + + it('should set href', async () => { + const el = await fixture(html``); + + let anchorEl = getAnchorEl(el); + expect(el.href).to.equal('javascript: void(0);'); + expect(el.getAttribute(APP_BAR_CONSTANTS.attributes.HREF)).to.equal('javascript: void(0);'); + expect(anchorEl).to.be.ok; + expect(anchorEl.href).to.equal('javascript: void(0);'); + expect(anchorEl.classList.contains(APP_BAR_CONSTANTS.classes.LOGO_TITLE_CONTAINER)).to.be.true; + expect(getStateLayer(el)).to.be.ok; + expect(getFocusIndicator(el)).to.be.ok; + await expect(el).to.be.accessible(); + + el.href = ''; + anchorEl = getAnchorEl(el); + const containerEl = el.shadowRoot?.querySelector(APP_BAR_CONSTANTS.selectors.LOGO_TITLE_CONTAINER) as HTMLElement; + + expect(el.href).to.equal(''); + expect(el.hasAttribute(APP_BAR_CONSTANTS.attributes.HREF)).to.be.false; + expect(containerEl).to.be.ok; + expect(anchorEl).not.to.be.ok; + expect(getStateLayer(el)).not.to.be.ok; + expect(getFocusIndicator(el)).not.to.be.ok; + }); + + it('should set anchor target', async () => { + const el = await fixture(html``); + + let anchorEl = getAnchorEl(el); + expect(el.target).to.equal('_blank'); + expect(el.getAttribute(APP_BAR_CONSTANTS.attributes.TARGET)).to.equal('_blank'); + expect(anchorEl.target).to.equal('_blank'); + + el.target = ''; + anchorEl = getAnchorEl(el); + + expect(el.target).to.equal(''); + expect(el.hasAttribute(APP_BAR_CONSTANTS.attributes.TARGET)).to.be.false; + expect(anchorEl.target).to.equal(''); + }); + + it('should set center section visibility', async () => { + const el = await fixture(html``); + + const centerEl = getCenterEl(el); + expect(centerEl.style.display).to.equal('none'); + expect(getRootEl(el).classList.contains(APP_BAR_CONSTANTS.classes.NO_CENTER)).to.be.true; + + const slottedCenterEl = document.createElement('div'); + slottedCenterEl.slot = 'center'; + el.appendChild(slottedCenterEl); + + await elementUpdated(el); + + expect(centerEl.style.display).to.equal(''); + expect(getRootEl(el).classList.contains(APP_BAR_CONSTANTS.classes.NO_CENTER)).to.be.false; + }); + + function getRootEl(el: IAppBarComponent): HTMLElement { + return el.shadowRoot?.firstElementChild as HTMLElement; + } + + function getTitleEl(el: IAppBarComponent): HTMLHeadingElement { + return el.shadowRoot?.querySelector('h1') as HTMLHeadingElement; + } + + function getAnchorEl(el: IAppBarComponent): HTMLAnchorElement { + return el.shadowRoot?.querySelector('a') as HTMLAnchorElement; + } + + function getCenterEl(el: IAppBarComponent): HTMLElement { + return el.shadowRoot?.querySelector(APP_BAR_CONSTANTS.selectors.CENTER_SECTION) as HTMLElement; + } + + function getStateLayer(el: IAppBarComponent): IStateLayerComponent { + return el.shadowRoot?.querySelector('forge-state-layer') as IStateLayerComponent + } + + function getFocusIndicator(el: IAppBarComponent): IFocusIndicatorComponent { + return el.shadowRoot?.querySelector('forge-focus-indicator') as IFocusIndicatorComponent; + } + + function clickElement(el: HTMLElement): Promise { + const { x, y, width, height } = el.getBoundingClientRect(); + return sendMouse({ type: 'click', position: [ + Math.floor(x + window.scrollX + width / 2), + Math.floor(y + window.scrollY + height / 2), + ]}); + } +}); diff --git a/src/lib/app-bar/app-bar/app-bar.ts b/src/lib/app-bar/app-bar/app-bar.ts new file mode 100644 index 000000000..b6e8e770d --- /dev/null +++ b/src/lib/app-bar/app-bar/app-bar.ts @@ -0,0 +1,119 @@ +import { CustomElement, attachShadowTemplate, FoundationProperty } from '@tylertech/forge-core'; +import { AppBarAdapter } from './app-bar-adapter'; +import { AppBarFoundation } from './app-bar-foundation'; +import { AppBarElevation, AppBarTheme, APP_BAR_CONSTANTS } from './app-bar-constants'; +import { BaseComponent, IBaseComponent } from '../../core/base/base-component'; + +import template from './app-bar.html'; +import styles from './app-bar.scss'; + +export interface IAppBarComponent extends IBaseComponent { + titleText: string; + elevation: AppBarElevation; + theme: AppBarTheme; + href: string; + target: string; +} + +declare global { + interface HTMLElementTagNameMap { + 'forge-app-bar': IAppBarComponent; + } +} + +/** + * @tag forge-app-bar + * + * @description App bars are a collection of components placed as a horizontal bar at the top of the screen. They typically contain a logo, title, and optional application-wide actions. + * + * @property {string} titleText - The text to display in the title. + * @property {AppBarElevation} elevation - The elevation of the app bar. + * @property {AppBarTheme} theme - The theme of the app bar. + * @property {string} href - The href that will be used to make the logo and title area a clickable link. + * + * @attribute {string} title-text - The text to display in the title. + * @attribute {string} elevation - The elevation of the app bar. + * @attribute {string} theme - The theme of the app bar. + * @attribute {string} href - The href that will be used to make the logo and title area a clickable link + * + * @cssproperty --forge-app-bar-background - The background color of the app bar. + * @cssproperty --forge-app-bar-foreground - The text color of the app bar. + * @cssproperty --forge-app-bar-z-index - The `z-index` of the app bar. + * @cssproperty --forge-app-bar-elevation - The elevation of the app bar. + * @cssproperty --forge-app-bar-height - The height of the app bar. + * @cssproperty --forge-app-bar-row-padding - The inline padding of the app bar. + * @cssproperty --forge-app-bar-logo-gap - The space between the logo and title. + * @cssproperty --forge-app-bar-title-padding - The padding around the title element. + * @cssproperty --forge-app-bar-transition-duration - The transition duration for animations. + * @cssproperty --forge-app-bar-transition-timing - The transition timing function for animations. + * + * @csspart root - The root container element. + * @csspart title - The title element. + * + * @slot logo - Reserved for the brand logo. + * @slot title - Reserved for the application title. This will overwrite the `titleText` property/attribute. + * @slot start - Places content at the beginning of the app bar. + * @slot center - Places content in the center of the app bar. + * @slot end - Places content at the end of the app bar. + */ +@CustomElement({ + name: APP_BAR_CONSTANTS.elementName +}) +export class AppBarComponent extends BaseComponent implements IAppBarComponent { + public static get observedAttributes(): string[] { + return [ + APP_BAR_CONSTANTS.attributes.TITLE_TEXT, + APP_BAR_CONSTANTS.attributes.ELEVATION, + APP_BAR_CONSTANTS.attributes.THEME, + APP_BAR_CONSTANTS.attributes.HREF, + APP_BAR_CONSTANTS.attributes.TARGET + ]; + } + + private _foundation: AppBarFoundation; + + constructor() { + super(); + attachShadowTemplate(this, template, styles); + this._foundation = new AppBarFoundation(new AppBarAdapter(this)); + } + + public connectedCallback(): void { + this._foundation.initialize(); + } + + public attributeChangedCallback(name: string, oldValue: string, newValue: string): void { + switch (name) { + case APP_BAR_CONSTANTS.attributes.TITLE_TEXT: + this.titleText = newValue; + break; + case APP_BAR_CONSTANTS.attributes.ELEVATION: + this.elevation = newValue as AppBarElevation; + break; + case APP_BAR_CONSTANTS.attributes.THEME: + this.theme = newValue as AppBarTheme; + break; + case APP_BAR_CONSTANTS.attributes.HREF: + this.href = newValue; + break; + case APP_BAR_CONSTANTS.attributes.TARGET: + this.target = newValue; + break; + } + } + + @FoundationProperty() + public declare titleText: string; + + @FoundationProperty() + public declare elevation: AppBarElevation; + + @FoundationProperty() + public declare theme: AppBarTheme; + + @FoundationProperty() + public declare href: string; + + @FoundationProperty() + public declare target: string; +} diff --git a/src/lib/app-bar/app-bar/index.ts b/src/lib/app-bar/app-bar/index.ts new file mode 100644 index 000000000..66c3e0eae --- /dev/null +++ b/src/lib/app-bar/app-bar/index.ts @@ -0,0 +1,11 @@ +import { defineCustomElement } from '@tylertech/forge-core'; +import { AppBarComponent } from './app-bar'; + +export * from './app-bar-adapter'; +export * from './app-bar-constants'; +export * from './app-bar-foundation'; +export * from './app-bar'; + +export function defineAppBarComponent(): void { + defineCustomElement(AppBarComponent); +} diff --git a/src/lib/app-bar/build.json b/src/lib/app-bar/build.json deleted file mode 100644 index 07c79a465..000000000 --- a/src/lib/app-bar/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/app-bar/index.ts b/src/lib/app-bar/index.ts index 0edbc7003..a444484c0 100644 --- a/src/lib/app-bar/index.ts +++ b/src/lib/app-bar/index.ts @@ -1,9 +1,6 @@ import { defineCustomElement } from '@tylertech/forge-core'; import { AppBarComponent } from './app-bar'; -export * from './app-bar-adapter'; -export * from './app-bar-constants'; -export * from './app-bar-foundation'; export * from './app-bar'; export * from './search'; export * from './menu-button'; diff --git a/src/lib/button/button.html b/src/lib/button/button.html index 82deaecce..a8333f6e6 100644 --- a/src/lib/button/button.html +++ b/src/lib/button/button.html @@ -3,7 +3,7 @@ - + diff --git a/src/lib/checkbox/checkbox.html b/src/lib/checkbox/checkbox.html index 54bcb2f16..d606e264c 100644 --- a/src/lib/checkbox/checkbox.html +++ b/src/lib/checkbox/checkbox.html @@ -11,7 +11,7 @@ - + diff --git a/src/lib/core/styles/elevation/index.scss b/src/lib/core/styles/elevation/index.scss index d4b7a9d7b..ed0d68ff0 100644 --- a/src/lib/core/styles/elevation/index.scss +++ b/src/lib/core/styles/elevation/index.scss @@ -3,6 +3,7 @@ @use 'sass:meta'; @use '../core/config'; @use '../tokens/elevation/tokens'; +@use '../utils'; /// /// Generates a `box-shadow` style value for the provided elevation z-value. @@ -54,3 +55,12 @@ $value: map.get(tokens.$z-index-map, $level); z-index: var(--#{config.$prefix}-z-index-#{$level}, #{$value}); } + +/// +/// Returns a global z-index CSS custom property reference (with the fallback value) specified by the provided level. +/// +/// @param {Number} $level - The z-index level to return. +/// +@function z-index-variable($level) { + @return utils.variable(z-index, tokens.$z-index-map, $level); +} diff --git a/src/lib/core/styles/theme/index.scss b/src/lib/core/styles/theme/index.scss index afad3d3cc..960a64b12 100644 --- a/src/lib/core/styles/theme/index.scss +++ b/src/lib/core/styles/theme/index.scss @@ -5,6 +5,8 @@ @use '../tokens/theme/color-emphasis'; @use '../utils'; +@forward './utils'; + /// /// Emits custom property declarations for all default theme tokens. /// diff --git a/src/lib/core/styles/tokens/app-bar/app-bar/_tokens.scss b/src/lib/core/styles/tokens/app-bar/app-bar/_tokens.scss new file mode 100644 index 000000000..372d16ca2 --- /dev/null +++ b/src/lib/core/styles/tokens/app-bar/app-bar/_tokens.scss @@ -0,0 +1,30 @@ +@use 'sass:map'; +@use '../../../utils'; +@use '../../../theme'; +@use '../../../elevation'; +@use '../../../animation'; +@use '../../../typography'; +@use '../../../spacing'; + +$tokens: ( + background: utils.module-val(app-bar, background, theme.variable(brand)), + foreground: utils.module-val(app-bar, foreground, theme.variable(on-brand)), + + z-index: utils.module-val(app-bar, z-index, elevation.z-index-variable(header)), + elevation: utils.module-val(app-bar, elevation, elevation.value(4)), + + height: utils.module-val(app-bar, height, 56px), + row-padding: utils.module-val(app-bar, row-padding, spacing.variable(xxsmall)), + + logo-gap: utils.module-val(app-bar, logo-gap, spacing.variable(medium)), + logo-font-size: utils.module-val(app-bar, logo-font-size, typography.font-size-relative('2500')), + + title-padding: utils.module-val(app-bar, title-padding, spacing.variable(xsmall)), + + transition-duration: utils.module-val(app-bar, transition-duration, animation.variable(duration-short4)), + transition-timing: utils.module-val(app-bar, transition-timing, animation.variable(easing-standard)), +) !default; + +@function get($key) { + @return map.get($tokens, $key); +} diff --git a/src/lib/core/styles/tokens/theme/_tokens.brand.scss b/src/lib/core/styles/tokens/theme/_tokens.brand.scss new file mode 100644 index 000000000..ea04090de --- /dev/null +++ b/src/lib/core/styles/tokens/theme/_tokens.brand.scss @@ -0,0 +1,17 @@ +@use 'sass:map'; +@use '../../utils'; +@use './token-utils'; +@use './tokens.surface' as surface; +@use '../color-palette'; + +// Light +$tokens: ( + brand: #283593, + on-brand: #ffffff +); + +// Dark +$tokens-dark: ( + brand: #212121, + on-brand: #ffffff +); diff --git a/src/lib/core/styles/tokens/theme/_tokens.scss b/src/lib/core/styles/tokens/theme/_tokens.scss index 0a223a916..ec5191a1c 100644 --- a/src/lib/core/styles/tokens/theme/_tokens.scss +++ b/src/lib/core/styles/tokens/theme/_tokens.scss @@ -1,6 +1,7 @@ @use 'sass:map'; @use '../../utils'; @use '../../theme/utils' as theme-utils; +@use './tokens.brand' as brand; @use './tokens.core' as core; @use './tokens.surface' as surface; @use './tokens.text' as text; @@ -9,6 +10,7 @@ // Forge default (light) theme $tokens: utils.flatten( + brand.$tokens, core.$tokens, surface.$tokens, text.$tokens, @@ -31,6 +33,7 @@ $tokens: utils.flatten( // Forge dark theme $tokens-dark: utils.flatten( + brand.$tokens-dark, core.$tokens-dark, surface.$tokens-dark, text.$tokens-dark, diff --git a/src/lib/core/utils/utils.ts b/src/lib/core/utils/utils.ts index 3776b9aa4..a338d6457 100644 --- a/src/lib/core/utils/utils.ts +++ b/src/lib/core/utils/utils.ts @@ -209,7 +209,7 @@ export function locateTargetHeuristic(element: HTMLElement, id?: string | null): * @param preserveChildren Whether or not to preserve the children of the old element in the new element. * @returns The new element. */ -export function replaceElement(oldElement: HTMLElement, newElement: HTMLElement, preserveChildren = true): HTMLElement { +export function replaceElement(oldElement: HTMLElement, newElement: T, preserveChildren = true): T { if (preserveChildren) { newElement.append(...oldElement.childNodes); } diff --git a/src/lib/floating-action-button/floating-action-button.html b/src/lib/floating-action-button/floating-action-button.html index 7c65c5720..fc3e1f6ee 100644 --- a/src/lib/floating-action-button/floating-action-button.html +++ b/src/lib/floating-action-button/floating-action-button.html @@ -4,7 +4,7 @@ - + diff --git a/src/lib/floating-action-button/floating-action-button.ts b/src/lib/floating-action-button/floating-action-button.ts index 6f71ddb7e..685a14419 100644 --- a/src/lib/floating-action-button/floating-action-button.ts +++ b/src/lib/floating-action-button/floating-action-button.ts @@ -1,4 +1,4 @@ -import { attachShadowTemplate, coerceBoolean, CustomElement, FoundationProperty } from '@tylertech/forge-core'; +import { attachShadowTemplate, CustomElement, FoundationProperty } from '@tylertech/forge-core'; import { ButtonTheme } from '../button'; import { BaseButton, IBaseButton } from '../button/base/base-button'; import { BASE_BUTTON_CONSTANTS } from '../button/base/base-button-constants'; diff --git a/src/lib/focus-indicator/focus-indicator.html b/src/lib/focus-indicator/focus-indicator.html index 6bf94eed7..cc340bc4c 100644 --- a/src/lib/focus-indicator/focus-indicator.html +++ b/src/lib/focus-indicator/focus-indicator.html @@ -1,3 +1 @@ - \ No newline at end of file + diff --git a/src/lib/focus-indicator/focus-indicator.scss b/src/lib/focus-indicator/focus-indicator.scss index 8d5a3786c..1ea25ca98 100644 --- a/src/lib/focus-indicator/focus-indicator.scss +++ b/src/lib/focus-indicator/focus-indicator.scss @@ -1,5 +1,5 @@ @use './animations'; -@use './configuration'; +@use './configuration' as *; @use './core'; // @@ -7,52 +7,35 @@ // :host { - display: contents; + @include configuration; } -:host([hidden]) { - display: none; -} - -// -// Base -// - -.forge-focus-indicator { - @include configuration.configuration; -} - -.forge-focus-indicator { +:host { @include core.base; } +:host([hidden]) { + display: none; +} // // States // :host([active]) { - .forge-focus-indicator { - @include core.active; - } + @include core.active; } :host(:not([inward])) { - .forge-focus-indicator { - @include core.outward; - } + @include core.outward; } :host([inward]) { - .forge-focus-indicator { - @include core.inward; - } + @include core.inward; } :host([circular]) { - .forge-focus-indicator { - @include core.circular; - } + @include core.circular; } // @@ -66,7 +49,7 @@ // @media (prefers-reduced-motion) { - .forge-focus-indicator { + :host { animation: none; } } diff --git a/src/lib/focus-indicator/focus-indicator.test.ts b/src/lib/focus-indicator/focus-indicator.test.ts index 16de4db80..d65052262 100644 --- a/src/lib/focus-indicator/focus-indicator.test.ts +++ b/src/lib/focus-indicator/focus-indicator.test.ts @@ -89,8 +89,7 @@ describe('FocusIndicator', () => { await focusKeyboard(detachedButton); - const surface = getShadowElement(focusIndicator, 'div[part=indicator]') as HTMLElement; - const style = getComputedStyle(surface); + const style = getComputedStyle(focusIndicator); expect(style.animationName).to.equal('inward-grow, inward-shrink'); expect(button.matches(':focus-visible')).to.be.true; @@ -100,8 +99,7 @@ describe('FocusIndicator', () => { it('should set circular', async () => { const { focusIndicator } = await createFixture({ circular: true }); - const surface = getShadowElement(focusIndicator, 'div[part=indicator]') as HTMLElement; - const style = getComputedStyle(surface); + const style = getComputedStyle(focusIndicator); expect(style.getPropertyValue('--_focus-indicator-shape')).to.equal('50%'); expect(focusIndicator.circular).to.be.true; diff --git a/src/lib/icon-button/_core.scss b/src/lib/icon-button/_core.scss index 740a69d2e..80cef9de0 100644 --- a/src/lib/icon-button/_core.scss +++ b/src/lib/icon-button/_core.scss @@ -111,10 +111,6 @@ @include override(icon-color, toggle-on-icon-color); } -@mixin toggle-outlined { - // @include override(icon-color, outlined-toggle-icon-color); -} - @mixin toggle-on-outlined { @include override(background-color, outlined-toggle-on-background-color); @include override(icon-color, outlined-toggle-on-icon-color); diff --git a/src/lib/icon-button/icon-button.html b/src/lib/icon-button/icon-button.html index daf389d12..943093350 100644 --- a/src/lib/icon-button/icon-button.html +++ b/src/lib/icon-button/icon-button.html @@ -4,7 +4,7 @@ - + diff --git a/src/lib/icon-button/icon-button.scss b/src/lib/icon-button/icon-button.scss index b70f2f6c5..42d38f05a 100644 --- a/src/lib/icon-button/icon-button.scss +++ b/src/lib/icon-button/icon-button.scss @@ -131,12 +131,6 @@ forge-state-layer { } } -:host([toggle]:not([on])[variant=outlined]) { - .forge-icon-button { - @include core.toggle-outlined; - } -} - :host([toggle][on][variant=outlined]) { .forge-icon-button { @include core.toggle-on-outlined; diff --git a/src/lib/list/list-item/list-item.html b/src/lib/list/list-item/list-item.html index 2739fd90d..46ac52b2d 100644 --- a/src/lib/list/list-item/list-item.html +++ b/src/lib/list/list-item/list-item.html @@ -13,6 +13,6 @@ - + diff --git a/src/lib/list/list-item/list-item.scss b/src/lib/list/list-item/list-item.scss index 165a9a979..8f2865472 100644 --- a/src/lib/list/list-item/list-item.scss +++ b/src/lib/list/list-item/list-item.scss @@ -202,6 +202,8 @@ slot[name=tertiary-title] { } forge-focus-indicator { + z-index: 1; // Fixes an animation artifact in Safari + @include focus-indicator.provide-theme(( shape: shape.variable(medium) // TODO: use list-item token? )); diff --git a/src/lib/menu/menu-constants.ts b/src/lib/menu/menu-constants.ts index e782afc02..10ab456e8 100644 --- a/src/lib/menu/menu-constants.ts +++ b/src/lib/menu/menu-constants.ts @@ -9,7 +9,7 @@ const classes = { }; const selectors = { - TOGGLE: `.${elementName}__toggle,[${elementName}-toggle],forge-button,button,[type=button],[role=button],a,[tabindex]:not([tabindex^="-"])`, + TOGGLE: `.${elementName}__toggle,[${elementName}-toggle],forge-button,forge-icon-button,forge-fab,button,[type=button],[role=button],a,[tabindex]:not([tabindex^="-"])`, MENU_LIST: 'forge-list' }; diff --git a/src/lib/slider/slider.html b/src/lib/slider/slider.html index 62f6786d1..1025041d5 100644 --- a/src/lib/slider/slider.html +++ b/src/lib/slider/slider.html @@ -7,7 +7,7 @@
- +
diff --git a/src/lib/switch/switch.html b/src/lib/switch/switch.html index 274a8234a..3d74d75a9 100644 --- a/src/lib/switch/switch.html +++ b/src/lib/switch/switch.html @@ -22,7 +22,7 @@
- +