diff --git a/change/@microsoft-fast-components-0f5602ef-a1c2-4f35-83a0-3e883906645e.json b/change/@microsoft-fast-components-0f5602ef-a1c2-4f35-83a0-3e883906645e.json new file mode 100644 index 00000000000..cd5498091d8 --- /dev/null +++ b/change/@microsoft-fast-components-0f5602ef-a1c2-4f35-83a0-3e883906645e.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "fix: update components to new CSSDirective API", + "packageName": "@microsoft/fast-components", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@microsoft-fast-element-e7b9adf7-1b40-4e7e-896f-c29600f26654.json b/change/@microsoft-fast-element-e7b9adf7-1b40-4e7e-896f-c29600f26654.json new file mode 100644 index 00000000000..009620dbe59 --- /dev/null +++ b/change/@microsoft-fast-element-e7b9adf7-1b40-4e7e-896f-c29600f26654.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "feat: new CSSDirective design", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@microsoft-fast-foundation-b8a22db2-e6e3-4ba0-a9e1-725a4ef68eda.json b/change/@microsoft-fast-foundation-b8a22db2-e6e3-4ba0-a9e1-725a4ef68eda.json new file mode 100644 index 00000000000..3251b611cb7 --- /dev/null +++ b/change/@microsoft-fast-foundation-b8a22db2-e6e3-4ba0-a9e1-725a4ef68eda.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "fix: update foundation to new CSSDirective API", + "packageName": "@microsoft/fast-foundation", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index d8ef1f1174a..bd7eb0c6006 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -11,6 +11,9 @@ export interface Accessor { setValue(source: any, value: any): void; } +// @public +export type AddBehavior = (behavior: Behavior) => void; + // @public export type AddViewBehaviorFactory = (factory: ViewBehaviorFactory) => string; @@ -227,16 +230,35 @@ export class Controller extends Prop export function createTypeRegistry(): TypeRegistry; // @public -export function css(strings: TemplateStringsArray, ...values: (ComposableStyles | CSSDirective)[]): ElementStyles; +export const css: CSSTemplateTag; // @public -export class CSSDirective { - createBehavior(): Behavior | undefined; - createCSS(): ComposableStyles; +export interface CSSDirective { + createCSS(add: AddBehavior): ComposableStyles; } // @public -export function cssPartial(strings: TemplateStringsArray, ...values: (ComposableStyles | CSSDirective)[]): CSSDirective; +export const CSSDirective: Readonly<{ + getForInstance: (object: any) => CSSDirectiveDefinition> | undefined; + getByType: (key: Function) => CSSDirectiveDefinition> | undefined; + define>(type: any): TType; +}>; + +// @public +export function cssDirective(): (type: Constructable) => void; + +// @public +export interface CSSDirectiveDefinition = Constructable> { + readonly type: TType; +} + +// @public @deprecated (undocumented) +export const cssPartial: (strings: TemplateStringsArray, ...values: (ComposableStyles | CSSDirective)[]) => CSSDirective; + +// @public +export type CSSTemplateTag = ((strings: TemplateStringsArray, ...values: (ComposableStyles | CSSDirective)[]) => ElementStyles) & { + partial(strings: TemplateStringsArray, ...values: (ComposableStyles | CSSDirective)[]): CSSDirective; +}; // @public export function customElement(nameOrDef: string | PartialFASTElementDefinition): (type: Constructable) => void; diff --git a/packages/web-components/fast-element/src/styles/css-directive.ts b/packages/web-components/fast-element/src/styles/css-directive.ts index 0e51a85c995..74f26c4586c 100644 --- a/packages/web-components/fast-element/src/styles/css-directive.ts +++ b/packages/web-components/fast-element/src/styles/css-directive.ts @@ -1,25 +1,63 @@ +import type { Constructable } from "../interfaces.js"; import type { Behavior } from "../observation/behavior.js"; +import { createTypeRegistry } from "../platform.js"; import type { ComposableStyles } from "./element-styles.js"; +/** + * Used to add behaviors when constructing styles. + * @public + */ +export type AddBehavior = (behavior: Behavior) => void; + /** * Directive for use in {@link css}. * * @public */ -export class CSSDirective { +export interface CSSDirective { /** * Creates a CSS fragment to interpolate into the CSS document. * @returns - the string to interpolate into CSS */ - public createCSS(): ComposableStyles { - return ""; - } + createCSS(add: AddBehavior): ComposableStyles; +} +/** + * Defines metadata for a CSSDirective. + * @public + */ +export interface CSSDirectiveDefinition< + TType extends Constructable = Constructable +> { /** - * Creates a behavior to bind to the host element. - * @returns - the behavior to bind to the host element, or undefined. + * The type that the definition provides metadata for. */ - public createBehavior(): Behavior | undefined { - return undefined; - } + readonly type: TType; +} + +const registry = createTypeRegistry(); + +/** + * Instructs the css engine to provide dynamic styles or + * associate behaviors with styles. + * @public + */ +export const CSSDirective = Object.freeze({ + getForInstance: registry.getForInstance, + getByType: registry.getByType, + define>(type): TType { + registry.register({ type }); + return type; + }, +}); + +/** + * Decorator: Defines a CSSDirective. + * @public + */ +export function cssDirective() { + /* eslint-disable-next-line @typescript-eslint/explicit-function-return-type */ + return function (type: Constructable) { + CSSDirective.define(type); + }; } diff --git a/packages/web-components/fast-element/src/styles/css.ts b/packages/web-components/fast-element/src/styles/css.ts index 73caf675903..a9477776eb0 100644 --- a/packages/web-components/fast-element/src/styles/css.ts +++ b/packages/web-components/fast-element/src/styles/css.ts @@ -1,7 +1,7 @@ import type { FASTElement } from "../components/fast-element.js"; import { isString } from "../interfaces.js"; import type { Behavior } from "../observation/behavior.js"; -import { CSSDirective } from "./css-directive.js"; +import { AddBehavior, CSSDirective } from "./css-directive.js"; import { ComposableStyles, ElementStyles } from "./element-styles.js"; function collectStyles( @@ -11,18 +11,16 @@ function collectStyles( const styles: ComposableStyles[] = []; let cssString = ""; const behaviors: Behavior[] = []; + const add = (behavior: Behavior): void => { + behaviors.push(behavior); + }; for (let i = 0, ii = strings.length - 1; i < ii; ++i) { cssString += strings[i]; let value = values[i]; - if (value instanceof CSSDirective) { - const behavior = value.createBehavior(); - value = value.createCSS(); - - if (behavior) { - behaviors.push(behavior); - } + if (CSSDirective.getForInstance(value) !== void 0) { + value = (value as CSSDirective).createCSS(add); } if (value instanceof ElementStyles || value instanceof CSSStyleSheet) { @@ -55,23 +53,47 @@ function collectStyles( * @param values - The values that are interpolated with the string fragments. * @remarks * The css helper supports interpolation of strings and ElementStyle instances. + * Use the .partial method to create partial CSS fragments. * @public */ -export function css( +export type CSSTemplateTag = (( strings: TemplateStringsArray, ...values: (ComposableStyles | CSSDirective)[] -): ElementStyles { +) => ElementStyles) & { + /** + * Transforms a template literal string into partial CSS. + * @param strings - The string fragments that are interpolated with the values. + * @param values - The values that are interpolated with the string fragments. + * @public + */ + partial( + strings: TemplateStringsArray, + ...values: (ComposableStyles | CSSDirective)[] + ): CSSDirective; +}; + +/** + * Transforms a template literal string into styles. + * @param strings - The string fragments that are interpolated with the values. + * @param values - The values that are interpolated with the string fragments. + * @remarks + * The css helper supports interpolation of strings and ElementStyle instances. + * @public + */ +export const css: CSSTemplateTag = (( + strings: TemplateStringsArray, + ...values: (ComposableStyles | CSSDirective)[] +): ElementStyles => { const { styles, behaviors } = collectStyles(strings, values); const elementStyles = new ElementStyles(styles); return behaviors.length ? elementStyles.withBehaviors(...behaviors) : elementStyles; -} +}) as any; -class CSSPartial extends CSSDirective implements Behavior { +class CSSPartial implements CSSDirective, Behavior { private css: string = ""; private styles?: ElementStyles; - constructor(styles: ComposableStyles[], private behaviors: Behavior[]) { - super(); + constructor(styles: ComposableStyles[], private behaviors: Behavior[]) { const stylesheets: ReadonlyArray { } else { accumulated.push(current); } + return accumulated; }, [] @@ -95,45 +118,37 @@ class CSSPartial extends CSSDirective implements Behavior { } } - createBehavior(): Behavior { - return this; - } + createCSS(add: AddBehavior): string { + this.behaviors.forEach(add); + + if (this.styles) { + add(this); + } - createCSS(): string { return this.css; } bind(el: FASTElement): void { - if (this.styles) { - el.$fastController.addStyles(this.styles); - } - - if (this.behaviors.length) { - el.$fastController.addBehaviors(this.behaviors); - } + el.$fastController.addStyles(this.styles); } unbind(el: FASTElement): void { - if (this.styles) { - el.$fastController.removeStyles(this.styles); - } - - if (this.behaviors.length) { - el.$fastController.removeBehaviors(this.behaviors); - } + el.$fastController.removeStyles(this.styles); } } -/** - * Transforms a template literal string into partial CSS. - * @param strings - The string fragments that are interpolated with the values. - * @param values - The values that are interpolated with the string fragments. - * @public - */ -export function cssPartial( +CSSDirective.define(CSSPartial); + +css.partial = ( strings: TemplateStringsArray, ...values: (ComposableStyles | CSSDirective)[] -): CSSDirective { +): CSSDirective => { const { styles, behaviors } = collectStyles(strings, values); return new CSSPartial(styles, behaviors); -} +}; + +/** + * @deprecated Use css.partial instead. + * @public + */ +export const cssPartial = css.partial; diff --git a/packages/web-components/fast-element/src/styles/styles.spec.ts b/packages/web-components/fast-element/src/styles/styles.spec.ts index 634a166a247..ece6fdbe0f8 100644 --- a/packages/web-components/fast-element/src/styles/styles.spec.ts +++ b/packages/web-components/fast-element/src/styles/styles.spec.ts @@ -1,13 +1,13 @@ import { expect } from "chai"; import { AdoptedStyleSheetsStrategy, + ComposableStyles, ElementStyles, } from "./element-styles"; import { DOM } from "../dom"; -import { CSSDirective } from "./css-directive"; -import { css, cssPartial } from "./css"; +import { AddBehavior, cssDirective, CSSDirective } from "./css-directive"; +import { css } from "./css"; import type { Behavior } from "../observation/behavior"; -import type { FASTElement } from ".."; import { StyleElementStrategy } from "../polyfills"; import type { StyleTarget } from "../interfaces"; import { ExecutionContext } from "../observation/observable"; @@ -242,7 +242,8 @@ describe("css", () => { describe("with a CSSDirective", () => { describe("should interpolate the product of CSSDirective.createCSS() into the resulting ElementStyles CSS", () => { it("when the result is a string", () => { - class Directive extends CSSDirective { + @cssDirective() + class Directive implements CSSDirective { createCSS() { return "red"; } @@ -254,7 +255,9 @@ describe("css", () => { it("when the result is an ElementStyles", () => { const _styles = css`:host{color: red}` - class Directive extends CSSDirective { + + @cssDirective() + class Directive implements CSSDirective { createCSS() { return _styles; } @@ -267,7 +270,9 @@ describe("css", () => { if (DOM.supportsAdoptedStyleSheets) { it("when the result is a CSSStyleSheet", () => { const _styles = new CSSStyleSheet(); - class Directive extends CSSDirective { + + @cssDirective() + class Directive implements CSSDirective { createCSS() { return _styles; } @@ -286,9 +291,11 @@ describe("css", () => { unbind(){} } - class Directive extends CSSDirective { - createBehavior() { - return behavior; + @cssDirective() + class Directive implements CSSDirective { + createCSS(add: AddBehavior): ComposableStyles { + add(behavior); + return ""; } } @@ -301,16 +308,18 @@ describe("css", () => { describe("cssPartial", () => { it("should have a createCSS method that is the CSS string interpolated with the createCSS product of any CSSDirectives", () => { - class myDirective extends CSSDirective { + const add = () => void 0; + + @cssDirective() + class myDirective implements CSSDirective { createCSS() { return "red" }; - createBehavior() { return undefined; } } - const partial = cssPartial`color: ${new myDirective}`; - expect (partial.createCSS()).to.equal("color: red"); + const partial = css.partial`color: ${new myDirective}`; + expect (partial.createCSS(add)).to.equal("color: red"); }); - it("Should add behaviors from interpolated CSS directives when bound to an element", () => { + it("Should add behaviors from interpolated CSS directives", () => { const behavior = { bind() {}, unbind() {}, @@ -318,31 +327,35 @@ describe("cssPartial", () => { const behavior2 = {...behavior}; - class directive extends CSSDirective { - createCSS() { return "" }; - createBehavior() { return behavior; } + @cssDirective() + class directive implements CSSDirective { + createCSS(add: AddBehavior) { + add(behavior); + return "" + }; } - class directive2 extends CSSDirective { - createCSS() { return "" }; - createBehavior() { return behavior2; } + + @cssDirective() + class directive2 implements CSSDirective { + createCSS(add: AddBehavior) { + add(behavior2); + return "" + }; } - const partial = cssPartial`${new directive}${new directive2}`; - const el = { - $fastController: { - addBehaviors(behaviors: Behavior[]) { - expect(behaviors[0]).to.equal(behavior); - expect(behaviors[1]).to.equal(behavior2); - } - } - } as FASTElement; + const partial = css.partial`${new directive}${new directive2}`; + const behaviors: Behavior[] = []; + const add = (x: Behavior) => behaviors.push(x); + + partial.createCSS(add); - partial.createBehavior()?.bind(el, ExecutionContext.default) + expect(behaviors[0]).to.equal(behavior); + expect(behaviors[1]).to.equal(behavior2); }); it("should add any ElementStyles interpolated into the template function when bound to an element", () => { const styles = css`:host {color: blue; }`; - const partial = cssPartial`${styles}`; + const partial = css.partial`${styles}`; let called = false; const el = { $fastController: { @@ -351,9 +364,15 @@ describe("cssPartial", () => { called = true; } } - } as FASTElement; + }; + + const behaviors: Behavior[] = []; + const add = (x: Behavior) => behaviors.push(x); + partial.createCSS(add); + + expect(behaviors[0]).to.equal(partial); - partial.createBehavior()?.bind(el, ExecutionContext.default) + (partial as any as Behavior).bind(el, ExecutionContext.default); expect(called).to.be.true; }) 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 db91dd94503..ebb904cda5f 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 @@ -4,7 +4,6 @@ import { DesignSystem } from "../design-system"; import { uniqueElementName } from "../test-utilities/fixture"; import { FoundationElement } from "../foundation-element"; import { CSSDesignToken, DesignToken, DesignTokenChangeRecord, DesignTokenSubscriber } from "./design-token"; -import { defaultElement } from "./custom-property-manager"; import spies from "chai-spies"; chia.use(spies); @@ -54,7 +53,8 @@ describe("A DesignToken", () => { describe("that is a CSSDesignToken", () => { it("should have a createCSS() method that returns a string with the name property formatted as a CSS variable", () => { - expect(DesignToken.create("implicit").createCSS()).to.equal("var(--implicit)"); + const add = () => void 0; + expect(DesignToken.create("implicit").createCSS(add)).to.equal("var(--implicit)"); }); it("should have a readonly cssCustomProperty property that is the name formatted as a CSS custom property", () => { expect(DesignToken.create("implicit").cssCustomProperty).to.equal("--implicit"); diff --git a/packages/web-components/fast-foundation/src/design-token/design-token.ts b/packages/web-components/fast-foundation/src/design-token/design-token.ts index 9dbbe651091..0a2c562062a 100644 --- a/packages/web-components/fast-foundation/src/design-token/design-token.ts +++ b/packages/web-components/fast-foundation/src/design-token/design-token.ts @@ -128,8 +128,8 @@ export interface DesignTokenSubscriber> { /** * Implementation of {@link (DesignToken:interface)} */ -class DesignTokenImpl extends CSSDirective - implements DesignToken { +class DesignTokenImpl + implements CSSDirective, DesignToken { public readonly name: string; public readonly cssCustomProperty: string | undefined; public readonly id: string; @@ -201,8 +201,6 @@ class DesignTokenImpl extends CSSDirective } constructor(configuration: Required) { - super(); - this.name = configuration.name; if (configuration.cssCustomPropertyName !== null) { @@ -255,7 +253,6 @@ class DesignTokenImpl extends CSSDirective public withDefault(value: DesignTokenValue | DesignToken) { this.setValueFor(defaultElement, value); - return this; }