Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(fast-element1): fix design token updates when attaching/detaching nodes and deleting values #6960

Open
wants to merge 5 commits into
base: archives/fast-element-1
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Fix design token issues involving deleting values and CSS reflection",
"packageName": "@microsoft/fast-foundation",
"email": "7282195+m-akinc@users.noreply.github.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,32 @@ describe("A DesignToken", () => {
removeElement(ancestor);
});

it("should set CSS custom property for element if value stops matching inherited value", async () => {
it("should override inherited CSS custom property from ancestor", async () => {
const ancestor = addElement();
const target = addElement(ancestor);
const token = DesignToken.create<number>("test");
token.setValueFor(ancestor, 12);
token.setValueFor(target, 14);
await DOM.nextUpdate();
expect(window.getComputedStyle(target).getPropertyValue(token.cssCustomProperty)).to.equal('14');
removeElement(ancestor);
});

it("should undefine CSS custom property for element if value deleted", async () => {
const ancestor = addElement();
const target = addElement(ancestor);
const token = DesignToken.create<number>("test");
token.setValueFor(ancestor, 12);
token.setValueFor(target, 14);
await DOM.nextUpdate();
expect(window.getComputedStyle(target).getPropertyValue(token.cssCustomProperty)).to.equal('14');
token.deleteValueFor(target);
await DOM.nextUpdate();
expect(window.getComputedStyle(target).getPropertyValue(token.cssCustomProperty)).to.equal('12');
removeElement(ancestor);
});

it("should set CSS custom property for element if value stops matching inherited value because value changed on ancestor", async () => {
const ancestor = addElement();
const target = addElement(ancestor);
const token = DesignToken.create<number>("test");
Expand All @@ -207,6 +232,56 @@ describe("A DesignToken", () => {
expect(window.getComputedStyle(target).getPropertyValue(token.cssCustomProperty)).to.equal('12');
removeElement(ancestor);
});

it("should set CSS custom property for element if value stops matching inherited value because of reparenting", async () => {
const ancestorA = addElement();
const ancestorB = addElement();
const target = addElement(ancestorA);
const token = DesignToken.create<number>("test");
token.setValueFor(ancestorA, 12);
token.setValueFor(ancestorB, 14);
token.setValueFor(target, 12);
await DOM.nextUpdate();
expect(window.getComputedStyle(target).getPropertyValue(token.cssCustomProperty)).to.equal('12');
ancestorA.removeChild(target);
ancestorB.appendChild(target);
await DOM.nextUpdate();
expect(window.getComputedStyle(target).getPropertyValue(token.cssCustomProperty)).to.equal('12');
removeElement(ancestorA, ancestorB);
});

it("should set CSS custom property for element if matching value is deleted from ancestor", async () => {
const ancestor = addElement();
const target = addElement(ancestor);
const token = DesignToken.create<number>("test");
token.setValueFor(ancestor, 12);
token.setValueFor(target, 12);
await DOM.nextUpdate();
// This is inherited from ancestor's CSS
expect(window.getComputedStyle(target).getPropertyValue(token.cssCustomProperty)).to.equal('12');
token.deleteValueFor(ancestor);
await DOM.nextUpdate();
// This should now come from CSS set directly on target
expect(window.getComputedStyle(target).getPropertyValue(token.cssCustomProperty)).to.equal('12');
removeElement(ancestor);
});

it("should set CSS custom property for element if removed from parent with matching value", async () => {
const ancestor = addElement();
const target = addElement(ancestor);
const token = DesignToken.create<number>("test");
token.setValueFor(ancestor, 12);
token.setValueFor(target, 12);
await DOM.nextUpdate();
// This is inherited from ancestor's CSS
expect(window.getComputedStyle(target).getPropertyValue(token.cssCustomProperty)).to.equal('12');
ancestor.removeChild(target);
document.body.append(target);
await DOM.nextUpdate();
// This should now come from CSS set directly on target
expect(window.getComputedStyle(target).getPropertyValue(token.cssCustomProperty)).to.equal('12');
removeElement(ancestor);
});
});
describe("that is not a CSSDesignToken", () => {
it("should not set a CSS custom property for the element", () => {
Expand Down Expand Up @@ -640,7 +715,7 @@ describe("A DesignToken", () => {
await DOM.nextUpdate();
expect(handleChange).to.have.been.called.once;
});
it("should notify a subscriber for a token after being appended to a parent with a different token value than the previous context", async () => {
it("should notify a subscriber for a derived token after being appended to a parent with a different token value than the previous context", async () => {
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
const tokenA = DesignToken.create<number>("token-a");
const tokenB = DesignToken.create<number>("token-b");

Expand Down Expand Up @@ -678,6 +753,7 @@ describe("A DesignToken", () => {

expect(tokenA.getValueFor(child)).to.equal(6);
tokenA.subscribe(subscriber, child);

expect(handleChange).not.to.have.been.called()
parent.appendChild(child);

Expand Down Expand Up @@ -760,6 +836,11 @@ describe("A DesignToken", () => {
tokenA.deleteValueFor(target);

expect(tokenB.getValueFor(target)).to.equal(14);

// When disconnecting target, it will no longer inherit a value for tokenA,
// and updating the value for tokenB will throw an error. So remove the
// value for tokenB first to avoid the console error.
tokenB.deleteValueFor(target);
removeElement(parent)
})
});
Expand Down Expand Up @@ -859,6 +940,11 @@ describe("A DesignToken", () => {
expect(spy.get(parent)).to.be.false;
expect(spy.get(target)).to.be.false;

// Note that elements do not get subscribed for changes in their parent's values
// until a token value is gotten for/set on that (child) element.
// That is why setting values on ancestors do not trigger value change callbacks
// for the descendants in this test.

token.setValueFor(ancestor, 12);
expect(spy.get(ancestor)).to.be.true;
expect(spy.get(parent)).to.be.false;
Expand All @@ -877,6 +963,37 @@ describe("A DesignToken", () => {
removeElement(ancestor);
});

it("should notify an un-targeted subscriber for each element whose value changes", () => {
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
const ancestor = addElement();
const parent = addElement(ancestor);
const target = addElement(parent);
const token = DesignToken.create<number>("test");
const spy = new Map<HTMLElement, boolean>([[ancestor, false], [parent, false], [ target, false ]]);

// Set/get the values to initialize change notifications for these elements
token.setValueFor(ancestor, 10);
token.getValueFor(parent);
token.getValueFor(target);

const handleChange = chia.spy((record: DesignTokenChangeRecord<typeof token>) => {
spy.set(record.target, true)
});
const subscriber: DesignTokenSubscriber<typeof token> = {
handleChange
}

token.subscribe(subscriber);
expect(handleChange).to.not.have.been.called;

token.setValueFor(parent, 14);
expect(spy.get(ancestor)).to.be.false;
expect(spy.get(parent)).to.be.true;
expect(spy.get(target)).to.be.true;
expect(handleChange).to.have.been.called.twice;

removeElement(ancestor);
});

it("should notify a target-subscriber if the value is changed for the provided target", () => {
const parent = addElement();
const target = addElement(parent);
Expand All @@ -887,6 +1004,8 @@ describe("A DesignToken", () => {
handleChange
}

// A side effect of subscribing for a specific target is that the target gets initialized
// for change notifications, so when we set a value on the parent, it will notify for the target.
token.subscribe(subscriber, target);

token.setValueFor(parent, 12);
Expand Down Expand Up @@ -918,6 +1037,38 @@ describe("A DesignToken", () => {
removeElement(target);
});

it("should notify a subscriber when the value is deleted for an element", () => {
const ancestor = addElement();
const parent = addElement(ancestor);
const target = addElement(parent);
const token = DesignToken.create<number>("test");
const spy = new Map<HTMLElement, boolean>([[ancestor, false], [parent, false], [ target, false ]]);

const handleChange = chia.spy((record: DesignTokenChangeRecord<typeof token>) => {
spy.set(record.target, true)
});
const subscriber: DesignTokenSubscriber<typeof token> = {
handleChange
}

// Set/get the values to initialize change notifications for these elements
token.setValueFor(ancestor, 10);
token.setValueFor(parent, 12);
token.getValueFor(target);

token.subscribe(subscriber);

expect(handleChange).to.not.have.been.called;

token.deleteValueFor(parent);
expect(spy.get(ancestor)).to.be.false;
expect(spy.get(parent)).to.be.true;
expect(spy.get(target)).to.be.true;
expect(handleChange).to.have.been.called.twice;

removeElement(ancestor);
});

it("should infer DesignToken and CSSDesignToken token types on subscription record", () => {
type AssertCSSDesignToken<T> = T extends CSSDesignToken<any> ? T : never;
DesignToken.create<number>("css").subscribe({handleChange(record) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,14 +386,18 @@ class DesignTokenBindingObserver<T extends { createCSS?(): string }> {
* @internal
*/
public handleChange() {
this.node.store.set(
this.token,

(this.observer.observe(
this.node.target,
defaultExecutionContext
) as unknown) as StaticDesignTokenValue<T>
);
try {
this.node.store.set(
this.token,

(this.observer.observe(
this.node.target,
defaultExecutionContext
) as unknown) as StaticDesignTokenValue<T>
);
} catch (e) {
console.error(e);
}
}
}

Expand Down Expand Up @@ -421,6 +425,7 @@ class Store {

delete<T extends { createCSS?(): string }>(token: DesignTokenImpl<T>) {
this.values.delete(token);
Observable.getNotifier(this).notify(token.id);
}

all() {
Expand Down Expand Up @@ -724,6 +729,8 @@ class DesignTokenNode implements Behavior, Subscriber {
token,
this.bindingObservers.has(token) ? this.getRaw(token) : value
);
// Need to stop reflecting any tokens that can now be inherited
child.updateCSSTokenReflection(child.store, token);
}
}

Expand All @@ -739,7 +746,18 @@ class DesignTokenNode implements Behavior, Subscriber {
}

Observable.getNotifier(this.store).unsubscribe(child);
return child.parent === this ? childToParent.delete(child) : false;

if (child.parent !== this) {
return false;
}
const deleted = childToParent.delete(child);

for (const [token] of this.store.all()) {
child.hydrate(token, child.getRaw(token));
// Need to start reflecting any assigned values that were previously inherited
child.updateCSSTokenReflection(child.store, token);
}
return deleted;
}

/**
Expand Down
Loading