Skip to content
This repository has been archived by the owner on Mar 4, 2020. It is now read-only.

feat: support theming and styling #16

Merged
merged 25 commits into from
Aug 9, 2018
Merged

Conversation

levithomason
Copy link
Member

@levithomason levithomason commented Jul 27, 2018

Apologies, this PR is necessarily large:

  1. Move rules and variables to themes/teams
  2. Rename "rules" to "styles"
  3. Add support for nested theming
  4. Add support for theme switching
  5. Add typings for all theming

Relevant features...

Styles prop

The styles prop is now supported:

<Button 
  content="I am gold"
  styles={{ root: { color: 'goldenrod' } }}
/>

Nested Providers

You can now nest Providers and themes will be merged:

<Provider theme={teamsTheme}>
  <Button content="Default Button" />

  <Provider
    theme={{
      componentVariables: {
        Button: { backgroundColor: 'goldenrod' }
      }
    }}
  >
    <Button content="Golden Button" />
  />
</Provider>

@levithomason levithomason force-pushed the feat/organize-theme branch 2 times, most recently from 0eb4db4 to 327b86c Compare July 27, 2018 15:40
@mnajdova
Copy link
Contributor

I fixed some bugs regarding merging the component's variables and styles. The values that we receive from the theme are in the following format:

componentVariables = [
  [
    Avatar: (...)
  ],
  [
    Avatar: (...)
  ]
]

so I introduced some helpers methods for converting these lists to one array of callables and another one for merging the callables. The variables and the styles can now be overridden when using the components by providing the variables and styles props.

Let me know if you have any feedback or need some more help with this.

@levithomason
Copy link
Member Author

Thanks for wanting to help. Note, I had a lot of unpushed changes that included dealing with merging themes downstream on context. I've forced pushed those to save the work and resume progress here.

I will fix conflicts and continue.

style: { fontWeight: 700 },
},
]
console.log('THEME', theme)
Copy link
Contributor

Choose a reason for hiding this comment

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

seems to be a debug leftover

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, branch was still in progress until right now 👍

@codecov
Copy link

codecov bot commented Aug 6, 2018

Codecov Report

Merging #16 into master will increase coverage by 0.32%.
The diff coverage is 70.83%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master      #16      +/-   ##
==========================================
+ Coverage   85.97%   86.29%   +0.32%     
==========================================
  Files          76       39      -37     
  Lines        1112      664     -448     
  Branches      228       90     -138     
==========================================
- Hits          956      573     -383     
+ Misses        149       88      -61     
+ Partials        7        3       -4
Impacted Files Coverage Δ
src/components/Header/Header.tsx 94.73% <ø> (-0.92%) ⬇️
src/components/Button/Button.tsx 81.08% <ø> (-1.85%) ⬇️
src/components/Chat/Chat.tsx 94.73% <ø> (-0.51%) ⬇️
src/components/Accordion/Accordion.tsx 66% <ø> (-1.31%) ⬇️
src/components/List/ListItem.tsx 82.35% <ø> (-1.29%) ⬇️
src/components/Avatar/Avatar.tsx 91.17% <ø> (-0.93%) ⬇️
src/components/Layout/Layout.tsx 88% <ø> (-0.47%) ⬇️
src/components/Icon/Icon.tsx 79.16% <ø> (-2.98%) ⬇️
src/components/Accordion/AccordionContent.tsx 80% <ø> (-4.22%) ⬇️
src/components/Menu/Menu.tsx 100% <ø> (ø) ⬆️
... and 23 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f14208b...f12091f. Read the comment docs.

@levithomason levithomason changed the title [WIP] feat: support theming feat: support theming Aug 6, 2018
@levithomason levithomason changed the title feat: support theming feat: support theming and styling Aug 6, 2018
@@ -119,7 +113,7 @@ class Avatar extends UIComponent<any, any> {
size="mini"
name={icon}
color="white"
style={avatarRules.presenceIcon()}
className={classes.presenceIcon}
Copy link
Member Author

Choose a reason for hiding this comment

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

This looks like a bug, will update.

? callable(labelPropVariables)()
: callable(Label.variables)()
const iconVariables = callable(iconProps.variables)() || {}
const labelVariables = callable(variables)() || {}
Copy link
Member Author

Choose a reason for hiding this comment

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

This may also need careful review.

The iconProps available at this point are produced by the shorthand factory. Any variables provided on iconProps at this point will not have access to the theme values on context so they cannot be resolved before being handed to the overrides function.

This means we will not be able to hand resolved variables to components reliably, unless we also make factories aware of how to resolve variables (which I think is a very bad idea). That would require making factories access context.

/cc @kuzhelov @mnajdova

Copy link
Member Author

Choose a reason for hiding this comment

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

To be clear, I don't have a solution or proposal for this right now but I think this PR needs to get merged. It is too difficult to maintain merge conflict fixes if we push it out.

Copy link
Contributor

Choose a reason for hiding this comment

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

yes, agree with increasing complexity to address merge conflicts - will help you with merging this PR today.

vars.contentLineHeight = siteVars.lineHeightSmall

return vars
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Moving this particular file represents a significant change. The ListItem variables were defined in the listVariables file, making them accessible on context under List instead of ListItem. I had to rename this file to listItemVariables and expose it on context as ListItem in order for our theming contract to work (components get variables and styles by their display name).

<RendererProvider renderer={outgoingTheme.renderer}>
<ThemeProvider theme={outgoingTheme}>{children}</ThemeProvider>
</RendererProvider>
)
Copy link
Member Author

Choose a reason for hiding this comment

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

This is important. Each Provider now:

  1. consumes the theme from context
  2. merges it with its theme prop
  3. passes the merged theme downstream

This enables nested cascading themes.

}, target)
}

const mergeThemes = (...themes: IThemeInput[]): IThemePrepared => {
Copy link
Member Author

Choose a reason for hiding this comment

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

This function is best reviewed by looking at the tests, mergeThemes-test.ts.

_htmlFontSize = fontSize || getComputedFontSize()
}

export default rem
Copy link
Member Author

Choose a reason for hiding this comment

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

Cruft

PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.func]),
),
rtl: PropTypes.bool,
theme: PropTypes.shape({
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this required? It looks to be based on the interface.

Copy link
Member Author

Choose a reason for hiding this comment

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

Currently, docs are still generated from propTypes. That needs to be moved to being generated from typings. Then we can consider removing propTypes.

The other consideration is that most consumers are not running typescript. So, I think we may end up keeping both interfaces and propTypes around. Also, some runtime checks are really handy since we can do things with values (suggest icon names, etc).

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I meant to say "should the theme propType be .isRequired?". I wasn't talking about the propTypes themselves.

Copy link
Member Author

Choose a reason for hiding this comment

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

Got it, thanks. We have a scheduled task to make a default theme. For now, it is required.


// root, icon, etc.
const componentParts: string[] = stylesArr.reduce((acc, next) => {
return next ? _.union(acc, _.keys(next)) : acc
Copy link
Contributor

Choose a reason for hiding this comment

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

next should always be truthy because of the above call to toCompactArray, if I'm understanding this correctly.


/**
* Returns a string of HTML classes.
* Renders one or many component styles (objects of component parts) to the DOM.
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this line about rendering to the DOM?

}, target)
}

const mergeThemes = (...themes: IThemeInput[]): IThemePrepared => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I think this would be more simple if it took an array of themes.

  1. It doesn't really take multiple arguments. It takes a single collection of themes. Representing this with an appropriate data type makes this obvious.

  2. It becomes possible to add additional arguments if needed.

  3. It makes it easier to provide a list of themes to this function. You can do mergeThemes(...myListOfThemes), but I would ask "why?" when you can just do mergeThemes(myListOfThemes).

  4. Least importantly, it saves you casting arguments to an array.

Justified myself because you asked :).

acc.rtl = mergeRTL(acc.rtl, next.rtl)

// Use the correct renderer for RTL
acc.renderer = acc.rtl ? felaRtlRenderer : felaRenderer
Copy link
Contributor

Choose a reason for hiding this comment

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

Should emptyTheme have a default renderer? It looks to be possible to never set renderer if next is never truthy. Same goes for rtl.

@@ -0,0 +1,5 @@
const toCompactArray = <T = any>(...values: T[]): T[] => {
return [].concat(...values).filter(Boolean)
Copy link
Contributor

Choose a reason for hiding this comment

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

You shouldn't have to spread values here. Also, why not _.compact?

Copy link
Member Author

Choose a reason for hiding this comment

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

Spread is required as each value may be a single value or an array. we need to concat each argument.

_.compact only compacts a single array, it is essentially equivalent to [].filter(Boolean).

The feature I need is:

  1. Cast to array
  2. Allow items or arrays of items
  3. Compact the resulting array

So:

toCompactArray(1)
//=> [1]

toCompactArray(1, null, 2)
//=> [1, 2]

toCompactArray(1, 'two', [3, 4], null, ['more', 'stuff', undefined])
// => [1, 'two', 3, 4, 'more', 'stuff']

A more literal name would be something quite verbose like castToFlattenedCompactArray() since it does all of these things.

You could also create it from _.compact(_.flatten(...values)), however, given how often it is used and on hot paths I went with a minimal custom function that we can more easily optimize. Lodash does a lot of fancy checks and handling that we don't need to waste more cycles on.

Example, we can already optimize this by using reduce and skipping the second loop via filter. We can just not concat empty values into the accumulator. You could also put an early return check for values.length === 1 scenarios, etc...

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure, but why can't you just pass an array when you call toCompactArray? 🤔

You'd still need to handle the flattening, of course.

componentVariables = {},
componentStyles = {},
rtl = false,
renderer = felaRenderer,
Copy link
Contributor

Choose a reason for hiding this comment

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

See my previous comment about missing values for rtl and renderer. It would be a benefit to overall maintainability if the provider did all of the heavy lifting, and these consumers could be more confident about the interface they receive, since they don't have to work with all the same uncertainty.

import { IComponentPartStylesInput, ICSSInJSStyle } from '../../../../../types/theme'

const accordionStyles: IComponentPartStylesInput = {
root: (): ICSSInJSStyle => ({
Copy link
Contributor

@dvdzkwsk dvdzkwsk Aug 7, 2018

Choose a reason for hiding this comment

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

Would it perhaps be easier to type the overall styles object as [key: string]: (): ICSSInJSStyle (or whatever the dammed syntax is) so that every individual style function doesn't have to declare its type?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, did this however it doesn't provide completions inside the style object for index keys. It will only do so if you type the return value. Even that has many shortcomings. Note the const accordionStyles: IComponentPartStylesInput, which is doing exactly what you are referring to.

Since this PR is being broken down into smaller bits, we'll save the typings for later.


return {
...rules,
...(disabled
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: may be more maintainable to break this out into another function rather than inlining it.

@@ -0,0 +1,21 @@
export interface IDividerVariables {
[key: string]: string | number
Copy link
Contributor

Choose a reason for hiding this comment

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

Why pin an unknown key to a specific type? Mostly just curious.

Copy link
Member Author

Choose a reason for hiding this comment

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

Just had to pick something and this seemed reasonable. This is essentially a "first guess" at what acceptable style variable values would be. Very much open to change and iteration.

We're targeting simple and flat variable objects. I can say I don't think we should allow arrays and objects and such. The theming system is pretty robust as it is, keeping variables simple will help make themes more usable.

@@ -1,3 +1,5 @@
const callable = val => (typeof val !== 'function' ? () => val : val)
const callable = (possibleFunction: any) => (...args: any[]) => {
return typeof possibleFunction === 'function' ? possibleFunction(...args) : possibleFunction
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Copy link
Member

Choose a reason for hiding this comment

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

Can we attach a benchmark into JSDoc?
https://jsperf.com/startdust-callable

I thought that callableRest() would be faster 😼 But, not

const styleParam: ComponentStyleFunctionParam = {
props,
variables,
siteVariables,
Copy link
Member Author

Choose a reason for hiding this comment

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

⚠️ NOTE TO SELF

siteVariables should not be passed to style functions. This was found in the breakout work for moving rules and variables to themes/teams/components. The Text component was directly accessing site variables via an import. Instead, it needs to get these values from the textVariables.ts.

This is the only case of siteVariables being accessed in a component's style function. It should also be removed.

@levithomason levithomason merged commit e345edf into master Aug 9, 2018
@levithomason levithomason deleted the feat/organize-theme branch August 9, 2018 23:05
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.

None yet

5 participants