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 dc5b6a35e00..fa8c2e1353f 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 a049f5ba13b..884e9709f4e 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 { CSSDesignToken, DesignToken, DesignTokenChangeRecord, DesignTokenSubscriber } 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();