Skip to content

Commit

Permalink
feat: adds DesignToken root configurability (microsoft#5284)
Browse files Browse the repository at this point in the history
* wire API to configure design token root

* remove test from rollup

* adding tests

* adding deregister test

* clean up tests

* adding deregister documentation

* enable tests

* adding await to before/after each

* replace missing test case

* rename mderegister methods, update docs, and add opt-out feature to DesignSystem to prevent DesignToken registration

* Change files

* fix ae docs

* refactor to remove hard reference to document

Co-authored-by: nicholasrice <nicholasrice@users.noreply.github.com>
  • Loading branch information
nicholasrice and nicholasrice authored Oct 14, 2021
1 parent 919de55 commit 5cc7366
Show file tree
Hide file tree
Showing 8 changed files with 326 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Implement DesignToken root-element registration for CSS custom property emission of default token values",
"packageName": "@microsoft/fast-foundation",
"email": "nicholasrice@users.noreply.github.com",
"dependentChangeType": "patch"
}
3 changes: 3 additions & 0 deletions packages/web-components/fast-foundation/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,7 @@ export type DerivedDesignTokenValue<T> = T extends Function ? never : (target: H
// @public
export interface DesignSystem {
register(...params: any[]): DesignSystem;
withDesignTokenRoot(root: HTMLElement | Document | null): DesignSystem;
withElementDisambiguation(callback: ElementDisambiguationCallback): DesignSystem;
withPrefix(prefix: string): DesignSystem;
withShadowRootMode(mode: ShadowRootMode): DesignSystem;
Expand Down Expand Up @@ -871,6 +872,8 @@ export const DesignToken: Readonly<{
create: typeof create;
notifyConnection(element: HTMLElement): boolean;
notifyDisconnection(element: HTMLElement): boolean;
registerRoot(target?: HTMLElement | Document): void;
unregisterRoot(target?: HTMLElement | Document): void;
}>;

// @public
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { Constructable } from "@microsoft/fast-element";
import { Constructable, DOM } from "@microsoft/fast-element";
import { expect } from "chai";
import { FoundationElement } from "..";
import { Container, DI, Registration } from "../di";
import { uniqueElementName } from "../test-utilities/fixture";
import { DesignSystem, ElementDisambiguation } from "./design-system";
import type { DesignSystemRegistrationContext } from "./registration-context";
import { DesignToken } from "../design-token/design-token";

describe("DesignSystem", () => {
it("Should return the same instance for the same element", () => {
Expand Down Expand Up @@ -382,4 +383,57 @@ describe("DesignSystem", () => {

expect(found).to.be.instanceOf(AltTest);
});

it("should set the DesignToken root to the default root when register is invoked", async () => {
const token = DesignToken.create<number>("design-system-registration").withDefault(12);
const host = document.createElement("div");
expect(window.getComputedStyle(document.body).getPropertyValue(token.cssCustomProperty)).to.equal("");

DesignSystem.getOrCreate(host)
.register({
register(container: Container, context: DesignSystemRegistrationContext) {}
});


await DOM.nextUpdate();
expect(window.getComputedStyle(document.body).getPropertyValue(token.cssCustomProperty)).to.equal("12");

DesignToken.unregisterRoot();
await DOM.nextUpdate();
});

it("should provide a way to specify the DesignToken root", async () => {
const token = DesignToken.create<number>("custom-design-system-registration").withDefault(12);
const host = document.createElement("div");
expect(window.getComputedStyle(document.body).getPropertyValue(token.cssCustomProperty)).to.equal("");

DesignSystem.getOrCreate(host)
.withDesignTokenRoot(host)
.register({
register(container: Container, context: DesignSystemRegistrationContext) {}
});


await DOM.nextUpdate();
const value = host.style.getPropertyValue(token.cssCustomProperty)
expect(value).to.equal("12");
expect(window.getComputedStyle(document.body).getPropertyValue(token.cssCustomProperty)).to.equal("");
DesignToken.unregisterRoot(host);
await DOM.nextUpdate();
expect(window.getComputedStyle(document.body).getPropertyValue(token.cssCustomProperty)).to.equal("");
});
it("should provide a way to disable DesignToken root registration", async () => {
const token = DesignToken.create<number>("disabled-design-system-registration").withDefault(12);
expect(window.getComputedStyle(document.body).getPropertyValue(token.cssCustomProperty)).to.equal("");

DesignSystem.getOrCreate()
.withDesignTokenRoot(null)
.register({
register(container: Container, context: DesignSystemRegistrationContext) {}
});


await DOM.nextUpdate();
expect(window.getComputedStyle(document.body).getPropertyValue(token.cssCustomProperty)).to.equal("");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
ElementDefinitionContext,
ElementDefinitionParams,
} from "./registration-context";
import { DesignToken } from "../design-token/design-token";

/**
* Indicates what to do with an ambiguous (duplicate) element.
Expand Down Expand Up @@ -90,6 +91,17 @@ export interface DesignSystem {
* @public
*/
withElementDisambiguation(callback: ElementDisambiguationCallback): DesignSystem;

/**
* Overrides the {@link (DesignToken:interface)} root, controlling where
* {@link (DesignToken:interface)} default value CSS custom properties
* are emitted.
*
* Providing `null` disables automatic DesignToken registration.
* @param root - the root to register
* @public
*/
withDesignTokenRoot(root: HTMLElement | Document | null): DesignSystem;
}

let rootDesignSystem: DesignSystem | null = null;
Expand Down Expand Up @@ -187,6 +199,8 @@ function extractTryDefineElementParams(
}

class DefaultDesignSystem implements DesignSystem {
private designTokensInitialized: boolean = false;
private designTokenRoot: HTMLElement | null | undefined;
private prefix: string = "fast";
private shadowRootMode: ShadowRootMode | undefined = undefined;
private disambiguate: ElementDisambiguationCallback = () =>
Expand Down Expand Up @@ -215,6 +229,11 @@ class DefaultDesignSystem implements DesignSystem {
return this;
}

public withDesignTokenRoot(root: HTMLElement | null): DesignSystem {
this.designTokenRoot = root;
return this;
}

public register(...registrations: any[]): DesignSystem {
const container = this.container;
const elementDefinitionEntries: ElementDefinitionEntry[] = [];
Expand Down Expand Up @@ -280,6 +299,14 @@ class DefaultDesignSystem implements DesignSystem {
},
};

if (!this.designTokensInitialized) {
this.designTokensInitialized = true;

if (this.designTokenRoot !== null) {
DesignToken.registerRoot(this.designTokenRoot);
}
}

container.registerWithContext(context, ...registrations);

for (const entry of elementDefinitionEntries) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
FASTElement,
observable,
Observable,
TargetedHTMLDirective,
} from "@microsoft/fast-element";

export const defaultElement = document.createElement("div");
Expand Down Expand Up @@ -157,13 +156,81 @@ class ElementStyleSheetTarget implements PropertyTarget {
setProperty(name: string, value: any) {
DOM.queueUpdate(() => this.target.setProperty(name, value));
}

removeProperty(name: string) {
DOM.queueUpdate(() => this.target.removeProperty(name));
}
}

/**
* Controls emission for default values. This control is capable
* of emitting to multiple {@link PropertyTarget | PropertyTargets},
* and only emits if it has at least one root.
*
* @internal
*/
export class RootStyleSheetTarget implements PropertyTarget {
private static roots = new Set<HTMLElement | Document>();
private static properties: Record<string, string> = {};
public setProperty(name: string, value: any): void {
RootStyleSheetTarget.properties[name] = value;

for (const target of RootStyleSheetTarget.roots.values()) {
PropertyTargetManager.getOrCreate(
RootStyleSheetTarget.normalizeRoot(target)
).setProperty(name, value);
}
}

public removeProperty(name: string): void {
delete RootStyleSheetTarget.properties[name];
for (const target of RootStyleSheetTarget.roots.values()) {
PropertyTargetManager.getOrCreate(
RootStyleSheetTarget.normalizeRoot(target)
).removeProperty(name);
}
}

public static registerRoot(root: HTMLElement | Document) {
const { roots } = RootStyleSheetTarget;
if (!roots.has(root)) {
roots.add(root);
const target = PropertyTargetManager.getOrCreate(this.normalizeRoot(root));
for (const key in RootStyleSheetTarget.properties) {
target.setProperty(key, RootStyleSheetTarget.properties[key]);
}
}
}

public static unregisterRoot(root: HTMLElement | Document) {
const { roots } = RootStyleSheetTarget;
if (roots.has(root)) {
roots.delete(root);

const target = PropertyTargetManager.getOrCreate(
RootStyleSheetTarget.normalizeRoot(root)
);
for (const key in RootStyleSheetTarget.properties) {
target.removeProperty(key);
}
}
}

/**
* Returns the document when provided the default element,
* otherwise is a no-op
* @param root - the root to normalize
*/
private static normalizeRoot(root: HTMLElement | Document) {
return root === defaultElement ? document : root;
}
}

// Caches PropertyTarget instances
const propertyTargetCache: WeakMap<HTMLElement, PropertyTarget> = new WeakMap();
const propertyTargetCache: WeakMap<
HTMLElement | Document,
PropertyTarget
> = new WeakMap();
// Use Constructable StyleSheets for FAST elements when supported, otherwise use
// HTMLStyleElement instances
const propertyTargetCtor: Constructable<PropertyTarget> = DOM.supportsAdoptedStyleSheets
Expand All @@ -176,21 +243,23 @@ const propertyTargetCtor: Constructable<PropertyTarget> = DOM.supportsAdoptedSty
* @internal
*/
export const PropertyTargetManager = Object.freeze({
getOrCreate(source: HTMLElement): PropertyTarget {
getOrCreate(source: HTMLElement | Document): PropertyTarget {
if (propertyTargetCache.has(source)) {
return propertyTargetCache.get(source)!;
}

let target: PropertyTarget;

if (source === defaultElement) {
target = new RootStyleSheetTarget();
} else if (source instanceof Document) {
target = DOM.supportsAdoptedStyleSheets
? new DocumentStyleSheetTarget()
: new HeadStyleElementStyleSheetTarget();
} else if (isFastElement(source)) {
} else if (isFastElement(source as HTMLElement)) {
target = new propertyTargetCtor(source);
} else {
target = new ElementStyleSheetTarget(source);
target = new ElementStyleSheetTarget(source as HTMLElement);
}

propertyTargetCache.set(source, target);
Expand Down
Loading

0 comments on commit 5cc7366

Please sign in to comment.