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

Commit 2954cbe

Browse files
committed
feat(bindings): useStyles hook
1 parent 87bc7a9 commit 2954cbe

File tree

12 files changed

+553
-133
lines changed

12 files changed

+553
-133
lines changed

docs/src/components/Playground/renderConfig.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import * as Accessibility from '@fluentui/accessibility'
22
import * as CodeSandbox from '@fluentui/code-sandbox'
33
import * as DocsComponent from '@fluentui/docs-components'
44
import * as FluentUI from '@fluentui/react'
5+
import * as FluentBindings from '@fluentui/react-bindings'
56
import * as ReactFela from 'react-fela'
67
import * as _ from 'lodash'
8+
import * as PropTypes from 'prop-types'
79
import * as React from 'react'
810
import * as ReactDOM from 'react-dom'
911
import * as Classnames from 'classnames'
@@ -39,6 +41,10 @@ export const imports: Record<string, { version: string; module: any }> = {
3941
version: projectPackageJson.version,
4042
module: FluentUI,
4143
},
44+
'@fluentui/react-bindings': {
45+
version: null,
46+
module: FluentBindings,
47+
},
4248
classnames: {
4349
version: projectPackageJson.dependencies['classnames'],
4450
module: Classnames,
@@ -47,6 +53,10 @@ export const imports: Record<string, { version: string; module: any }> = {
4753
version: projectPackageJson.dependencies['lodash'],
4854
module: _,
4955
},
56+
'prop-types': {
57+
version: projectPackageJson.dependencies['prop-types'],
58+
module: PropTypes,
59+
},
5060
react: {
5161
version: projectPackageJson.peerDependencies['react'],
5262
module: React,
Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,54 @@
1-
import * as React from 'react'
21
import { Status } from '@fluentui/react'
2+
import { compose } from '@fluentui/react-bindings'
3+
import * as React from 'react'
4+
import * as PropTypes from 'prop-types'
5+
6+
// const MyStatus = compose<{ title: never; disabled?: boolean; name: string }>(Status, {
7+
// displayName: 'MyStatus',
8+
// mapPropsToBehavior: props => ({
9+
// name: props.name,
10+
// }),
11+
// mapPropsToStyles: props => ({
12+
// disabled: props.disabled,
13+
// }),
14+
// // Existing
15+
// // handledProps: ['disabled']
16+
// // Emotion approach
17+
// shouldForwardProp: propName => propName === 'disabled',
18+
// overrideStyles: true,
19+
// })
20+
21+
const StatusWithProp = compose<{ square?: boolean }>(Status, {
22+
displayName: 'SquareStatus',
23+
mapPropsToStyles: props => ({
24+
square: props.square,
25+
}),
26+
})
27+
28+
StatusWithProp.propTypes = {
29+
square: PropTypes.bool,
30+
}
331

4-
const StatusExampleShorthand = () => <Status title="default state" />
32+
const StatusExampleShorthand = () => (
33+
<>
34+
<h2>
35+
Default <code>Status</code>
36+
</h2>
37+
<Status title="default state" />
38+
<h2>
39+
<code>Status</code> with additional prop
40+
</h2>
41+
<StatusWithProp />
42+
<StatusWithProp square />
43+
<h2>
44+
Composed <code>MyStatus</code>
45+
</h2>
46+
{/* <MyStatus
47+
accessibility={props => ({ attributes: { root: { 'data-shift': props.name } } })}
48+
name="Hoba!"
49+
/>
50+
<MyStatus disabled /> */}
51+
</>
52+
)
553

654
export default StatusExampleShorthand
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as React from 'react'
2+
3+
type ComposeOptions = {
4+
className?: string
5+
displayName: string
6+
mapPropsToBehavior?: Function
7+
mapPropsToStyles?: Function
8+
shouldForwardProp?: Function
9+
overrideStyles?: boolean
10+
}
11+
12+
const compose = <UserProps, CProps = {}>(
13+
Component: React.ComponentType<CProps>,
14+
options: ComposeOptions,
15+
): React.ComponentType<CProps & UserProps> => {
16+
const ComponentComponent = { ...Component }
17+
18+
ComponentComponent.displayName = options.displayName
19+
ComponentComponent.defaultProps = {
20+
...Component.defaultProps,
21+
__unstable_config: {
22+
overrideStyles: false,
23+
...options,
24+
},
25+
}
26+
27+
return ComponentComponent as any
28+
}
29+
30+
export default compose
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import cx from 'classnames'
2+
import * as React from 'react'
3+
// @ts-ignore We have this export in package, but it is not present in typings
4+
import { ThemeContext } from 'react-fela'
5+
6+
import {
7+
ComponentSlotStylesPrepared,
8+
ComponentStyleFunctionParam,
9+
emptyTheme,
10+
mergeComponentStyles,
11+
mergeComponentVariables,
12+
} from '@fluentui/styles'
13+
import { ProviderContextPrepared } from '@fluentui/react'
14+
import resolveStylesAndClasses from '../styles/resolveStylesAndClasses'
15+
16+
type UseStylesOptions<StyleProps> = {
17+
className?: string
18+
mapPropsToStyles?: () => StyleProps
19+
mapPropsToInlineStyles?: () => InlineStyleProps // Consider better name
20+
rtl?: boolean
21+
}
22+
23+
type InlineStyleProps = {
24+
className?: string
25+
design?: any // TODO type
26+
styles?: any // TODO type
27+
variables?: any // TODO type
28+
}
29+
30+
const useStyles = <StyleProps>(
31+
displayName: string | string[],
32+
options: UseStylesOptions<StyleProps>,
33+
) => {
34+
const {
35+
className = 'no-classname-🙉',
36+
mapPropsToStyles = () => ({} as StyleProps),
37+
mapPropsToInlineStyles = () => ({} as InlineStyleProps),
38+
rtl = false,
39+
} = options
40+
41+
const context: ProviderContextPrepared = React.useContext(ThemeContext)
42+
const { disableAnimations = false, renderer = null, theme = emptyTheme } = context || {}
43+
44+
// TODO: throw if there is no context
45+
46+
const props = mapPropsToStyles()
47+
const { className: userClassName, styles, design, variables } = mapPropsToInlineStyles()
48+
49+
const componentVariables = Array.isArray(displayName)
50+
? displayName.map(displayName => theme.componentVariables[displayName])
51+
: [theme.componentVariables[displayName]]
52+
const componentStyles = Array.isArray(displayName)
53+
? displayName.map(displayName => theme.componentStyles[displayName])
54+
: [theme.componentStyles[displayName]]
55+
56+
// Merge inline variables on top of cached variables
57+
const resolvedVariables = mergeComponentVariables(
58+
...componentVariables,
59+
variables,
60+
)(theme.siteVariables)
61+
62+
// Resolve styles using resolved variables, merge results, allow props.styles to override
63+
const mergedStyles: ComponentSlotStylesPrepared = mergeComponentStyles(
64+
...componentStyles,
65+
{ root: design },
66+
{ root: styles },
67+
)
68+
69+
const styleParam: ComponentStyleFunctionParam = {
70+
displayName: Array.isArray(displayName) ? displayName[0] : displayName,
71+
props,
72+
variables: resolvedVariables,
73+
theme,
74+
rtl,
75+
disableAnimations,
76+
}
77+
78+
// Fela plugins rely on `direction` param in `theme` prop instead of RTL
79+
// Our API should be aligned with it
80+
// Heads Up! Keep in sync with Design.tsx render logic
81+
const direction = rtl ? 'rtl' : 'ltr'
82+
const felaParam = {
83+
theme: { direction },
84+
disableAnimations,
85+
displayName, // does not affect styles, only used by useEnhancedRenderer in docs
86+
}
87+
88+
const { resolvedStyles, classes } = resolveStylesAndClasses(
89+
mergedStyles,
90+
styleParam,
91+
// @ts-ignore
92+
renderer ? style => renderer.renderRule(() => style, felaParam) : undefined,
93+
)
94+
95+
classes.root = cx(className, classes.root, userClassName)
96+
97+
return [classes, resolvedStyles]
98+
}
99+
100+
export default useStyles

packages/react-bindings/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ export * from './FocusZone/FocusZone.types'
1010
export * from './FocusZone/focusUtilities'
1111

1212
export { default as useAccessibility } from './hooks/useAccessibility'
13+
export { default as useStyles } from './hooks/useStyles'
1314
export { default as unstable_useDispatchEffect } from './hooks/useDispatchEffect'
1415
export { default as useStateManager } from './hooks/useStateManager'
1516

1617
export { default as getElementType } from './utils/getElementType'
1718
export { default as getUnhandledProps } from './utils/getUnhandledProps'
19+
20+
export { default as compose } from './compose'
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import cx from 'classnames'
2+
import * as _ from 'lodash' // REMOVE ME PLEASE!
3+
4+
import callable from '../utils/callable'
5+
import {
6+
ComponentSlotStylesPrepared,
7+
ComponentStyleFunctionParam,
8+
ComponentVariablesObject,
9+
DebugData,
10+
emptyTheme,
11+
isDebugEnabled,
12+
mergeComponentStyles,
13+
mergeComponentVariables,
14+
PropsWithVarsAndStyles,
15+
withDebugId,
16+
} from '@fluentui/styles'
17+
18+
import createAnimationStyles from './createAnimationStyles'
19+
import resolveStylesAndClasses from './resolveStylesAndClasses'
20+
import { ComponentSlotClasses, ProviderContextPrepared } from '@fluentui/react' // TODO fix me
21+
22+
export interface RenderResultConfig {
23+
classes: ComponentSlotClasses
24+
variables: ComponentVariablesObject
25+
styles: ComponentSlotStylesPrepared
26+
}
27+
28+
export interface RenderConfig {
29+
className?: string
30+
displayName: string
31+
props: PropsWithVarsAndStyles
32+
saveDebug: (debug: DebugData | null) => void
33+
}
34+
35+
const getStyles = (options: any, context?: ProviderContextPrepared): RenderConfig => {
36+
const { displayName, className, props, saveDebug } = options
37+
38+
// const displayName = ''
39+
// const className = ''
40+
// const props = {} // include state
41+
// const saveDebug = () => {}
42+
43+
const {
44+
disableAnimations = false,
45+
renderer = null,
46+
rtl = false,
47+
theme = emptyTheme,
48+
_internal_resolvedComponentVariables: resolvedComponentVariables = {},
49+
} = context || {}
50+
51+
// Resolve variables for this component, cache the result in provider
52+
if (!resolvedComponentVariables[displayName]) {
53+
resolvedComponentVariables[displayName] =
54+
callable(theme.componentVariables[displayName])(theme.siteVariables) || {} // component variables must not be undefined/null (see mergeComponentVariables contract)
55+
}
56+
57+
// Merge inline variables on top of cached variables
58+
const resolvedVariables = props.variables
59+
? mergeComponentVariables(
60+
resolvedComponentVariables[displayName],
61+
withDebugId(props.variables, 'props.variables'),
62+
)(theme.siteVariables)
63+
: resolvedComponentVariables[displayName]
64+
65+
const animationCSSProp = props.animation
66+
? // @ts-ignore
67+
createAnimationStyles(props.animation, context.theme)
68+
: {}
69+
70+
// Resolve styles using resolved variables, merge results, allow props.styles to override
71+
const mergedStyles: ComponentSlotStylesPrepared = mergeComponentStyles(
72+
theme.componentStyles[displayName],
73+
withDebugId({ root: props.design }, 'props.design'),
74+
withDebugId({ root: props.styles }, 'props.styles'),
75+
withDebugId({ root: animationCSSProp }, 'props.animation'),
76+
)
77+
78+
const styleParam: ComponentStyleFunctionParam = {
79+
displayName,
80+
props,
81+
variables: resolvedVariables,
82+
theme,
83+
rtl,
84+
disableAnimations,
85+
}
86+
87+
// Fela plugins rely on `direction` param in `theme` prop instead of RTL
88+
// Our API should be aligned with it
89+
// Heads Up! Keep in sync with Design.tsx render logic
90+
const direction = rtl ? 'rtl' : 'ltr'
91+
const felaParam = {
92+
theme: { direction },
93+
disableAnimations,
94+
displayName, // does not affect styles, only used by useEnhancedRenderer in docs
95+
}
96+
97+
const { resolvedStyles, resolvedStylesDebug, classes } = resolveStylesAndClasses(
98+
mergedStyles,
99+
styleParam,
100+
// @ts-ignore
101+
renderer ? style => renderer.renderRule(() => style, felaParam) : undefined,
102+
)
103+
104+
classes.root = cx(className, classes.root, props.className)
105+
106+
// conditionally add sources for evaluating debug information to component
107+
if (process.env.NODE_ENV !== 'production' && isDebugEnabled) {
108+
saveDebug({
109+
componentName: displayName,
110+
componentVariables: _.filter(
111+
resolvedVariables._debug,
112+
variables => !_.isEmpty(variables.resolved),
113+
),
114+
componentStyles: resolvedStylesDebug,
115+
siteVariables: _.filter(theme.siteVariables._debug, siteVars => {
116+
if (_.isEmpty(siteVars) || _.isEmpty(siteVars.resolved)) {
117+
return false
118+
}
119+
120+
const keys = Object.keys(siteVars.resolved)
121+
if (
122+
keys.length === 1 &&
123+
keys.pop() === 'fontSizes' &&
124+
_.isEmpty(siteVars.resolved['fontSizes'])
125+
) {
126+
return false
127+
}
128+
129+
return true
130+
}),
131+
})
132+
}
133+
134+
return {
135+
// @ts-ignore
136+
classes,
137+
variables: resolvedVariables,
138+
styles: resolvedStyles,
139+
theme,
140+
}
141+
}
142+
143+
export default getStyles

packages/react-bindings/src/utils/getUnhandledProps.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,19 @@
99
function getUnhandledProps<P extends Record<string, any>>(
1010
handledProps: (keyof P)[],
1111
props: P,
12+
shouldForwardProp: Function = () => false,
1213
): Partial<P> {
1314
return Object.keys(props).reduce<Partial<P>>((acc, prop) => {
14-
if (handledProps.indexOf(prop) === -1) (acc as any)[prop] = props[prop]
15+
if (shouldForwardProp(prop)) {
16+
;(acc as any)[prop] = props[prop]
17+
return acc
18+
}
1519

20+
if (handledProps.indexOf(prop) !== -1) {
21+
return acc
22+
}
23+
24+
;(acc as any)[prop] = props[prop]
1625
return acc
1726
}, {})
1827
}

0 commit comments

Comments
 (0)