From a0ef378a1029ca82ccefa5f57ed824e888c9d2b5 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Mon, 26 Apr 2021 11:37:21 -0400 Subject: [PATCH] feat(design-system): better integrate with DI and enforce constraints (#4634) * vNext: update components to extend FoundationElement (#4570) * support foundation element on accordion and accordion item * update anchor to use foundation element * update AnchoredRegion to extend FoundationElement * update Badge to extend FoundationElement * update breadcrumb and breadcrumb item to extend FoundationElement * update Button to extend FoundationElement * update checkbox to extend from FoundationElement * update Dialog to extend FoundationElement * update disclosure to extend FoundationElement * update divider to extend FoundationElement * update Flipper to extend FoundationElement * update horizontal scroll to extend FoundationElement * update Listbox and ListboxOption to extend FoundationElement * update combobox to extend FoundationElement * update select to extend combobox and update tests * fix listbox option styles and export * update tests (wip) * update Menu and MenuItem to extend FoundationElement * update number field to extend FoundationElement * update base name values * fix: prevent the mixin helper from copying over constructor properties * feat: fixture ergonomic improvements for foundation elements * test: fix Anchor and associated unit tests based on new system * remove incorrect tagFor usage * update radio and radiogroup * update skeleton * update slider and slider label * update switch * update tabs et all to use FoundationElement * update text area and text field to use foundation el * Update tooltip to use FoundationElement * update tests and tree item and view * remove website from lerna packages in favor of npm registry to prevent breaking changes for the time being * update progress and progress ring to use Foundation element * fixing the tests * feat: enable fixtures to handle N foundation elements and custom system * fixing tests! * Change files * fix errors in fast-website * update typings for explorer * update naming convention to lowercase fast * update imports for sites * Change files * update template names to lowercase * update style casing and apply updates to component registries * update tsdoc links for templates Co-authored-by: Rob Eisenberg * (vNext) update casing for style exports (#4618) * update casing for style exports to illustrate they are functions * Change files * add api report * feat: refactor color recipes (#4623) * refactor color recipes away from DesignSystem data structure * rename dir * cleanup * factor binary-search out to it's own file * updating code docs * Change files * fixing binary-search * Update packages/web-components/fast-components/src/color-vNext/palette.ts Co-authored-by: Brian Heston <47367562+bheston@users.noreply.github.com> * addressing feedback * adding readme * pretty pretty closes #3833 Co-authored-by: nicholasrice Co-authored-by: Brian Heston <47367562+bheston@users.noreply.github.com> * fix: update storybook to use custom element definitions and fix rollup (#4629) * update storybook to use custom element defintions * Change files Co-authored-by: nicholasrice * feat(design-system): better integrate with DI and enforce constraints * test(DesignSystem): add basic tests for new API behavior * fix(fast-components): update rollup index to use the new DS API * Change files Co-authored-by: Chris Holt Co-authored-by: Nicholas Rice <3213292+nicholasrice@users.noreply.github.com> Co-authored-by: Brian Heston <47367562+bheston@users.noreply.github.com> Co-authored-by: nicholasrice Co-authored-by: EisenbergEffect --- ...-0e27942e-3b81-4a7a-bd6c-031a14017a89.json | 7 + ...-6fc300e8-5aea-4f33-bde6-b229bbb15761.json | 7 + .../fast-components/src/index-rollup.ts | 10 +- .../src/slider-label/slider-label.styles.ts | 2 +- .../fast-foundation/docs/api-report.md | 17 ++- .../src/design-system/design-system.spec.ts | 137 +++++++++++------- .../src/design-system/design-system.ts | 102 ++++++++++--- .../src/design-token/design-token.spec.ts | 16 +- .../foundation-element.spec.ts | 12 +- .../src/test-utilities/fixture.ts | 12 +- 10 files changed, 219 insertions(+), 103 deletions(-) create mode 100644 change/@microsoft-fast-components-0e27942e-3b81-4a7a-bd6c-031a14017a89.json create mode 100644 change/@microsoft-fast-foundation-6fc300e8-5aea-4f33-bde6-b229bbb15761.json diff --git a/change/@microsoft-fast-components-0e27942e-3b81-4a7a-bd6c-031a14017a89.json b/change/@microsoft-fast-components-0e27942e-3b81-4a7a-bd6c-031a14017a89.json new file mode 100644 index 00000000000..06200431dae --- /dev/null +++ b/change/@microsoft-fast-components-0e27942e-3b81-4a7a-bd6c-031a14017a89.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "fix(fast-components): update rollup index to use the new DS API", + "packageName": "@microsoft/fast-components", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@microsoft-fast-foundation-6fc300e8-5aea-4f33-bde6-b229bbb15761.json b/change/@microsoft-fast-foundation-6fc300e8-5aea-4f33-bde6-b229bbb15761.json new file mode 100644 index 00000000000..a1273309fff --- /dev/null +++ b/change/@microsoft-fast-foundation-6fc300e8-5aea-4f33-bde6-b229bbb15761.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "feat(design-system): better integrate with DI and enforce constraints", + "packageName": "@microsoft/fast-foundation", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/fast-components/src/index-rollup.ts b/packages/web-components/fast-components/src/index-rollup.ts index 8631ea04679..1ee37d70de7 100644 --- a/packages/web-components/fast-components/src/index-rollup.ts +++ b/packages/web-components/fast-components/src/index-rollup.ts @@ -8,10 +8,6 @@ export * from "./index"; /** * TODO rename this to FASTDesignSystem when {@link @FASTDesignSystem} interface is removed. */ -export const fastDesignSystem = new DesignSystem(); - -Object.values(fastComponents).forEach(definition => { - fastDesignSystem.register(definition()); -}); - -fastDesignSystem.applyTo(document.body); +export const fastDesignSystem = DesignSystem.getOrCreate().register( + ...Object.values(fastComponents).map(definition => definition()) +); diff --git a/packages/web-components/fast-components/src/slider-label/slider-label.styles.ts b/packages/web-components/fast-components/src/slider-label/slider-label.styles.ts index 764d1870108..b093ff4ed7e 100644 --- a/packages/web-components/fast-components/src/slider-label/slider-label.styles.ts +++ b/packages/web-components/fast-components/src/slider-label/slider-label.styles.ts @@ -52,7 +52,7 @@ export const verticalSliderStyles = css` export const sliderLabelStyles = (context, definition) => css` ${display("block")} :host { - // font-family: ${bodyFont}; + font-family: ${bodyFont}; color: ${neutralForegroundRestBehavior.var}; fill: currentcolor; } diff --git a/packages/web-components/fast-foundation/docs/api-report.md b/packages/web-components/fast-foundation/docs/api-report.md index 8b68008dcba..7a3f3a9808f 100644 --- a/packages/web-components/fast-foundation/docs/api-report.md +++ b/packages/web-components/fast-foundation/docs/api-report.md @@ -759,17 +759,22 @@ export interface DelegatesARIATextbox extends ARIAGlobalStatesAndProperties { export type DerivedDesignTokenValue = T extends Function ? never : (target: HTMLElement) => T; // @alpha (undocumented) -export class DesignSystem { +export interface DesignSystem { // (undocumented) - applyTo(element: HTMLElement): Container; + register(...params: any[]): DesignSystem; // (undocumented) - register(...params: any[]): this; + withElementDisambiguation(callback: ElementDisambiguationCallback): DesignSystem; // (undocumented) - withElementDisambiguation(callback: ElementDisambiguationCallback): this; - // (undocumented) - withPrefix(prefix: string): this; + withPrefix(prefix: string): DesignSystem; } +// @alpha (undocumented) +export const DesignSystem: Readonly<{ + tagFor(type: Constructable): string; + responsibleFor(element: HTMLElement): DesignSystem; + getOrCreate(element?: HTMLElement): DesignSystem; +}>; + // @public export interface DesignSystemConsumer { // (undocumented) diff --git a/packages/web-components/fast-foundation/src/design-system/design-system.spec.ts b/packages/web-components/fast-foundation/src/design-system/design-system.spec.ts index 80278fdb691..a48646b1a25 100644 --- a/packages/web-components/fast-foundation/src/design-system/design-system.spec.ts +++ b/packages/web-components/fast-foundation/src/design-system/design-system.spec.ts @@ -1,53 +1,83 @@ import type { Constructable } from "@microsoft/fast-element"; import { expect } from "chai"; -import type { Container } from "../di"; +import { Container, DI } from "../di"; import { uniqueElementName } from "../test-utilities/fixture"; import { DesignSystem, DesignSystemRegistrationContext } from "./design-system"; describe("DesignSystem", () => { + it("Should return the same instance for the same element", () => { + const host = document.createElement("div"); + const ds1 = DesignSystem.getOrCreate(host); + const ds2 = DesignSystem.getOrCreate(host); + + expect(ds1).to.equal(ds2); + }); + + it("Should find the responsible design system for an element in the hierarchy", () => { + const host = document.createElement("div"); + const child = document.createElement("div"); + host.appendChild(child); + + const ds1 = DesignSystem.getOrCreate(host); + const ds2 = DesignSystem.responsibleFor(child); + + expect(ds1).to.equal(ds2); + }); + it("Should initialize with a default prefix of 'fast'", () => { const host = document.createElement("div"); - const container = new DesignSystem().applyTo(host); + let prefix = ''; - expect(container.get(DesignSystemRegistrationContext).elementPrefix).to.equal( - "fast" - ); + DesignSystem.getOrCreate(host) + .register({ + register(container: Container) { + prefix = container.get(DesignSystemRegistrationContext).elementPrefix; + } + }); + + expect(prefix).to.equal("fast"); }); it("Should initialize with a provided prefix", () => { const host = document.createElement("div"); - const container = new DesignSystem().withPrefix("custom").applyTo(host); + let prefix = ''; - expect(container.get(DesignSystemRegistrationContext).elementPrefix).to.equal( - "custom" - ); + DesignSystem.getOrCreate(host) + .withPrefix("custom") + .register({ + register(container: Container) { + prefix = container.get(DesignSystemRegistrationContext).elementPrefix; + } + }); + + expect(prefix).to.equal("custom"); }); - it("Should apply registries to the container", () => { + it("Should apply registries to the container associated with the host", () => { let capturedContainer: Container | null = null; const host = document.createElement("div"); - const container = new DesignSystem() + + DesignSystem.getOrCreate(host) .register({ register(container: Container) { capturedContainer = container; }, - }) - .applyTo(host); + }); - expect(capturedContainer).to.equal(container); + const container = DI.getOrCreateDOMContainer(host); + expect(container).equals(capturedContainer); }); it("Should provide a way for registries to define elements", () => { let capturedDefine: any; const host = document.createElement("div"); - new DesignSystem() + DesignSystem.getOrCreate(host) .register({ register(container: Container) { capturedDefine = container.get(DesignSystemRegistrationContext) .tryDefineElement; }, - }) - .applyTo(host); + }); expect(capturedDefine).to.not.be.null; }); @@ -55,35 +85,34 @@ describe("DesignSystem", () => { it("Should provide a way for registries to get the default prefix", () => { let capturePrefix: string | null = null; const host = document.createElement("div"); - new DesignSystem() + DesignSystem.getOrCreate(host) .withPrefix("custom") .register({ register(container: Container) { capturePrefix = container.get(DesignSystemRegistrationContext) .elementPrefix; }, - }) - .applyTo(host); + }); expect(capturePrefix).to.equal("custom"); }); - it("Should register elements when applied", () => { + it("Should register elements", () => { const elementName = uniqueElementName(); const customElement = class extends HTMLElement {}; const host = document.createElement("div"); - const system = new DesignSystem().register({ - register(container: Container) { - const context = container.get(DesignSystemRegistrationContext); - context.tryDefineElement(elementName, customElement, x => - x.defineElement() - ); - }, - }); expect(customElements.get(elementName)).to.be.undefined; - system.applyTo(host); + DesignSystem.getOrCreate(host) + .register({ + register(container: Container) { + const context = container.get(DesignSystemRegistrationContext); + context.tryDefineElement(elementName, customElement, x => + x.defineElement() + ); + }, + }); expect(customElements.get(elementName)).to.equal(customElement); }); @@ -93,7 +122,7 @@ describe("DesignSystem", () => { const elementName2 = uniqueElementName(); const host = document.createElement("div"); let capturedType: Constructable | null = null; - const system = new DesignSystem() + DesignSystem.getOrCreate(host) .withElementDisambiguation((name, type, existingType) => { capturedType = existingType; return elementName2; @@ -119,8 +148,7 @@ describe("DesignSystem", () => { ); }, } - ) - .applyTo(host); + ); expect(capturedType).to.not.be.null; expect(customElements.get(elementName)).to.not.be.undefined; @@ -131,28 +159,31 @@ describe("DesignSystem", () => { const elementName = uniqueElementName(); const customElement = class extends HTMLElement {}; const host = document.createElement("div"); - const system = new DesignSystem().register( - { - register(container: Container) { - const context = container.get(DesignSystemRegistrationContext); - context.tryDefineElement(elementName, customElement, x => - x.defineElement() - ); - }, - }, - { - register(container: Container) { - const context = container.get(DesignSystemRegistrationContext); - context.tryDefineElement( - elementName, - class extends HTMLElement {}, - x => x.defineElement() - ); + const system = DesignSystem.getOrCreate(host); + + expect(() => { + system.register( + { + register(container: Container) { + const context = container.get(DesignSystemRegistrationContext); + context.tryDefineElement(elementName, customElement, x => + x.defineElement() + ); + }, }, - } - ); + { + register(container: Container) { + const context = container.get(DesignSystemRegistrationContext); + context.tryDefineElement( + elementName, + class extends HTMLElement {}, + x => x.defineElement() + ); + }, + } + ); + }).not.to.throw(); - expect(() => system.applyTo(host)).not.to.throw(); expect(customElements.get(elementName)).to.equal(customElement); }); }); diff --git a/packages/web-components/fast-foundation/src/design-system/design-system.ts b/packages/web-components/fast-foundation/src/design-system/design-system.ts index 06acd813a3c..5aa1bf18230 100644 --- a/packages/web-components/fast-foundation/src/design-system/design-system.ts +++ b/packages/web-components/fast-foundation/src/design-system/design-system.ts @@ -68,31 +68,97 @@ const elementTagsByType = new Map(); /** * @alpha */ -export class DesignSystem { - private registrations: any[] = []; +export interface DesignSystem { + register(...params: any[]): DesignSystem; + withPrefix(prefix: string): DesignSystem; + withElementDisambiguation(callback: ElementDisambiguationCallback): DesignSystem; +} + +const designSystemKey = DI.createInterface(x => + x.cachedCallback(handler => { + const element = document.body as any; + const owned = element.$designSystem as DesignSystem; + + if (owned) { + return owned; + } + + return new DefaultDesignSystem(element, handler); + }) +); + +/** + * @alpha + */ +export const DesignSystem = Object.freeze({ + tagFor(type: Constructable): string { + return elementTagsByType.get(type)!; + }, + + responsibleFor(element: HTMLElement): DesignSystem { + const owned = (element as any).$designSystem as DesignSystem; + + if (owned) { + return owned; + } + + const container = DI.findResponsibleContainer(element); + return container.get(designSystemKey); + }, + + getOrCreate(element: HTMLElement = document.body): DesignSystem { + const owned = (element as any).$designSystem as DesignSystem; + + if (owned) { + return owned; + } + + const container = DI.getOrCreateDOMContainer(element); + + if (!container.has(designSystemKey, false)) { + container.register( + Registration.instance( + designSystemKey, + new DefaultDesignSystem(element, container) + ) + ); + } + + return container.get(designSystemKey); + }, +}); + +class DefaultDesignSystem implements DesignSystem { private prefix: string = "fast"; private disambiguate: ElementDisambiguationCallback = () => null; + private context: DesignSystemRegistrationContext; - public withPrefix(prefix: string) { - this.prefix = prefix; - return this; + constructor(private host: HTMLElement, private container: Container) { + (host as any).$designSystem = this; + + container.register( + Registration.callback(DesignSystemRegistrationContext, () => this.context) + ); } - public withElementDisambiguation(callback: ElementDisambiguationCallback) { - this.disambiguate = callback; + public withPrefix(prefix: string): DesignSystem { + this.prefix = prefix; return this; } - public register(...params: any[]) { - this.registrations.push(...params); + public withElementDisambiguation( + callback: ElementDisambiguationCallback + ): DesignSystem { + this.disambiguate = callback; return this; } - public applyTo(element: HTMLElement) { - const container = DI.getOrCreateDOMContainer(element); + public register(...registrations: any[]): DesignSystem { + const container = this.container; const elementDefinitionEntries: ElementDefinitionEntry[] = []; const disambiguate = this.disambiguate; - const context: DesignSystemRegistrationContext = { + + const context: DesignSystemRegistrationContext = (this.context = { elementPrefix: this.prefix, tryDefineElement( name: string, @@ -131,13 +197,9 @@ export class DesignSystem { ) ); }, - }; - - container.register( - Registration.instance(DesignSystemRegistrationContext, context) - ); + }); - container.register(...this.registrations); + container.register(...registrations); for (const entry of elementDefinitionEntries) { entry.callback(entry); @@ -147,7 +209,7 @@ export class DesignSystem { } } - return container; + return this; } } @@ -170,6 +232,6 @@ class ElementDefinitionEntry implements ElementDefinitionContext { } tagFor(type: Constructable): string { - return elementTagsByType.get(type)!; + return DesignSystem.tagFor(type)!; } } diff --git a/packages/web-components/fast-foundation/src/design-token/design-token.spec.ts b/packages/web-components/fast-foundation/src/design-token/design-token.spec.ts index 2624185214f..a6414279c51 100644 --- a/packages/web-components/fast-foundation/src/design-token/design-token.spec.ts +++ b/packages/web-components/fast-foundation/src/design-token/design-token.spec.ts @@ -2,15 +2,23 @@ import { css, DOM, FASTElement, html, Observable } from "@microsoft/fast-element"; import { expect } from "chai"; import { DesignSystem } from "../design-system"; +import { uniqueElementName } from "../test-utilities/fixture"; import { FoundationElement } from "../foundation-element"; import { DesignToken } from "./design-token"; -new DesignSystem().register( - FoundationElement.compose({ type: class extends FoundationElement { }, template: html``, baseName: "custom-element" })() -).applyTo(document.body) +const elementName = uniqueElementName(); + +DesignSystem.getOrCreate() + .register( + FoundationElement.compose({ + type: class extends FoundationElement { }, + baseName: elementName, + template: html`` + })() + ); function addElement(parent = document.body): FASTElement & HTMLElement { - const el = document.createElement("fast-custom-element") as any; + const el = document.createElement(`fast-${elementName}`) as any; parent.appendChild(el); return el; } diff --git a/packages/web-components/fast-foundation/src/foundation-element/foundation-element.spec.ts b/packages/web-components/fast-foundation/src/foundation-element/foundation-element.spec.ts index 9e45f4ea6f0..512489cfb3d 100644 --- a/packages/web-components/fast-foundation/src/foundation-element/foundation-element.spec.ts +++ b/packages/web-components/fast-foundation/src/foundation-element/foundation-element.spec.ts @@ -132,7 +132,8 @@ describe("FoundationElement", () => { }); const host = document.createElement("div"); - const container = new DesignSystem().register(myElement()).applyTo(host); + DesignSystem.getOrCreate(host).register(myElement()); + const container = DI.getOrCreateDOMContainer(host); const fullName = `fast-${baseName}`; const presentation = container.get( @@ -171,7 +172,8 @@ describe("FoundationElement", () => { }); const host = document.createElement("div"); - const container = new DesignSystem().register(myElement()).applyTo(host); + DesignSystem.getOrCreate(host).register(myElement()); + const container = DI.getOrCreateDOMContainer(host); const presentation = container.get( ComponentPresentation.keyFrom(fullName) @@ -200,9 +202,9 @@ describe("FoundationElement", () => { }; const host = document.createElement("div"); - const container = new DesignSystem() - .register(myElement(overrides)) - .applyTo(host); + DesignSystem.getOrCreate(host) + .register(myElement(overrides)); + const container = DI.getOrCreateDOMContainer(host); const originalFullName = `fast-${originalName}`; const overrideFullName = `my-${overrideName}`; diff --git a/packages/web-components/fast-foundation/src/test-utilities/fixture.ts b/packages/web-components/fast-foundation/src/test-utilities/fixture.ts index 2fca23f46d8..5b806c04c58 100644 --- a/packages/web-components/fast-foundation/src/test-utilities/fixture.ts +++ b/packages/web-components/fast-foundation/src/test-utilities/fixture.ts @@ -6,7 +6,7 @@ import { ViewTemplate, } from "@microsoft/fast-element"; import { DesignSystem, DesignSystemRegistrationContext } from "../design-system"; -import type { Registry } from "../di"; +import { DI, Registry } from "../di"; import type { FoundationElement, FoundationElementDefinition, @@ -155,16 +155,14 @@ export async function fixture( if (Array.isArray(templateNameOrRegistry)) { const first = templateNameOrRegistry[0]; - const container = (options.designSystem || new DesignSystem()) - .register(templateNameOrRegistry) - .applyTo(parent); - + (options.designSystem || DesignSystem.getOrCreate(parent)).register( + templateNameOrRegistry + ); + const container = DI.getOrCreateDOMContainer(parent); const context = container.get(DesignSystemRegistrationContext); const elementName = `${context.elementPrefix}-${first.definition.baseName}`; const html = `<${elementName}>`; templateNameOrRegistry = new ViewTemplate(html, []); - } else if (options.designSystem) { - options.designSystem.applyTo(parent); } const view = templateNameOrRegistry.create();