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

Commit

Permalink
feat(factories): use children in shorthands as render props (#1951)
Browse files Browse the repository at this point in the history
* feat(factories): add renderProps via `children` to shorthands

* do not pass children down

* use new callback

* fix types

* update docs

* fix usage in `Dropdown`

* fix warnings in tests

* add changelog

* improve typings

* fix EditorToolbar

* add link to React docs

* changelog
  • Loading branch information
layershifter authored and miroslavstastny committed Nov 26, 2019
1 parent 7d07123 commit 7498901
Show file tree
Hide file tree
Showing 17 changed files with 221 additions and 248 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Update Silver color scheme, adding `foregroundHover`, `foregroundPressed` and `background` definitions @pompomon ([#2078](https://github.com/microsoft/fluent-ui-react/pull/2078))
- Expanding experimental accessibility schema to more components @mshoho ([#2052](https://github.com/stardust-ui/react/pull/2052))
- Add base `Carousel` component @silviuavram ([#1979](https://github.com/microsoft/fluent-ui-react/pull/1979))
- Add support for render props pattern via `children` prop to shorthands @layershifter ([#1951](https://github.com/stardust-ui/react/pull/1951))

### Documentation
- Add usage example for `Tooltip` on disabled elements @mnajdova ([#2091](https://github.com/microsoft/fluent-ui-react/pull/2091))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,9 @@ class ComponentExample extends React.Component<ComponentExampleProps, ComponentE
onClick: this.resetSourceCode,
disabled: !wasCodeChanged,
},
render =>
render({ content: 'Copy' }, (Component, props) => (
{
content: 'Copy',
children: (Component, props) => (
<CopyToClipboard key="copy" value={currentCode}>
{(active, onClick) => (
<Component
Expand All @@ -354,7 +355,8 @@ class ComponentExample extends React.Component<ComponentExampleProps, ComponentE
/>
)}
</CopyToClipboard>
)),
),
},
{
disabled: currentCodeLanguage !== 'ts',
icon: 'github',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ const AvatarExampleImageCustomizationShorthand = () => (
/>
&emsp;
<Avatar
image={render =>
image={{
name: 'chess rook',
// This example does not react to the avatar size variable
// and otherwise produces bad results when border is applied compared to "normal" image
render({ name: 'chess rook' }, (ComponentType, props) => (
children: (ComponentType, props) => (
<Icon
{...{ ...props, avatar: undefined, fluid: undefined }}
name="lock"
Expand All @@ -26,8 +27,8 @@ const AvatarExampleImageCustomizationShorthand = () => (
variables={{ color: 'blue' }}
styles={{ boxSizing: 'border-box', padding: '8px' }}
/>
))
}
),
}}
status={{ color: 'green', icon: 'stardust-checkmark', title: 'Available' }}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
import * as React from 'react'
import { Menu, Tooltip } from '@fluentui/react'

const itemRenderer = (MenuItem, props) => {
const { tooltip = '', ...rest } = props

return (
<Tooltip content={tooltip}>
<MenuItem {...rest} />
</Tooltip>
)
}
const items = [
{ key: 'editorials', content: 'Editorials', tooltip: 'Click for opening Editorials' },
{ key: 'review', content: 'Reviews', tooltip: 'Click for opening Reviews' },
{ key: 'events', content: 'Upcoming Events', tooltip: 'Click for opening Upcoming Events' },
{
key: 'editorials',
content: 'Editorials',
tooltip: 'Click for opening Editorials',
children: itemRenderer,
},
{
key: 'review',
content: 'Reviews',
tooltip: 'Click for opening Reviews',
children: itemRenderer,
},
{
key: 'events',
content: 'Upcoming Events',
tooltip: 'Click for opening Upcoming Events',
children: itemRenderer,
},
]

const MenuExampleWithTooltip = () => (
<Menu
defaultActiveIndex={0}
items={items.map(item => render =>
render(
/* what to render */
item,

/* how to render */
(MenuItem, props) => {
const { tooltip = '', ...rest } = props || {}
return (
<Tooltip content={tooltip}>
<MenuItem {...rest} />
</Tooltip>
)
},
),
)}
/>
)
const MenuExampleWithTooltip = () => <Menu defaultActiveIndex={0} items={items} />

export default MenuExampleWithTooltip
Original file line number Diff line number Diff line change
Expand Up @@ -517,26 +517,23 @@ const layouts: Record<CustomToolbarProps['layout'], CustomToolbarLayout> = {
const CustomToolbar: React.FunctionComponent<CustomToolbarProps> = props => {
const { layout = 'standard' } = props

const items = layouts[layout](props).map(item =>
_.isNil((item as any).tooltip)
? item
: render =>
render(
item, // rendering Tooltip for the Toolbar Item
(ToolbarItem, props) => {
const { tooltip, key, ...rest } = props // Adding tooltipAsLabelBehavior as the ToolbarItems contains only icon

return (
<Tooltip
key={key}
trigger={<ToolbarItem {...rest} />}
accessibility={tooltipAsLabelBehavior}
content={tooltip}
/>
)
},
),
)
const items = layouts[layout](props).map((item: ToolbarItemProps) => ({
...item,
children: (item as any).tooltip
? (ToolbarItem, props) => {
const { tooltip, key, ...rest } = props // Adding tooltipAsLabelBehavior as the ToolbarItems contains only icon

return (
<Tooltip
key={key}
trigger={<ToolbarItem {...rest} />}
accessibility={tooltipAsLabelBehavior}
content={tooltip}
/>
)
}
: null,
}))

return <Toolbar variables={{ isCt: true }} items={items} />
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as React from 'react'
import * as _ from 'lodash'
import {
Toolbar,
Tooltip,
Expand Down Expand Up @@ -58,27 +57,24 @@ const ToolbarExampleShorthand = () => {
menuOpen: moreMenuOpen,
onMenuOpenChange: (e, { menuOpen }) => setMoreMenuOpen(menuOpen),
},
].map(item =>
_.isNil(item.tooltip)
? item
: render =>
render(
item,
// rendering Tooltip for the Toolbar Item
(ToolbarItem, props) => {
const { tooltip, key, ...rest } = props
// Adding tooltipAsLabelBehavior as the ToolbarItems contains only icon
return (
<Tooltip
key={key}
trigger={<ToolbarItem {...rest} />}
accessibility={tooltipAsLabelBehavior}
content={tooltip}
/>
)
},
),
)}
].map(item => ({
...item,
// rendering Tooltip for the Toolbar Item
children: item.tooltip
? (ToolbarItem, props) => {
const { tooltip, key, ...rest } = props
// Adding tooltipAsLabelBehavior as the ToolbarItems contains only icon
return (
<Tooltip
key={key}
trigger={<ToolbarItem {...rest} />}
accessibility={tooltipAsLabelBehavior}
content={tooltip}
/>
)
}
: undefined,
}))}
/>
)
}
Expand Down
139 changes: 33 additions & 106 deletions docs/src/pages/ShorthandProps.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,126 +56,53 @@ It is also possible to pass falsy values (`false`, `null` or
<Dropdown toggleIndicator={null} />
```

## Function as value
## Using Render Props

Providing function as a shorthand value is the most involving but, at the same time, the most
powerful option for customizing component's slot. The only requirements for this function are:

- it should finish synchronously
- it should return React Element as a result

Thus, in its simplest form, it could be used the following way:

```jsx
<Button icon={() => <Icon name="emoji" />} />
```

However, if it will be just about that, this function form wouldn't introduce any additional value. So, lets take a look on the scenarios where it is really helpful.

### 'Render' callback argument

When function is provided as a shorthand value, there is a `render` callback provided
as its argument. This `render` argument is a function that 'knows' how to render all
the previously considered shorthand types - i.e. primitive values, objects, React Elements.

Here is an example that represents this:

```jsx
<>
{/* All three have the same effect: */}
<Button icon="emoji" />
{/* render() transforms string to <Icon /> element */}
<Button icon={render => render('emoji')} />
{/* render() transforms object to <Icon /> element */}
<Button icon={render => render({ name: 'emoji' })} />
</>
```

### Handle Async data loading scenarios

This 'render callback' arg makes it possible for the client to tackle scenarios where slot's data may be fetched asynchronously.

Consider the following scenario: suppose we need to render `Button`'s icon, but the
problem is that icon's name is fetched from some remote source. Thus, we cannot do it as
simply as that, as icon's name is not known at the moment `Button` is rendered:

```jsx
<Button icon={{ name: /* unknown yet */ undefined }} />
```

It is quite common case that there is some component that is responsible for data fetching
abstraction - and it is possible to 'tell' this component what should be rendered while data
is loading, as well as what should be rendered when data is fetched.
Our components implement [`render props`](https://reactjs.org/docs/render-props.html) technique to allow customize rendered output or wrap component slots. All Stardust components support it via `children` prop on shorthand objects, for example:

```jsx
<AsyncData
getData={fetch('https://some/url')}
onLoading={<div>Loading..</div>} // renders when data is loading
onLoaded={data => <div>Loaded Data: {data}</div>} // renders once data is fetched
<Attachment
header="Contacts.docx"
action={{
content: '?',
children: (
Component /* evaluated `ElementType`, `Button` in this case */,
props /* necessary attributes (including accessibility) that should be applied */,
) => <Tooltip content="Help..." trigger={<Component {...props} />} />,
}}
/>
```

Lets suppose that we want to render `loading` icon before data is fetched, and switch it with
the icon which `name` will be provided by fetched `data` object. Also, note
that we have `render` arg that knows _how_ to render `Button`'s icons.

Putting it all together, we will address our async data fetch case:
You can also use this technique to completely replace slot contents:

```jsx
<Button
icon={render => (
<AsyncData
getData={fetch('https://some/url')}
onLoading={<Icon name="loading" />}
/* using 'render' arg here to render icon from loaded na */
onLoaded={data => render({ name: data.iconName })}
/>
)}
content="Click me"
icon={{
/* ⚠️ Replacement without applying props can break accessibility and styling concerns */
children: () => <FaBeer />,
}}
/>
```

Note that this example can be further simplified, as `render` can be used to render
'loading' icon as well:
### Use with shorthand collections

```jsx
<Button
icon={render => (
<AsyncData
getData={fetch('https://some/url')}
/* using 'render' arg here to render loading icon as well */
onLoading={render('loading')}
onLoaded={data => render({ name: data.iconName })}
/>
)}
/>
```

### Customizing rendered tree

There is another use case that `render` callback arg is very useful for - this is the
case where custom element's tree should be rendered for the slot, but, at the same time, all
the evaluated styles and accessibility behaviors should be preserved for rendered custom
element.
The same approach can be used with components that accept shorthand collections, for example `Menu`:

```jsx
<Button
icon={render =>
render(
/* what to render */
{ name: 'emoji' },

/* how to render */
(ComponentType, props) => (
<div class="my-icon-container">
<i class="my-emoji-icon" {...props.accessibility.root} />
</div>
),
)
}
<Menu
items={[
{
key: 'first',
content: 'First',
tooltip: 'First item',
children: (Component, props) => {
/* ☝️ `tooltip` comes from shorthand object */
const { tooltip, ...rest } = props

return <Tooltip content={tooltip} trigger={<Component {...props} />} />
},
},
]}
/>
```

Here we are passing a recipe of how evaluated `ComponentType` (`Icon` in our
case, which is ignored by the rendered tree) and `props` should be passed to custom
elements tree. Note that `props` object, amongst others, contains all the necessary
evaluated accessibility attributes that should be applied to rendered element.
8 changes: 4 additions & 4 deletions docs/src/prototypes/AsyncShorthand/AsyncShorthand.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@ class CustomChatMessage extends React.Component {
iconOnly
className="actions"
items={[
{ key: 'a', icon: 'thumbs up' },
{ key: 'b', icon: 'user' },
{ key: 'c', icon: 'ellipsis horizontal' },
].map(item => render => render(item, this.renderMenuItem))}
{ key: 'a', icon: 'thumbs up', children: this.renderMenuItem },
{ key: 'b', icon: 'user', children: this.renderMenuItem },
{ key: 'c', icon: 'ellipsis horizontal', children: this.renderMenuItem },
]}
/>
</div>
}
Expand Down
Loading

0 comments on commit 7498901

Please sign in to comment.