From 429fd2e5fcc2b89098a02fbaac46a0f0b5982844 Mon Sep 17 00:00:00 2001 From: Levi Thomason Date: Sat, 7 Apr 2018 12:22:16 -0700 Subject: [PATCH] initial Label migration to fela --- .../elements/Label/Types/LabelExampleBasic.js | 75 +- docs/app/index.js | 28 +- package.json | 6 +- src/elements/Label/Label.js | 141 +- src/elements/Label/LabelDetail.js | 26 +- src/elements/Label/README.md | 113 ++ src/elements/Label/rules.js | 1328 +++++++++++++++++ src/elements/Label/variables.js | 270 ++++ src/lib/createComponent.js | 43 + src/lib/getUnhandledProps.js | 11 +- src/lib/index.js | 1 + src/lib/styles/defaultSiteVariables.js | 986 ++++++++++++ yarn.lock | 103 +- 13 files changed, 3067 insertions(+), 64 deletions(-) create mode 100644 src/elements/Label/README.md create mode 100644 src/elements/Label/rules.js create mode 100644 src/elements/Label/variables.js create mode 100644 src/lib/createComponent.js create mode 100644 src/lib/styles/defaultSiteVariables.js diff --git a/docs/app/Examples/elements/Label/Types/LabelExampleBasic.js b/docs/app/Examples/elements/Label/Types/LabelExampleBasic.js index e431e2e0a6..c0758691b2 100644 --- a/docs/app/Examples/elements/Label/Types/LabelExampleBasic.js +++ b/docs/app/Examples/elements/Label/Types/LabelExampleBasic.js @@ -1,10 +1,77 @@ import React from 'react' -import { Icon, Label } from 'semantic-ui-react' +import { Container, Divider, Icon, Image, Label } from 'semantic-ui-react' const LabelExampleBasic = () => ( - + + ) export default LabelExampleBasic diff --git a/docs/app/index.js b/docs/app/index.js index add86b070b..6e21b4efd3 100644 --- a/docs/app/index.js +++ b/docs/app/index.js @@ -1,7 +1,26 @@ +import { createRenderer } from 'fela' +import monolithic from 'fela-monolithic' import React from 'react' import ReactDOM from 'react-dom' +import { Provider, ThemeProvider } from 'react-fela' import Router from './routes' +import * as defaultSiteVariables from '../../src/lib/styles/defaultSiteVariables' + +// ---------------------------------------- +// Style Renderer +// ---------------------------------------- + +const config = { + middleware: [ + // composes style objects + ], + enhancers: [ + monolithic(), + ], +} + +const renderer = createRenderer(config) // ---------------------------------------- // Rendering @@ -10,7 +29,14 @@ import Router from './routes' const mountNode = document.createElement('div') document.body.appendChild(mountNode) -const render = NewApp => ReactDOM.render(, mountNode) +const render = NewApp => ReactDOM.render( + + + + + , + mountNode, +) // ---------------------------------------- // HMR diff --git a/package.json b/package.json index 1589a4f714..a230db932a 100644 --- a/package.json +++ b/package.json @@ -58,8 +58,12 @@ "babel-runtime": "^6.25.0", "classnames": "^2.2.5", "fbjs": "^0.8.16", + "fela": "^6.1.7", + "fela-monolithic": "^5.0.21", + "hoist-non-react-statics": "^2.5.0", "lodash": "^4.17.4", - "prop-types": "^15.5.10" + "prop-types": "^15.5.10", + "react-fela": "^7.2.0" }, "devDependencies": { "@types/react": "^16.0.0", diff --git a/src/elements/Label/Label.js b/src/elements/Label/Label.js index db5cbd8870..d25168a6bf 100644 --- a/src/elements/Label/Label.js +++ b/src/elements/Label/Label.js @@ -1,31 +1,35 @@ import cx from 'classnames' import _ from 'lodash' import PropTypes from 'prop-types' -import React, { Component } from 'react' +import React from 'react' import { childrenUtils, - createShorthand, - createShorthandFactory, + createComponent, customPropTypes, - getElementType, - getUnhandledProps, META, SUI, - useKeyOnly, - useKeyOrValueAndKey, - useValueAndKey, } from '../../lib' import Icon from '../Icon/Icon' import Image from '../Image/Image' import LabelDetail from './LabelDetail' import LabelGroup from './LabelGroup' +import * as rules from './rules' +import variables from './variables' + /** * A label displays content classification. */ -export default class Label extends Component { +class Component extends React.Component { static propTypes = { + styles: PropTypes.objectOf(PropTypes.string), + ElementType: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.string, + ]), + rest: PropTypes.object, + /** An element type to render as (string or function). */ as: customPropTypes.as, @@ -108,6 +112,18 @@ export default class Label extends Component { /** Shorthand for Icon to appear as the last child and trigger onRemove. */ removeIcon: customPropTypes.itemShorthand, + /** A function to render the content part. */ + renderContent: PropTypes.func, + + /** A function to render the detail part. */ + renderDetail: PropTypes.func, + + /** A function to render the icon part. */ + renderIcon: PropTypes.func, + + /** A function to render the image part. */ + renderImage: PropTypes.func, + /** A label can appear as a ribbon attaching itself to an element. */ ribbon: PropTypes.oneOfType([ PropTypes.bool, @@ -121,6 +137,13 @@ export default class Label extends Component { tag: PropTypes.bool, } + static defaultProps = { + renderContent: ({ content }) => content, + renderDetail: ({ detail }) => LabelDetail.create(detail), + renderIcon: ({ icon }) => Icon.create(icon), + renderImage: ({ image }) => Image.create(image), + } + static _meta = { name: 'Label', type: META.TYPES.ELEMENT, @@ -144,54 +167,68 @@ export default class Label extends Component { render() { const { - active, - attached, - basic, + // active, + // attached, + // basic, children, - circular, + // circular, className, - color, + // color, content, - corner, + // corner, detail, - empty, - floating, - horizontal, + // empty, + // floating, + // horizontal, icon, image, onRemove, - pointing, + // pointing, removeIcon, - ribbon, - size, - tag, + renderContent, + renderDetail, + renderIcon, + renderImage, + // ribbon, + // size, + // tag, + styles, + ElementType, + rest, } = this.props - const pointingClass = (pointing === true && 'pointing') - || ((pointing === 'left' || pointing === 'right') && `${pointing} pointing`) - || ((pointing === 'above' || pointing === 'below') && `pointing ${pointing}`) + // const pointingClass = (pointing === true && 'pointing') + // || ((pointing === 'left' || pointing === 'right') && `${pointing} pointing`) + // || ((pointing === 'above' || pointing === 'below') && `pointing ${pointing}`) + // + // const classes = cx( + // 'ui', + // color, + // pointingClass, + // size, + // useKeyOnly(active, 'active'), + // useKeyOnly(basic, 'basic'), + // useKeyOnly(circular, 'circular'), + // useKeyOnly(empty, 'empty'), + // useKeyOnly(floating, 'floating'), + // useKeyOnly(horizontal, 'horizontal'), + // useKeyOnly(image === true, 'image'), + // useKeyOnly(tag, 'tag'), + // useKeyOrValueAndKey(corner, 'corner'), + // useKeyOrValueAndKey(ribbon, 'ribbon'), + // useValueAndKey(attached, 'attached'), + // 'label', + // className, + // ) const classes = cx( - 'ui', - color, - pointingClass, - size, - useKeyOnly(active, 'active'), - useKeyOnly(basic, 'basic'), - useKeyOnly(circular, 'circular'), - useKeyOnly(empty, 'empty'), - useKeyOnly(floating, 'floating'), - useKeyOnly(horizontal, 'horizontal'), - useKeyOnly(image === true, 'image'), - useKeyOnly(tag, 'tag'), - useKeyOrValueAndKey(corner, 'corner'), - useKeyOrValueAndKey(ribbon, 'ribbon'), - useValueAndKey(attached, 'attached'), - 'label', + styles.label, + { + [styles.link]: ElementType === 'a', + [styles.image]: image === true, + }, className, ) - const rest = getUnhandledProps(Label, this.props) - const ElementType = getElementType(Label, this.props) if (!childrenUtils.isNil(children)) { return {children} @@ -201,14 +238,20 @@ export default class Label extends Component { return ( - {Icon.create(icon)} - {typeof image !== 'boolean' && Image.create(image)} - {content} - {createShorthand(LabelDetail, val => ({ content: val }), detail)} - {onRemove && Icon.create(removeIconShorthand, { overrideProps: this.handleIconOverrides })} + {renderIcon(this.props)} + {renderImage(this.props)} + {renderContent(this.props)} + {renderDetail(this.props)} + {(onRemove && Icon.create(removeIconShorthand, { overrideProps: this.handleIconOverrides }))} ) } } -Label.create = createShorthandFactory(Label, value => ({ content: value })) +export default createComponent({ + Component, + shorthand: value => ({ content: value }), + rules, + variables, + // getDefaultElement: (props) => 'div', +}) diff --git a/src/elements/Label/LabelDetail.js b/src/elements/Label/LabelDetail.js index 032d734e92..60e62c9f6d 100644 --- a/src/elements/Label/LabelDetail.js +++ b/src/elements/Label/LabelDetail.js @@ -4,17 +4,17 @@ import React from 'react' import { childrenUtils, + createComponent, customPropTypes, - getElementType, - getUnhandledProps, META, } from '../../lib' +import variables from './variables' +import * as rules from './rules' + function LabelDetail(props) { - const { children, className, content } = props - const classes = cx('detail', className) - const rest = getUnhandledProps(LabelDetail, props) - const ElementType = getElementType(LabelDetail, props) + const { children, className, content, ElementType, rest, styles } = props + const classes = cx(styles.__detail, className) return ( @@ -30,6 +30,13 @@ LabelDetail._meta = { } LabelDetail.propTypes = { + styles: PropTypes.objectOf(PropTypes.string), + ElementType: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.string, + ]), + rest: PropTypes.object, + /** An element type to render as (string or function). */ as: customPropTypes.as, @@ -43,4 +50,9 @@ LabelDetail.propTypes = { content: customPropTypes.contentShorthand, } -export default LabelDetail +export default createComponent({ + Component: LabelDetail, + shorthand: val => ({ content: val }), + rules, + variables, +}) diff --git a/src/elements/Label/README.md b/src/elements/Label/README.md new file mode 100644 index 0000000000..77e35adcf6 --- /dev/null +++ b/src/elements/Label/README.md @@ -0,0 +1,113 @@ +# Overview + +Goals: + +- Remove dependency on precompiled CSS stylesheets +- Support site themes/styles +- Support component themes/styles +- Support compile time/runtime updates to global/component themes/styles + +## How are we going to do this? + +- Port LESS site variables to JS +- Port LESS component variables to JS +- Port LESS component definitions to JS +- Render JS definitions as fela rule functions + +## What changed? + +In support of the goals, the following changes were made + +| File | Responsibility | +|-----------------------------------------------|---------------------------------------------| +| `/docs/app/index.js` | Sets up the fela renderer/theme providers (temp, would be a single SUIR Provider)./ | +| `/lib/styles/defaultSiteVariables.js` | `NEW` Defines global site variables (default theme). | +| `/elements/Label/variables.js` | `NEW` Accepts site variables and returns component variables. | +| `/elements/Label/rules.js` | `NEW` Generates fela style objects, taking in site variables and component variables. | +| `/lib/createComponent.js` | `NEW` Single function that would configure all components. Handles style, shorthand, element type, rest props, etc. | +| `/elements/Label/Label.js` | Updated to use createComponent() and generated styles (classNames). | + +Look for comments and TODOs each file for more info. + +Note, variable/style files were copied as LESS and are being migrated piece by piece as needed. This also allows us to track changes and make decisions on patterns incrementally. + +## :warning: How to use :warning: + +Run `yarn start`, know that: + +- only the `Label` is being tested +- only this route works: http://localhost:8080/maximize/label-example-basic +- edit `docs/app/Examples/elements/Label/Types/LabelExampleBasic.js` for testing + +# Findings + +My thoughts as I ported the Label to CSS in JS using `fela`. + +## `className` + +### Monolithic vs Atomic Class Names + +We avoid atomic class names for a better debugging experience, consistency, extensibility, and allowing 3rd parties to more easily integrate with our components. + +We allow `fela` to generate `monolithic()` `className`s. We associate a `className` to each rule function: + +```jsx +// Label/rules.js + +export const label = props => ({ + className: 'ui-label', + display: 'inline-block', + ... +} +``` + +We then compose the monolithic `className`s using `cx` as we did before: + +```jsx +// Label/Label.js + +render() { + const { ElementType, styles } = this.props + + const classes = cx( + styles.label, + ElementType === 'a' && styles.link, + ) + + ... +} + +``` + +One difference is that we use BEM inspired class names. Use of `!important` should no longer be required as the new classNames are composable without collisions. + +The `!important` flags existed only due to heavy reuse of common class names. However, React component APIs are not bound by class names. We can still provide sleek and beautiful component APIs while generating human readable but machine friendly class names. + +Example: + +| React API | SUI Class | Fela Class | +|:--------------------|:------------------|:-----------------------------| +| `