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

Commit a50c76b

Browse files
committed
feat(bindings): useStyles hook
1 parent 45fa169 commit a50c76b

File tree

6 files changed

+272
-144
lines changed

6 files changed

+272
-144
lines changed

docs/src/components/ComponentDoc/ComponentControls/ComponentControls.tsx

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,22 +73,24 @@ const ComponentControls: React.FC<ComponentControlsProps> = props => {
7373
aria-label={toolbarAriaLabel || null}
7474
items={[
7575
{
76-
key: 'show-code',
7776
icon: { name: 'code', style: { width: '20px', height: '20px' } },
7877
onClick: onShowCode,
7978
active: showCode,
8079
children: (Component, props) => (
81-
<Tooltip content="Try it" trigger={<Component {...props} />} />
80+
<Tooltip content="Try it" key="show-code" trigger={<Component {...props} />} />
8281
),
8382
},
8483

8584
{
86-
key: 'show-variables',
8785
icon: { name: 'paint brush', style: { width: '20px', height: '20px' } },
8886
onClick: onShowVariables,
8987
active: showVariables,
9088
children: (Component, props) => (
91-
<Tooltip content="Theme it" trigger={<Component {...props} />} />
89+
<Tooltip
90+
content="Theme it"
91+
key="show-variables"
92+
trigger={<Component {...props} />}
93+
/>
9294
),
9395
},
9496
{
@@ -97,29 +99,30 @@ const ComponentControls: React.FC<ComponentControlsProps> = props => {
9799
kind: 'divider',
98100
},
99101
{
100-
key: 'show-transparent',
101102
icon: { name: 'adjust', style: { width: '20px', height: '20px' } },
102103
onClick: onShowTransparent,
103104
active: showTransparent,
104105
children: (Component, props) => (
105-
<Tooltip content="Transparent" trigger={<Component {...props} />} />
106+
<Tooltip
107+
content="Transparent"
108+
key="show-transparent"
109+
trigger={<Component {...props} />}
110+
/>
106111
),
107112
},
108113
{
109-
key: 'show-rtl',
110114
icon: { name: 'align right', style: { width: '20px', height: '20px' } },
111115
onClick: onShowRtl,
112116
active: showRtl,
113117
children: (Component, props) => (
114-
<Tooltip content="RTL" trigger={<Component {...props} />} />
118+
<Tooltip content="RTL" key="show-rtl" trigger={<Component {...props} />} />
115119
),
116120
},
117121

118122
{
119-
key: 'maximize',
120123
icon: { name: 'external alternate', style: { width: '20px', height: '20px' } },
121124
children: (Component, props) => (
122-
<Tooltip content="Popout" trigger={<Component {...props} />} />
125+
<Tooltip content="Popout" key="maximize" trigger={<Component {...props} />} />
123126
),
124127
as: NavLink,
125128
to: `/maximize/${_.kebabCase(
@@ -137,18 +140,20 @@ const ComponentControls: React.FC<ComponentControlsProps> = props => {
137140
kind: 'divider',
138141
},
139142
{
140-
key: 'show-codesandbox',
141143
onClick: onCodeSandboxClick,
142144
icon: { name: codeSandboxIcon, style: { width: '20px', height: '20px' } },
143145
children: (Component, props) => (
144-
<Tooltip content={codeSandboxTooltip} trigger={<Component {...props} />} />
146+
<Tooltip
147+
content={codeSandboxTooltip}
148+
key="show-codesandbox"
149+
trigger={<Component {...props} />}
150+
/>
145151
),
146152
},
147153
{
148-
key: 'copy-link',
149154
icon: { name: 'linkify', style: { width: '20px', height: '20px' } },
150155
children: (Component, props) => (
151-
<CopyToClipboard value={anchorName}>
156+
<CopyToClipboard key="copy-link" value={anchorName}>
152157
{(active, onClick) => (
153158
<Tooltip
154159
content={active ? 'Copied!' : 'Permalink'}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
emptyTheme,
8+
mergeComponentStyles,
9+
mergeComponentVariables,
10+
} from '@fluentui/react/src/utils/mergeThemes'
11+
import {
12+
ComponentSlotStylesPrepared,
13+
ComponentStyleFunctionParam,
14+
ProviderContextPrepared,
15+
} from '@fluentui/react'
16+
import resolveStylesAndClasses from '@fluentui/react/src/utils/resolveStylesAndClasses'
17+
18+
const useStyles = (displayName: string, options: any) => {
19+
const className = options.className || 'no-classname-🙉'
20+
const rtl = options.rtl || false
21+
const props = options.mapPropsToStyles()
22+
const { className: userClassName, styles, design, variables } = options.mapInlineToStyles()
23+
24+
const context: ProviderContextPrepared = React.useContext(ThemeContext)
25+
26+
const { disableAnimations = false, renderer = null, theme = emptyTheme } = context || {}
27+
28+
// Merge inline variables on top of cached variables
29+
const resolvedVariables = mergeComponentVariables(
30+
theme.componentVariables[displayName],
31+
variables,
32+
)(theme.siteVariables)
33+
34+
// Resolve styles using resolved variables, merge results, allow props.styles to override
35+
const mergedStyles: ComponentSlotStylesPrepared = mergeComponentStyles(
36+
theme.componentStyles[displayName],
37+
{ root: design },
38+
{ root: styles },
39+
)
40+
41+
const styleParam: ComponentStyleFunctionParam = {
42+
displayName,
43+
props,
44+
variables: resolvedVariables,
45+
theme,
46+
rtl,
47+
disableAnimations,
48+
}
49+
50+
// Fela plugins rely on `direction` param in `theme` prop instead of RTL
51+
// Our API should be aligned with it
52+
// Heads Up! Keep in sync with Design.tsx render logic
53+
const direction = rtl ? 'rtl' : 'ltr'
54+
const felaParam = {
55+
theme: { direction },
56+
disableAnimations,
57+
displayName, // does not affect styles, only used by useEnhancedRenderer in docs
58+
}
59+
60+
const { resolvedStyles, classes } = resolveStylesAndClasses(
61+
mergedStyles,
62+
styleParam,
63+
// @ts-ignore
64+
renderer ? style => renderer.renderRule(() => style, felaParam) : undefined,
65+
)
66+
67+
classes.root = cx(className, classes.root, userClassName)
68+
69+
return [classes, resolvedStyles]
70+
}
71+
72+
export default useStyles

packages/react-bindings/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ 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

Lines changed: 111 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
import { Accessibility } from '@fluentui/accessibility'
2+
import {
3+
getElementType,
4+
getUnhandledProps,
5+
useAccessibility,
6+
useStyles,
7+
} from '@fluentui/react-bindings'
28
import * as customPropTypes from '@fluentui/react-proptypes'
39
import * as PropTypes from 'prop-types'
410
import * as React from 'react'
511
import Image, { ImageProps } from '../Image/Image'
612
import Label, { LabelProps } from '../Label/Label'
713
import Status, { StatusProps } from '../Status/Status'
8-
import { WithAsProp, ShorthandValue, withSafeTypeForAs } from '../../types'
9-
import {
10-
createShorthandFactory,
11-
UIComponent,
12-
UIComponentProps,
13-
commonPropTypes,
14-
SizeValue,
15-
ShorthandFactory,
16-
} from '../../utils'
14+
import { WithAsProp, ShorthandValue, withSafeTypeForAs, ProviderContextPrepared } from '../../types'
15+
import { createShorthandFactory, UIComponentProps, commonPropTypes, SizeValue } from '../../utils'
16+
// @ts-ignore
17+
import { ThemeContext } from 'react-fela'
1718

1819
export interface AvatarProps extends UIComponentProps {
1920
/**
@@ -40,91 +41,117 @@ export interface AvatarProps extends UIComponentProps {
4041
getInitials?: (name: string) => string
4142
}
4243

43-
class Avatar extends UIComponent<WithAsProp<AvatarProps>, any> {
44-
static create: ShorthandFactory<AvatarProps>
45-
46-
static className = 'ui-avatar'
47-
48-
static displayName = 'Avatar'
49-
50-
static propTypes = {
51-
...commonPropTypes.createCommon({
52-
children: false,
53-
content: false,
44+
const Avatar = React.forwardRef<HTMLDivElement, WithAsProp<AvatarProps>>((props, ref) => {
45+
const {
46+
className,
47+
design,
48+
name,
49+
status,
50+
image,
51+
label,
52+
getInitials,
53+
size,
54+
styles,
55+
variables,
56+
} = props
57+
58+
const { rtl }: ProviderContextPrepared = React.useContext(ThemeContext)
59+
const [classes, resolvedStyles] = useStyles(Avatar.displayName, {
60+
mapPropsToStyles: () => ({
61+
size,
5462
}),
55-
name: PropTypes.string,
56-
image: customPropTypes.itemShorthandWithoutJSX,
57-
label: customPropTypes.itemShorthand,
58-
size: customPropTypes.size,
59-
status: customPropTypes.itemShorthand,
60-
getInitials: PropTypes.func,
61-
}
62-
63-
static defaultProps = {
64-
size: 'medium',
65-
getInitials(name: string) {
66-
if (!name) {
67-
return ''
68-
}
69-
70-
const reducedName = name
71-
.replace(/\s*\(.*?\)\s*/g, ' ')
72-
.replace(/\s*{.*?}\s*/g, ' ')
73-
.replace(/\s*\[.*?]\s*/g, ' ')
74-
75-
const initials = reducedName
76-
.split(' ')
77-
.filter(item => item !== '')
78-
.map(item => item.charAt(0))
79-
.reduce((accumulator, currentValue) => accumulator + currentValue)
80-
81-
if (initials.length > 2) {
82-
return initials.charAt(0) + initials.charAt(initials.length - 1)
83-
}
84-
return initials
85-
},
86-
} as AvatarProps
87-
88-
renderComponent({ accessibility, ElementType, classes, unhandledProps, styles, variables }) {
89-
const { name, status, image, label, getInitials, size } = this.props as AvatarProps
90-
91-
return (
92-
<ElementType {...accessibility.attributes.root} {...unhandledProps} className={classes.root}>
93-
{Image.create(image, {
63+
mapInlineToStyles: () => ({
64+
className,
65+
design,
66+
styles,
67+
variables,
68+
}),
69+
rtl,
70+
})
71+
const getA11Props = useAccessibility(props.accessibility, {
72+
debugName: Avatar.displayName,
73+
rtl,
74+
})
75+
const ElementType = getElementType(props)
76+
const unhandledProps = getUnhandledProps((Avatar as any).handledProps /* TODO */, props)
77+
78+
return (
79+
<ElementType {...getA11Props('root', { className: classes.root, ref, ...unhandledProps })}>
80+
{Image.create(image, {
81+
defaultProps: () => ({
82+
fluid: true,
83+
avatar: true,
84+
title: name,
85+
styles: resolvedStyles.image,
86+
}),
87+
})}
88+
{!image &&
89+
Label.create(label || {}, {
9490
defaultProps: () => ({
95-
fluid: true,
96-
avatar: true,
91+
content: getInitials(name),
92+
circular: true,
9793
title: name,
98-
styles: styles.image,
94+
styles: resolvedStyles.label,
9995
}),
10096
})}
101-
{!image &&
102-
Label.create(label || {}, {
103-
defaultProps: () => ({
104-
content: getInitials(name),
105-
circular: true,
106-
title: name,
107-
styles: styles.label,
108-
}),
109-
})}
110-
{Status.create(status, {
111-
defaultProps: () => ({
112-
size,
113-
styles: styles.status,
114-
variables: {
115-
borderColor: variables.statusBorderColor,
116-
borderWidth: variables.statusBorderWidth,
117-
},
118-
}),
119-
})}
120-
</ElementType>
121-
)
122-
}
97+
{Status.create(status, {
98+
defaultProps: () => ({
99+
size,
100+
styles: resolvedStyles.status,
101+
// variables: {
102+
// borderColor: variables.statusBorderColor,
103+
// borderWidth: variables.statusBorderWidth,
104+
// },
105+
// TODO: Fix me please PLEASE PLEAASSEEE
106+
}),
107+
})}
108+
</ElementType>
109+
)
110+
})
111+
;(Avatar as any).className = 'ui-avatar'
112+
Avatar.displayName = 'Avatar'
113+
;(Avatar as any).propTypes = {
114+
...commonPropTypes.createCommon({
115+
children: false,
116+
content: false,
117+
}),
118+
name: PropTypes.string,
119+
image: customPropTypes.itemShorthandWithoutJSX,
120+
label: customPropTypes.itemShorthand,
121+
size: customPropTypes.size,
122+
status: customPropTypes.itemShorthand,
123+
getInitials: PropTypes.func,
124+
}
125+
Avatar.defaultProps = {
126+
size: 'medium',
127+
getInitials(name: string) {
128+
if (!name) {
129+
return ''
130+
}
131+
132+
const reducedName = name
133+
.replace(/\s*\(.*?\)\s*/g, ' ')
134+
.replace(/\s*{.*?}\s*/g, ' ')
135+
.replace(/\s*\[.*?]\s*/g, ' ')
136+
137+
const initials = reducedName
138+
.split(' ')
139+
.filter(item => item !== '')
140+
.map(item => item.charAt(0))
141+
.reduce((accumulator, currentValue) => accumulator + currentValue)
142+
143+
if (initials.length > 2) {
144+
return initials.charAt(0) + initials.charAt(initials.length - 1)
145+
}
146+
return initials
147+
},
123148
}
124149

150+
// @ts-ignore
125151
Avatar.create = createShorthandFactory({ Component: Avatar, mappedProp: 'name' })
126152

127153
/**
128154
* An Avatar is a graphical representation of a user.
129155
*/
156+
// @ts-ignore
130157
export default withSafeTypeForAs<typeof Avatar, AvatarProps>(Avatar)

0 commit comments

Comments
 (0)