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

[WIP] feat: add compose(), caching, converting components to hooks #2229

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions build/gulp/tasks/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ const componentsSrc = [
`${paths.posix.packageSrc('react')}/components/*/[A-Z]*.tsx`,
`${paths.posix.packageSrc('react-bindings')}/FocusZone/[A-Z]!(*.types).tsx`,
`${paths.posix.packageSrc('react-component-ref')}/[A-Z]*.tsx`,
'!**/ButtonIcon.tsx',
'!**/StatusIcon.tsx',
'!**/ButtonContent.tsx',
'!**/ButtonContent.tsx',
'!**/AvatarLabel.tsx',
'!**/AvatarImage.tsx',
'!**/AvatarStatus.tsx',
'!**/SliderInput.tsx',
'!**/CheckboxLabel.tsx',
'!**/CheckboxIcon.tsx',
'!**/CheckboxToggleIcon.tsx',
]
const behaviorSrc = [`${paths.posix.packageSrc('accessibility')}/behaviors/*/[a-z]*Behavior.ts`]
const examplesIndexSrc = `${paths.posix.docsSrc()}/examples/*/*/*/index.tsx`
Expand Down
22 changes: 22 additions & 0 deletions docs/src/components/ComponentPlayground/componentGenerators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import { useSelectKnob, useStringKnob } from '@fluentui/docs-components'
import {
AvatarProps,
BoxProps,
ButtonProps,
DialogProps,
DividerProps,
EmbedProps,
IconProps,
ImageProps,
SliderProps,
StatusProps,
VideoProps,
} from '@fluentui/react'
import * as _ from 'lodash'
Expand All @@ -21,13 +24,22 @@ export const Avatar: KnobComponentGenerators<AvatarProps> = {
name: propName,
initialValue: _.capitalize(`${faker.name.firstName()} ${faker.name.lastName()}`),
}),
// TODO: fix support for composed components
image: () => null,
label: () => null,
status: () => null,
}

export const Box: KnobComponentGenerators<BoxProps> = {
// TODO: fix support for boxes
children: () => null,
}

export const Button: KnobComponentGenerators<ButtonProps> = {
// TODO: fix support for composed components
icon: () => null,
}

export const Dialog: KnobComponentGenerators<DialogProps> = {
footer: () => null,
}
Expand Down Expand Up @@ -79,6 +91,16 @@ export const Image: KnobComponentGenerators<ImageProps> = {
}),
}

export const Slider: KnobComponentGenerators<SliderProps> = {
// TODO: fix support for composed components
input: () => null,
}

export const Status: KnobComponentGenerators<StatusProps> = {
// TODO: fix support for composed components
icon: () => null,
}

export const Video: KnobComponentGenerators<VideoProps> = {
poster: ({ componentInfo, propName }) => ({
hook: useStringKnob,
Expand Down
7 changes: 4 additions & 3 deletions docs/src/components/ComponentPlayground/propGenerators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ export const color: KnobGenerator<string> = ({ propName, propDef, componentInfo,

export const size: KnobGenerator<string> = ({ propName, propDef, componentInfo }) => {
if (propDef.types.length > 1 || propDef.types[0].name !== 'SizeValue') {
throw new Error(
`A "${componentInfo.displayName}" for "size" prop defines type different than "SizeValue" it is not supported`,
)
return null
// throw new Error(
// `A "${componentInfo.displayName}" for "size" prop defines type different than "SizeValue" it is not supported`,
// )
}

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import * as React from 'react'
import { Slider } from '@fluentui/react'

const SliderExampleShorthand = () => <Slider />
const SliderExampleShorthand = () => (
<>
<Slider />
<Slider />
</>
Copy link
Member Author

Choose a reason for hiding this comment

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

Revert me plz

)

export default SliderExampleShorthand
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default sliderBehavior

export type SliderBehaviorProps = {
disabled?: boolean
// TODO: fix these SupportedIntrinsicInputProps['min']
min?: number
max?: number
value?: number
Expand Down
53 changes: 53 additions & 0 deletions packages/react-bindings/src/compose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as React from 'react'

type ComposeOptions = {
// TODO: better typings PLZ
className?: string
displayName: string
mapPropsToBehavior?: Function
mapPropsToStyles?: Function
handledProps?: string[]
overrideStyles?: boolean
}

const COMPOSE_CONFIG_PROP_NAME = '__unstable_config'
Copy link
Member

Choose a reason for hiding this comment

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

Should this go in the react-theming package? I thought that was where we were going to have compositional utilities.

We need a package which partners can easily use to compose components and provide a theme, with the least amount of other dependencies in it. This will allow a solid foundation to exist for building out an ecosystem of components.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think that composition covers only styling aspects 🤔


export type ComposableProps = { [COMPOSE_CONFIG_PROP_NAME]?: ComposeOptions }

export const compose = <UserProps, CProps = {}>(
Component: React.ComponentType<CProps>,
options: ComposeOptions,
): React.ComponentType<CProps & UserProps> => {
const ComposedComponent = Component.bind(null)

ComposedComponent.displayName = options.displayName

// We are passing config via props by setting default prop value
ComposedComponent.defaultProps = { ...(Component.defaultProps || {}) }
// @ts-ignore TODO PLS FIX ME
ComposedComponent.defaultProps[COMPOSE_CONFIG_PROP_NAME] = options

return ComposedComponent as any
}

export const useComposedConfig = <P extends ComposableProps>(props: P) => {
const { [COMPOSE_CONFIG_PROP_NAME]: options } = props

const {
className = '',
displayName,
handledProps = [],
mapPropsToBehavior = () => ({}),
mapPropsToStyles = () => ({}),
overrideStyles = false,
} = options || {}

return {
behaviorProps: mapPropsToBehavior(props),
styleProps: mapPropsToStyles(props),
className,
displayName,
handledProps: handledProps.concat(['__unstable_config']),
overrideStyles,
}
}
8 changes: 8 additions & 0 deletions packages/react-bindings/src/hooks/useStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ type UseStylesOptions<StyleProps extends PrimitiveProps> = {
mapPropsToStyles?: () => StyleProps
mapPropsToInlineStyles?: () => InlineStyleProps<StyleProps>
rtl?: boolean

__experimental_composeName?: string
__experimental_overrideStyles?: boolean
}

type UseStylesResult = {
Expand Down Expand Up @@ -67,6 +70,8 @@ const useStyles = <StyleProps extends PrimitiveProps>(
mapPropsToStyles = () => ({} as StyleProps),
mapPropsToInlineStyles = () => ({} as InlineStyleProps<StyleProps>),
rtl = false,
__experimental_composeName,
__experimental_overrideStyles,
} = options

// Stores debug information for component.
Expand All @@ -90,6 +95,9 @@ const useStyles = <StyleProps extends PrimitiveProps>(
enableStylesCaching,
enableVariablesCaching,
},

__experimental_composeName,
__experimental_overrideStyles,
})

return { classes, styles: resolvedStyles }
Expand Down
2 changes: 2 additions & 0 deletions packages/react-bindings/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ export * from './telemetry/types'

export { default as getElementType } from './utils/getElementType'
export { default as getUnhandledProps } from './utils/getUnhandledProps'

export * from './compose'
69 changes: 36 additions & 33 deletions packages/react/src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import * as React from 'react'
// @ts-ignore
import { ThemeContext } from 'react-fela'

import Image, { ImageProps } from '../Image/Image'
import Label, { LabelProps } from '../Label/Label'
import Status, { StatusProps } from '../Status/Status'
import AvatarImage, { AvatarImageProps } from './AvatarImage'
import AvatarLabel, { AvatarLabelProps } from './AvatarLabel'
import {
WithAsProp,
ShorthandValue,
Expand All @@ -23,6 +22,7 @@ import {
ProviderContextPrepared,
} from '../../types'
import { createShorthandFactory, UIComponentProps, commonPropTypes, SizeValue } from '../../utils'
import AvatarStatus, { AvatarStatusProps } from './AvatarStatus'

export interface AvatarProps extends UIComponentProps {
/**
Expand All @@ -31,10 +31,10 @@ export interface AvatarProps extends UIComponentProps {
accessibility?: Accessibility<never>

/** Shorthand for the image. */
image?: ShorthandValue<ImageProps>
image?: ShorthandValue<AvatarImageProps>

/** Shorthand for the label. */
label?: ShorthandValue<LabelProps>
label?: ShorthandValue<AvatarLabelProps>

/** The name used for displaying the initials of the avatar if the image is not provided. */
name?: string
Expand All @@ -43,7 +43,7 @@ export interface AvatarProps extends UIComponentProps {
size?: SizeValue

/** Shorthand for the status of the user. */
status?: ShorthandValue<StatusProps>
status?: ShorthandValue<AvatarStatusProps>

/** Custom method for generating the initials from the name property, which is shown if no image is provided. */
getInitials?: (name: string) => string
Expand Down Expand Up @@ -73,7 +73,7 @@ const Avatar: React.FC<WithAsProp<AvatarProps>> &
debugName: Avatar.displayName,
rtl: context.rtl,
})
const { classes, styles: resolvedStyles } = useStyles(Avatar.displayName, {
const { classes } = useStyles(Avatar.displayName, {
className: Avatar.className,
mapPropsToStyles: () => ({ size }),
mapPropsToInlineStyles: () => ({
Expand All @@ -87,34 +87,37 @@ const Avatar: React.FC<WithAsProp<AvatarProps>> &
const ElementType = getElementType(props)
const unhandledProps = getUnhandledProps(Avatar.handledProps, props)

// @ts-ignore
const imageElement = AvatarImage.create(image, {
defaultProps: () =>
getA11Props('image', {
fluid: true,
avatar: true,
title: name,
}),
})
// @ts-ignore
const statusElement = AvatarStatus.create(status, {
defaultProps: () =>
getA11Props('status', {
size,
}),
})
// @ts-ignore
const labelElement = AvatarLabel.create(label || {}, {
defaultProps: () =>
getA11Props('label', {
content: getInitials(name),
title: name,
size,
}),
})

const result = (
<ElementType {...getA11Props('root', { className: classes.root, ...unhandledProps })}>
{Image.create(image, {
defaultProps: () =>
getA11Props('image', {
fluid: true,
avatar: true,
title: name,
styles: resolvedStyles.image,
}),
})}
{!image &&
Label.create(label || {}, {
defaultProps: () =>
getA11Props('label', {
content: getInitials(name),
circular: true,
title: name,
styles: resolvedStyles.label,
}),
})}
{Status.create(status, {
defaultProps: () =>
getA11Props('status', {
size,
styles: resolvedStyles.status,
}),
})}
{imageElement}
{!image && labelElement}
{statusElement}
</ElementType>
)

Expand Down
19 changes: 19 additions & 0 deletions packages/react/src/components/Avatar/AvatarImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { compose } from '@fluentui/react-bindings'

import { createShorthandFactory } from '../../utils'
import Image, { ImageProps } from '../Image/Image'

export interface AvatarImageProps extends ImageProps {}

const AvatarImage = compose(Image, {
displayName: 'AvatarImage',
})

// @ts-ignore
AvatarImage.create = createShorthandFactory({
// @ts-ignore
Component: AvatarImage,
mappedProp: 'src',
})

export default AvatarImage
22 changes: 22 additions & 0 deletions packages/react/src/components/Avatar/AvatarLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { compose } from '@fluentui/react-bindings'

import { createShorthandFactory, SizeValue } from '../../utils'
import Box, { BoxProps } from '../Box/Box'

export interface AvatarLabelProps extends BoxProps {
size?: SizeValue
}

const AvatarLabel = compose(Box, {
displayName: 'AvatarLabel',
mapPropsToStyles: props => ({ size: props.size }),
})

// @ts-ignore
AvatarLabel.create = createShorthandFactory({
// @ts-ignore
Component: AvatarLabel,
mappedProp: 'content',
})

export default AvatarLabel
19 changes: 19 additions & 0 deletions packages/react/src/components/Avatar/AvatarStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { compose } from '@fluentui/react-bindings'

import { createShorthandFactory } from '../../utils'
import Status, { StatusProps } from '../Status/Status'

export interface AvatarStatusProps extends StatusProps {}

const AvatarStatus = compose(Status, {
displayName: 'AvatarStatus',
})

// @ts-ignore
AvatarStatus.create = createShorthandFactory({
// @ts-ignore
Component: AvatarStatus,
mappedProp: 'state',
})

export default AvatarStatus
Loading