fast icon indicating copy to clipboard operation
fast copied to clipboard

feat: add Layer behaviors, custom element(s), and CSSDirectives

Open nicholasrice opened this issue 4 years ago • 3 comments

🙋 Feature Request

Working with a colleague, I discovered there is a gap in the existing color system implementation in that there isn't an easily usable mechanism to designate a node tree as being a Layer (setting the DesignToken appropriately and applying the appropriate CSS). In trying to create a region with Layer1 styling in a React app, the easiest solution was to create an app-specific custom element that would set the fillColor DesignToken to the neutralLayer1 token, and apply the fill-color as the background.

While I don't think the approach taken was wrong, I do feel the library should provide better mechanisms to achieve these goals out-of-box. I'd like to propose several new Behaviors, a single (or multiple) custom elements, and potentially some CSSDirective's to bridge this gap.

Behaviors

We can expose Layer functionality through a set of very simple behaviors and an ElementStyles instance. For example,

// Use `fill-color` as background, set by behavior. Also use color
const layerStyles = css`
    :host {
        background: ${fillColor};
        color: ${neutralFillRest};
    }
`

// Behavior that, on bind, sets the fillColor for the bound element to the provided layer token
class LayerBehavior implements Behavior {
    constructor(public readonly recipe: DesignToken<Swatch>) {}
    bind(element: HTMLElement & FASTElement) {
        element.$fastController.addStyles(layerStyles);
        fillColor.setValueFor(element, this.recipe);
    };

    unbind(element: HTMLElement) {}
}

// re-usable behavior instances for each layer recipe
export const neutralLayer1Behavior = new LayerBehavior(neutralLayer1);
export const neutralLayer2Behavior = new LayerBehavior(neutralLayer2);
export const neutralLayer3Behavior = new LayerBehavior(neutralLayer3);
export const neutralLayer4Behavior = new LayerBehavior(neutralLayer4);
export const neutralLayerCardContainerBehavior = new LayerBehavior(neutralLayerCardContainer);

These behaviors alone would provide authors everything they need to convert their own FAST element to a Layer:

class MyElement extends FASTElement {
    constructor() {
        super();
        this.$fastController.addBehaviors([neutralLayer1Behavior]);
    }
}
<my-element>I have layer styling!</my-element>

Layer Custom Element

With these behaviors it would be trivial to expose a custom element from @microsoft/fast-components that leverages these behaviors from a simple declarative API:

export class Layer extends FoundationElement {
    @attr value: "1" | "2" | "3" | "4" | "card-container" = "1";
    valueChanged() {
        this.attached =
            this.value === "card-container"
                ? neutralLayerCardContainerBehavior
                : this.value === "4"
                ? neutralLayer4Behavior
                : this.value === "3"
                ? neutralLayer3Behavior
                : this.value === "2"
                ? neutralLayer2Behavior
                : neutralLayer1Behavior;
    }

    @observable
    private attached: Behavior;
    attachedChanged(prev: Behavior | undefined, next: Behavior) {
        if (prev) {
            this.$fastController.removeBehaviors([prev]);
        }

        this.$fastController.addBehaviors([next]);
    }
}

export const fastLayer = Layer.compose({
    baseName: "layer",
    template: html`
        <slot></slot>
    `,
    styles: css`
        ${display("block")} 
    `,
});
<fast-layer value="card-container"><!-- cards --></fast-layer>

In CSSDirectives

These behaviors could also be leveraged by CSSDirectives so that this behavior could be embedded into a stylesheet instead of a custom element class (this is all about visual styling, after all):

class LayerCSSDirective extends CSSDirective {
    constructor(public readonly layerBehavior: Behavior) 
    public createCSS() { return fillColor.createCSS(); }
    public createBehavior() { return this.layerBehavior; }
}

const layer1CSSDirective = new LayerCSSDirective(layer1Behavior);

const styles = css`
    :host { background: ${layer1CSSDirective}; }
`

I think together these tools would give authors better tools for working within the FAST color system.

nicholasrice avatar Oct 14 '21 23:10 nicholasrice

I think this plan sounds solid. I wonder if as part of this we should restructure the way the color layer recipes work. The initial named set was based on previous Fluent Design, with the intent of maxing out at 4. The updated design doesn't go that dark in most cases anymore, but we've found experiences that actually needed to go darker. Ultimately there is a delta representing the offset between layers, so perhaps it would make more sense to remove the limitations and calculate it based on an arbitrary layer index or relative to another layer. The latter is what the updated 'fill layer' recipe does actually, moving 'up' anyway.

We may be able to optimize the way 'card container' works as well, especially for the open source aspect of the design system for non-Fluent. I was able to simplify that for Fluent because of the way the layers have been flattened out. Probably more complicated to keep around especially if we had a relative way of setting it. (That is, the whole point was to make it one level darker than 'card' which was only necessary in light mode because 'card' couldn't go any lighter than white.)

bheston avatar Nov 10 '21 00:11 bheston

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Apr 16 '22 18:04 stale[bot]

This is still needed as part of the latest design package update.

bheston avatar Apr 18 '22 17:04 bheston