diff --git a/src/dev/pages/scaffold/scaffold.ejs b/src/dev/pages/scaffold/scaffold.ejs index 640711730..43b1cf41a 100644 --- a/src/dev/pages/scaffold/scaffold.ejs +++ b/src/dev/pages/scaffold/scaffold.ejs @@ -1,5 +1,5 @@
- +
Left
Right
Header
diff --git a/src/lib/core/styles/tokens/scaffold/_tokens.scss b/src/lib/core/styles/tokens/scaffold/_tokens.scss new file mode 100644 index 000000000..bbe961d43 --- /dev/null +++ b/src/lib/core/styles/tokens/scaffold/_tokens.scss @@ -0,0 +1,13 @@ +@use 'sass:map'; +@use '../../utils'; + +$tokens: ( + height: utils.module-val(scaffold, height, 100%), + width: utils.module-val(scaffold, width, 100%), + overflow: utils.module-val(scaffold, overflow, hidden), + body-position: utils.module-val(scaffold, body-position, relative), +) !default; + +@function get($key) { + @return map.get($tokens, $key); +} diff --git a/src/lib/scaffold/_core.scss b/src/lib/scaffold/_core.scss new file mode 100644 index 000000000..5e3d4865b --- /dev/null +++ b/src/lib/scaffold/_core.scss @@ -0,0 +1,93 @@ +@use './token-utils' as *; + +@forward './token-utils'; + +@mixin host { + display: block; + + width: #{token(width)}; + height: #{token(height)}; +} + +@mixin base { + position: relative; + + display: grid; + grid-template-areas: + 'left header right' + 'left body right' + 'left footer right'; + grid-template-rows: auto 1fr auto; + grid-template-columns: auto 1fr auto; + + height: #{token(height)}; + width: #{token(width)}; + + overflow: #{token(overflow)}; +} + +@mixin header { + grid-area: header; + min-width: 0; + min-height: 0; +} + +@mixin body { + position: #{token(body-position)}; + + display: grid; + grid-area: body; + grid-template-areas: + 'body-left body-header body-right' + 'body-left body-inner body-right' + 'body-left body-footer body-right'; + grid-template-rows: auto 1fr auto; + grid-template-columns: auto 1fr auto; + + width: #{token(width)}; + min-width: 0; + min-height: 0; + overflow: hidden; +} + +@mixin footer { + grid-area: footer; +} + +@mixin body-header { + grid-area: body-header; +} + +@mixin body-inner { + overflow: auto; + grid-area: body-inner; +} + +@mixin body-footer { + grid-area: body-footer; +} + +@mixin scrollable { + overflow: auto; +} + +@mixin body-left { + grid-area: body-left; +} + +@mixin body-right { + grid-area: body-right; +} + +@mixin left { + grid-area: left; +} + +@mixin right { + grid-area: right; +} + +@mixin slotted-base { + min-width: 0; + min-height: 0; +} diff --git a/src/lib/scaffold/_mixins.scss b/src/lib/scaffold/_mixins.scss deleted file mode 100644 index 1fa7b4769..000000000 --- a/src/lib/scaffold/_mixins.scss +++ /dev/null @@ -1,189 +0,0 @@ -@use '../theme'; - -@mixin core-styles() { - .forge-scaffold { - @include base; - - &__header { - @include header; - } - - &__body { - @include body; - - // These next 3 selectors are needed for IE 11 support - ::slotted([slot=left]) { - @include body-aside; - @include left; - } - - ::slotted([slot=right]) { - @include body-aside; - @include right; - } - - ::slotted([slot=body-left]) { - @include body-aside; - @include body-left-aside; - } - - ::slotted([slot=body-right]) { - @include body-aside; - @include body-right-aside; - } - - ::slotted([slot=body-header]) { - @include body-header; - } - - ::slotted([slot=body]) { - @include body-main; - } - - ::slotted([slot=body-footer]) { - @include body-footer; - } - } - - // These are the proper selectors to use for native Shadow DOM, but don't work in - // IE 11 (see above duplicate selectors for IE support) - ::slotted([slot=left]), - ::slotted([slot=right]), - ::slotted([slot=body-left]), - ::slotted([slot=body-right]) { - @include body-aside; - } - - ::slotted([slot=body-left]) { - @include body-left-aside; - } - - ::slotted([slot=body-right]) { - @include body-right-aside; - } - - ::slotted([slot=footer]) { - @include footer; - } - - ::slotted([slot=left]) { - @include left; - } - - ::slotted([slot=right]) { - @include right; - } - } -} - -@mixin viewport-styles() { - @include theme.css-custom-property(width, --forge-scaffold-width, 100vw); - @include theme.css-custom-property(height, --forge-scaffold-height, 100vh); - - --forge-scaffold-height: 100vh; - --forge-scaffold-width: 100vw; - - .forge-scaffold { - @include theme.css-custom-property(height, --forge-scaffold-height, 100vh); - - --forge-scaffold-height: 100vh; - - &__body { - @include theme.css-custom-property(width, --forge-scaffold-width, 100vw); - - --forge-scaffold-width: 100vw; - } - } -} - -@mixin host() { - @include theme.css-custom-property(width, --forge-scaffold-width, 100%); - @include theme.css-custom-property(height, --forge-scaffold-height, 100%); - - box-sizing: border-box; - display: block; - position: relative; -} - -@mixin base() { - @include theme.css-custom-property(height, --forge-scaffold-height, 100%); - @include theme.css-custom-property(position, --forge-scaffold-position, relative); - @include theme.css-custom-property(overflow, --forge-scaffold-overflow, hidden); - - display: grid; - grid-template-rows: auto 1fr auto; - grid-template-columns: auto 1fr auto; -} - -@mixin header() { - grid-row: 1; - grid-column: 2; - min-width: 0; -} - -@mixin body() { - @include theme.css-custom-property(width, --forge-scaffold-width, 100%); - @include theme.css-custom-property(position, --forge-scaffold-body-position, relative); - - display: grid; - grid-template-rows: auto 1fr auto; - grid-template-columns: auto 1fr auto; - grid-row: 2; - grid-column: 2; - overflow: hidden; -} - -@mixin body-header() { - grid-row: 1; - grid-column: 2; -} - -@mixin body-main() { - overflow: auto; - grid-row: 2; - grid-column: 2; -} - -@mixin body-footer() { - grid-row: 3; - grid-column: 2; -} - -@mixin footer() { - grid-row: 3; - grid-column: 2; -} - -@mixin body-aside() { - overflow: auto; -} - -@mixin body-left-aside() { - grid-column: 1; - - // When specifying a body-left slot, this allows the aside to span the last row - grid-row: 1/4; -} - -@mixin body-right-aside() { - grid-column: 3; - - // When specifying a body-left slot, this allows the aside to span the last row - grid-row: 1/4; -} - -@mixin left() { - grid-column: 1; - grid-row: 1/5; -} - -@mixin right() { - grid-column: 3; - grid-row: 1/5; -} - -@mixin host() { - display: block; - height: 100%; - width: 100%; -} diff --git a/src/lib/scaffold/_token-utils.scss b/src/lib/scaffold/_token-utils.scss new file mode 100644 index 000000000..20ca17012 --- /dev/null +++ b/src/lib/scaffold/_token-utils.scss @@ -0,0 +1,25 @@ +@use '../core/styles/tokens/scaffold/tokens'; +@use '../core/styles/tokens/token-utils'; + +$_module: scaffold; +$_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/scaffold/build.json b/src/lib/scaffold/build.json deleted file mode 100644 index 69a8f23e3..000000000 --- a/src/lib/scaffold/build.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "../../../node_modules/@tylertech/forge-cli/config/build-schema.json", - "extends": "../build.json", - "stylesheets": [ - "./forge-scaffold.scss" - ] -} diff --git a/src/lib/scaffold/forge-scaffold.scss b/src/lib/scaffold/forge-scaffold.scss deleted file mode 100644 index 3af33b416..000000000 --- a/src/lib/scaffold/forge-scaffold.scss +++ /dev/null @@ -1,38 +0,0 @@ -@use './mixins'; - -.forge-scaffold { - @include mixins.base; - - &__header { - @include mixins.header; - } - - &__body { - @include mixins.body; - - &-main { - @include mixins.body-main; - } - - &-footer { - @include mixins.body-footer; - } - } - - &__left-aside, - &__right-aside { - @include mixins.body-aside; - } - - &__left-aside { - @include mixins.body-left-aside; - } - - &__right-aside { - @include mixins.body-right-aside; - } - - &__footer { - @include mixins.footer; - } -} diff --git a/src/lib/scaffold/index.scss b/src/lib/scaffold/index.scss new file mode 100644 index 000000000..98a38c0d9 --- /dev/null +++ b/src/lib/scaffold/index.scss @@ -0,0 +1 @@ +@forward './core'; diff --git a/src/lib/scaffold/index.ts b/src/lib/scaffold/index.ts index 0cbd74c7a..87655baf5 100644 --- a/src/lib/scaffold/index.ts +++ b/src/lib/scaffold/index.ts @@ -1,5 +1,4 @@ import { defineCustomElement } from '@tylertech/forge-core'; - import { ScaffoldComponent } from './scaffold'; export * from './scaffold-constants'; diff --git a/src/lib/scaffold/scaffold-constants.ts b/src/lib/scaffold/scaffold-constants.ts index 6e2150a1d..17ad65cc0 100644 --- a/src/lib/scaffold/scaffold-constants.ts +++ b/src/lib/scaffold/scaffold-constants.ts @@ -2,6 +2,16 @@ import { COMPONENT_NAME_PREFIX } from '../constants'; const elementName: keyof HTMLElementTagNameMap = `${COMPONENT_NAME_PREFIX}scaffold`; +const observedAttributes = { + VIEWPORT: 'viewport' +}; + +const attributes = { + ...observedAttributes +}; + export const SCAFFOLD_CONSTANTS = { - elementName + elementName, + observedAttributes, + attributes }; diff --git a/src/lib/scaffold/scaffold.html b/src/lib/scaffold/scaffold.html index 9da801806..bd426dad7 100644 --- a/src/lib/scaffold/scaffold.html +++ b/src/lib/scaffold/scaffold.html @@ -1,10 +1,10 @@ \ No newline at end of file + diff --git a/src/lib/scaffold/scaffold.scss b/src/lib/scaffold/scaffold.scss index bdb8e3943..5578a3ae5 100644 --- a/src/lib/scaffold/scaffold.scss +++ b/src/lib/scaffold/scaffold.scss @@ -1,16 +1,96 @@ -@use '../theme'; -@use './mixins'; +@use './core' as *; -@include mixins.core-styles; +$host-tokens: [height width]; + +// +// Host +// :host { - @include mixins.host; + @include tokens($includes: $host-tokens); +} + +:host { + @include host; } :host([hidden]) { display: none; } +// +// Base +// + +.forge-scaffold { + @include tokens($excludes: $host-tokens); +} + +.forge-scaffold { + @include base; +} + +.header { + @include header; +} + +.body { + @include body; +} + +// +// Slotted +// + +::slotted(*) { + @include slotted-base; +} + +::slotted([slot=left]) { + @include left; +} + +::slotted([slot=right]) { + @include right; +} + +::slotted([slot=body-left]) { + @include body-left; +} + +::slotted([slot=body-right]) { + @include body-right; +} + +::slotted([slot=body-header]) { + @include body-header; +} + +::slotted([slot=body]) { + @include body-inner; +} + +::slotted([slot=body-footer]) { + @include body-footer; +} + +::slotted([slot=footer]) { + @include footer; +} + +::slotted([slot=left]), +::slotted([slot=right]), +::slotted([slot=body-left]), +::slotted([slot=body]), +::slotted([slot=body-right]) { + @include scrollable; +} + +// +// Viewport +// + :host([viewport]) { - @include mixins.viewport-styles; + @include override(height, 100dvh, value); + @include override(width, 100dvw, value); } diff --git a/src/lib/scaffold/scaffold.test.ts b/src/lib/scaffold/scaffold.test.ts new file mode 100644 index 000000000..5761e01d3 --- /dev/null +++ b/src/lib/scaffold/scaffold.test.ts @@ -0,0 +1,29 @@ +import { fixture, expect } from '@open-wc/testing'; +import type { IScaffoldComponent } from './scaffold'; +import { SCAFFOLD_CONSTANTS } from './scaffold-constants'; + +import './scaffold'; + +describe('Scaffold', () => { + it('should create shadow root', async () => { + const el = await fixture(''); + + expect(el.shadowRoot).to.be.ok; + }); + + it('should reflect viewport attribute when set via property', async () => { + const el = await fixture(''); + + el.viewport = true + + expect(el.viewport).to.be.true; + expect(el.hasAttribute(SCAFFOLD_CONSTANTS.attributes.VIEWPORT)).to.be.true; + }); + + it('should reflect viewport property when set via attribute', async () => { + const el = await fixture(''); + + expect(el.viewport).to.be.true; + expect(el.hasAttribute(SCAFFOLD_CONSTANTS.attributes.VIEWPORT)).to.be.true; + }); +}); diff --git a/src/lib/scaffold/scaffold.ts b/src/lib/scaffold/scaffold.ts index 2da90e4f7..8b079331a 100644 --- a/src/lib/scaffold/scaffold.ts +++ b/src/lib/scaffold/scaffold.ts @@ -5,7 +5,9 @@ import { SCAFFOLD_CONSTANTS } from './scaffold-constants'; import template from './scaffold.html'; import styles from './scaffold.scss'; -export interface IScaffoldComponent extends IBaseComponent {} +export interface IScaffoldComponent extends IBaseComponent { + viewport: boolean; +} declare global { interface HTMLElementTagNameMap { @@ -14,16 +16,58 @@ declare global { } /** - * The custom element class behind the `` element. - * * @tag forge-scaffold + * + * @summary A scaffold provides a generic layout structure for your content using common named areas. + * + * @property {boolean} viewport - Whether the scaffold should be full viewport height. + * + * @attribute {boolean} viewport - Whether the scaffold should be full viewport height. + * + * @cssproperty --forge-scaffold-height - The `height` of the scaffold. + * @cssproperty --forge-scaffold-width - The `width` of the scaffold. + * @cssproperty --forge-scaffold-overflow - The `overflow` of the scaffold. + * @cssproperty --forge-scaffold-body-position - The `position` of the scaffold body. + * + * @csspart root - The root container element. + * @csspart header - The header of the scaffold. + * @csspart body - The body of the scaffold. + * + * @slot header - Places content in the header. + * @slot body - Places content in the body. + * @slot footer - Places content in the footer. + * @slot left - Places content to the left of all content. + * @slot right - Places content to the right of all content. + * @slot body-header - Places content in the header of the body. + * @slot body-footer - Places content in the footer of the body. + * @slot body-left - Places content to the left of the body content. + * @slot body-right - Places content to the right of the body content. */ @CustomElement({ name: SCAFFOLD_CONSTANTS.elementName }) export class ScaffoldComponent extends BaseComponent implements IScaffoldComponent { + public static get observedAttributes(): string[] { + return Object.values(SCAFFOLD_CONSTANTS.observedAttributes); + } + constructor() { super(); attachShadowTemplate(this, template, styles); } + + public attributeChangedCallback(name: string, oldValue: string, newValue: string): void { + switch (name) { + case SCAFFOLD_CONSTANTS.observedAttributes.VIEWPORT: + this.viewport = newValue !== null; + break; + } + } + + public get viewport(): boolean { + return this.hasAttribute(SCAFFOLD_CONSTANTS.attributes.VIEWPORT); + } + public set viewport(value: boolean) { + this.toggleAttribute(SCAFFOLD_CONSTANTS.attributes.VIEWPORT, Boolean(value)); + } } diff --git a/src/test/spec/scaffold/scaffold.spec.ts b/src/test/spec/scaffold/scaffold.spec.ts deleted file mode 100644 index cad880cef..000000000 --- a/src/test/spec/scaffold/scaffold.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { defineScaffoldComponent, IScaffoldComponent, SCAFFOLD_CONSTANTS } from '@tylertech/forge/scaffold'; -import { removeElement, getShadowElement } from '@tylertech/forge-core'; - -interface ITestContext { - context: ITestScaffoldContext -} - -interface ITestScaffoldContext { - component: IScaffoldComponent; - append(): void; - destroy(): void; -} - -describe('ScaffoldComponent', function(this: ITestContext) { - beforeAll(function(this: ITestContext) { - defineScaffoldComponent(); - }); - - afterEach(function(this: ITestContext) { - this.context.destroy(); - }); - - it('should be connected', function(this: ITestContext) { - this.context = setupTestContext(true); - expect(this.context.component.isConnected).toBe(true); - }); - - it('should attach shadow template', function(this: ITestContext) { - this.context = setupTestContext(true); - expect(this.context.component.shadowRoot).toBeDefined(); - expect(this.context.component.shadowRoot!.childNodes.length).toBeGreaterThan(0); - }); - - it('should contains all slots', function(this: ITestContext) { - this.context = setupTestContext(); - const headerSlot = getShadowElement(this.context.component, 'slot[name=header]') as HTMLSlotElement; - const bodyLeftSlot = getShadowElement(this.context.component, 'slot[name=body-left]') as HTMLSlotElement; - const bodySlot = getShadowElement(this.context.component, 'slot[name=body]') as HTMLSlotElement; - const bodyRightSlot = getShadowElement(this.context.component, 'slot[name=body-right]') as HTMLSlotElement; - const bodyFooterSlot = getShadowElement(this.context.component, 'slot[name=body-footer]') as HTMLSlotElement; - const footerSlot = getShadowElement(this.context.component, 'slot[name=footer]') as HTMLSlotElement; - - expect(headerSlot).not.toBeNull(); - expect(bodyLeftSlot).not.toBeNull(); - expect(bodySlot).not.toBeNull(); - expect(bodyRightSlot).not.toBeNull(); - expect(bodyFooterSlot).not.toBeNull(); - expect(footerSlot).not.toBeNull(); - }); - - function setupTestContext(append = false): ITestScaffoldContext { - const fixture = document.createElement('div'); - fixture.id = 'scaffold-test-fixture'; - const component = document.createElement(SCAFFOLD_CONSTANTS.elementName) as IScaffoldComponent; - fixture.appendChild(component); - if (append) document.body.appendChild(fixture); - return { - component, - append: () => document.body.appendChild(fixture), - destroy: () => removeElement(fixture) - }; - } -});