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

Tokens: New Experimental Feature #7552

Merged
merged 40 commits into from
Jan 15, 2019
Merged

Tokens: New Experimental Feature #7552

merged 40 commits into from
Jan 15, 2019

Conversation

JasonGore
Copy link
Member

@JasonGore JasonGore commented Jan 8, 2019

Pull request checklist

Overview

This PR introduces tokens and incorporates the evaluation of tokens and styles with slots while avoiding unnecessary styles preprocessing in parent component construction.

This PR is beefy. I went into this thinking it'd be roughly the same size as the Slots PR but it ended up being significantly bigger for the following reasons:

  • Avoiding unnecessary styles processing for slots requires fundamentally changing Foundation implementation.
  • Changing Foundation implementation requires converting all existing Foundation components.
    • Accordion
    • Button
    • CollapsibleSection
    • Persona
    • PersonaCoin
    • Stack
    • Text
    • Toggle
  • Many more edge cases and scenarios to test out with intrinsic elements, OUFR components and Foundations components being used with slots.

What are Tokens?

Tokens are similar to styles in that they affect styling of a component. The difference is that tokens are props "knobs" that can be more simply used to affect the styling of the component.

Tokens are presented as a tokens prop in a component's prop interface. For example, button's tokens appear in types as follows:

export interface IButtonTokens {
  color?: string;
  colorHovered?: string;
  colorPressed?: string;
  iconColor?: string;
  iconColorHovered?: string;
  iconColorPressed?: string;
  ...
}

And then via use of IStyleableComponentProps, tokens prop is automatically added to the props interface via IButtonTokens type argument.

export interface IButtonProps extends IStyleableComponentProps<IButtonProps, IButtonStyles, IButtonTokens> {
  ...
}

And IStyleableComponentProps defines tokens as:

  tokens?: ITokenFunctionOrObject<TViewProps, TTokens>;

So for example where styles would have to be used to do Button styling:

<Button
  styles={{
    icon: {
      fontSize: 'large',
      color: 'black',
      fill: 'black'
    },
    menuIcon: {
      color: 'black'
    },
  }}
/>

With tokens it's more simply:

<Button
  tokens={{
    iconSize: 'large',
    iconColor: 'black'
  }}
/>

Tokens are very similar to styles in that they can either be objects or functions that take in props and theme as arguments:

const enabledTokens: IButtonComponent['tokens'] = (props, theme) => {
  const { semanticColors } = theme;
  return {
    iconColor: semanticColors.buttonText,
    iconColorHovered: semanticColors.buttonTextHovered,
    iconColorPressed: semanticColors.buttonTextPressed,
    ...
  };
};

Why Tokens?

Using styles props requires knowledge of component styling implementation, such as knowledge of all of the style sections and their specific styling code. This approach tends to be fragile, breaking easily whenever the styling of the component is modified.

Tokens are intended to be black box props "knobs" that devs can use at a higher level. The component is responsible for mapping their token specification to component styling. A component's style sections and implementation can change without breaking or regressing existing token usage.

Tokens vs. Styles

Our goal is for tokens to be the primary method for component consumers to style and theme components with the styles prop being used as a fallback for niche scenarios.

Tokens, however, are a full complement to styling and both can be used side-by-side very easily. Tokens add another stage to the styling pipeline, converting props input into styling output.

Props => Tokens processing => Styles processing => classname generation

Notable PR Items:

Tokens Naming (vs. styleVariables / styleVars)

  • Token:
    • Pros
      • New concept implying usage across platforms.
      • Micah likes.
    • Cons:
      • Does not have style naming, is closely related to "styles" but name does not imply it.
      • Does not seem to match "standard" def, which seems to imply tokens are more like semanticColors.
  • StyleVars:
    • Pros
      • Implies association with existing "styles", which is true because both complement each other.
      • Consistent with stardust.
    • Cons:
      • Micah no likes.

Foundation Changes

Existing Foundation preprocesses styling for all of its child components on render by using mergeStyleSets. However, with the addition of slots, component consumers now have access to every slot's component styles. This leads to extra style processing because slot component styles have to be merged again as slots are rendered.

This PR substantially changes the styling approach made by createComponent. createComponent now only merges styles targeted to its styles prop and does not preprocess any styling targeted towards slotted components. The slots utilities have been modified to call mergeStyles as slots are rendered, taking into account any styles props passed directly to the slot by the component consumer.

For now, this new createComponent resides only in experiments package, but is intended to fully replace Foundation after this PR has been reviewed and tested.

Slots and Styles

Slots and Styles are now bound concepts. The approach is that any part of a component that can be styled should also be a slot. Styles interfaces can simply be defined as IComponentStyles<ISlotProps>.

If devs don't want to use slots but want to have style sections, they can still use existing styled approach with use of getClassNames / classNamesFunction.

Example definition:

export interface ICollapsibleSectionSlots {
  root?: IHTMLSlot;
  title?: ICollapsibleSectionTitleSlot;
  body?: IHTMLSlot;
}

export type ICollapsibleSectionStyles = IComponentStyles<ICollapsibleSectionSlots>;

Styles Function Signatures Changed

Theme is now broken out as a separate argument for styles functions as opposed to being a props mixin. This is also consistent with tokens functions.

Token Function/Object Signature

Tokens now can be functions or objects similar to styles.

Intrinsic Slots vs. Component Slots

Similar to existing styled components, all slots / style sections of a component can be styled through its styles prop, regardless of the type of element contained in the slot.

However, now that styles props of all slotted components are also exposed, there are additional considerations to give to styling behavior. Slots currently are defined as either an intrinsic element or a Fabric component. This means styles props can't be passed directly to intrinsic element slots as this will generate a TS error. (The key difference here is the styles prop of the slotted component as opposed to the styles prop of the component containing the slots.)

For example, for an intrinsic root slot that is a div by default:

<Button
  styles={{ root: { color: 'blue' } }} // This works fine, even when root is a div
/>

<Button
  root={{ styles: { color: 'blue' } }} // This generates a TS error because styles is not a valid prop for a div
/>

If we want to allow a given slot to be both an intrinsic element and a Fabric component, we'll lose some type safety and Foundation will have to be modified to process tokens and styles before they are passed on to intrinsic elements. For now, devs that want to directly interact with an intrinsic slot's can use className and mergeStyles as similar to how they are already used for styled components.

Implicit Props

styles, tokens, theme, className are implicitly made available via IStyleableComponentProps, which component props can optionally extend. This means devs don't have to explicitly add any of these props to their component's prop interfaces.

Nested Shorthand Works (!)

// Nested shorthand from CollapsibleSection -> CollapsibleSectionTitle -> title text

export interface ICollapsibleSectionSlots {
  title?: ICollapsibleSectionTitleSlot;
}

export type ICollapsibleSectionTitleSlot = ISlotProp<ICollapsibleSectionTitleProps, 'text'>;

which allows for:

  <CollapsibleSection title="Shorthand Title" >

instead of having to do:

  <CollapsibleSection title={{ text: "Shorthand Title" }} >

TODOs (within PR)

  • Resolve TODOs in code
  • Documentation cleanup

TODOs (after this PR)

  • All existing experiments components (besides Button) have been converted to slots, but have not been "tokenified".
    • All of these components need a pass to see whether or not they should have tokens defined for them.
  • More impressive / demonstrable / practical examples showing power of slots and tokens
    • CollapsibleSection
    • Toggle
Microsoft Reviewers: Open in CodeFlow

fontSize: '20px',
boxSizing: 'border-box',
width: '2.2em',
height: '1em',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not sure why these EM values are here. Not a change for this pr but something maybe we can look into.

@dzearing
Copy link
Member

This is awesome!!!

@dzearing dzearing merged commit 1a3f537 into microsoft:master Jan 15, 2019
@micahgodbolt
Copy link
Member

WOOT!

@Markionium
Copy link
Member

Great to see this go in @JasonGore ! :D

@msft-github-bot
Copy link
Contributor

🎉@uifabric/[email protected] has been released which incorporates this pull request.:tada:

Handy links:

@msft-github-bot
Copy link
Contributor

🎉@uifabric/[email protected] has been released which incorporates this pull request.:tada:

Handy links:

@msft-github-bot
Copy link
Contributor

🎉@uifabric/[email protected] has been released which incorporates this pull request.:tada:

Handy links:

@msft-github-bot
Copy link
Contributor

🎉[email protected] has been released which incorporates this pull request.:tada:

Handy links:

@lijunle
Copy link
Member

lijunle commented Apr 28, 2019

I have one question about how the tokens are defined? I read the first post but maybe I missed something.

For example:

export interface IButtonTokens {
  color?: string;
  colorHovered?: string;
  colorPressed?: string;
  iconColor?: string;
  iconColorHovered?: string;
  iconColorPressed?: string;
  ...
}

Why colorHovered and colorPressed are tokens?

<Button
  styles={{
    icon: {
      fontSize: 'large',
      color: 'black',
      fill: 'black'
    },
    menuIcon: {
      color: 'black'
    },
  }}
/>
<Button
  tokens={{
    iconSize: 'large',
    iconColor: 'black'
  }}
/>

Will menuIconColor be a token although it is not used? Will there be menuIconColor{Hovered|Pressed} token?

@JasonGore
Copy link
Member Author

We are adding tokens as we work on the new Button in experiments. There could very well be menuIconColor{Hover|Pressed} but it would be part of MenuButton and SplitButton variants, not core Button.

Documentation has since been added regarding color{Hovered|Pressed}. They are added as tokens so that consumers do not have to worry about internal component implementation, finding the right styles section (root, icon, etc.) and digging into the default styling code to figure out how to properly override the color. Tokens allow users to do this more easily, and also allow component styling implementation to change without breaking token usage.

@microsoft microsoft locked as resolved and limited conversation to collaborators Aug 30, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants