Skip to content

Commit 40903c2

Browse files
authored
feat: introduce children function to shorthands (#4029)
* feat: introduce `children` function to shorthands * update docs * more updates * Update src/generic.d.ts
1 parent 2d368b9 commit 40903c2

File tree

7 files changed

+186
-56
lines changed

7 files changed

+186
-56
lines changed

docs/src/pages/ShorthandProps.mdx

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,19 @@ There are several forms of shorthand values that can be provided, but all of the
1919
Each component's shorthand has an associated default element type. For example, by default, there is `<Icon />` element rendered for `Button`'s icon shorthand. It is possible to customize props of this default element by providing props object as shorthand value:
2020

2121
```jsx
22-
// 💡 'color' and 'name' will be used as <Icon /> element's props
23-
<Button content='Like' icon={{ color: 'red', name: 'like' }} />
22+
<>
23+
{/* 💡 'color' and 'name' will be used as <Icon /> element's props */}
24+
<Button content='Like' icon={{ color: 'red', name: 'like' }} />
25+
{/* 💡 you can also add handlers and any DOM props to shorthands */}
26+
<Input
27+
action={{
28+
icon: 'search',
29+
onClick: () => console.log('An action was clicked!'),
30+
}}
31+
actionPosition='left'
32+
placeholder='Search...'
33+
/>
34+
</>
2435
```
2536

2637
## String as value
@@ -52,7 +63,11 @@ This works because `name` is the default prop of shorthand's `<Icon />` element.
5263
It is also possible to pass falsy values (`false`, `null` or `undefined`) - in that case there will be nothing rendered for the component's shorthand.
5364

5465
```jsx
55-
<Dropdown icon={null} />
66+
<>
67+
{/* 💡 hides a toogle icon in `Dropdown` */}
68+
<Dropdown icon={null} />
69+
<Dropdown icon={false} />
70+
</>
5671
```
5772

5873
## React Element as value
@@ -68,9 +83,9 @@ There are cases where it might be necessary to customize the element tree that w
6883
<Message.Content>
6984
There is a very important caveat here, though: whenever React Element is directly used as a
7085
shorthand value, all props that Semantic UI React has created for the shorthand's Component will
71-
be spread on the passed element. This means that the provided element should be able to handle props
72-
- while this requirement is satisfied for all SUIR components, you should be aware of that when
73-
either HTML or any third-party elements are provided.
86+
be spread on the passed element. This means that the provided element should be able to handle
87+
props - while this requirement is satisfied for all SUIR components, you should be aware of that
88+
when either HTML or any third-party elements are provided.
7489
</Message.Content>
7590
</Message>
7691

@@ -82,26 +97,47 @@ Due to this limitation, you should strive to use other options for shorthand val
8297

8398
However, there still might be cases where it would be impossible to use the object form of the shorthand value - for example, you might want to render some custom elements tree for the shorthand. In that case, function value should be used.
8499

85-
## Function as value
100+
## Render props via `children`
86101

87-
Providing function as a shorthand value is the most involving but, at the same time, the most powerful option for customizing component's shorthand. The only requirements for this function are:
102+
Providing function as a shorthand value is the most involving but, at the same time, the most powerful option for customizing component's shorthand. It should return React Element as a result or `null`.
88103

89-
- it should finish synchronously
90-
- it should return React Element as a result
104+
```jsx
105+
<Button
106+
content='Like'
107+
icon={{
108+
children: (Component, componentProps) => <Component {...componentProps} color='red' />,
109+
name: 'question',
110+
}}
111+
/>
112+
```
91113

92-
Thus, in its simplest form, it could be used the following way:
114+
### Customizing rendered shorthand
115+
116+
There is another use case when render function is very useful for - this is the case where custom element's tree should be rendered for the shorthand. As you might recall, there is a problem that might happen when React Element is provided directly as shorthand value - in that case, props are not propagated to rendered. In order to avoid that the following strategy should be considered:
93117

94118
```jsx
95119
<Button
96120
content='Like'
97-
icon={(Component, componentProps) => <Component {...componentProps} color='red' name='like' />}
121+
icon={{ children: (Component, componentProps) => <Label basic>+1</Label> }}
98122
/>
99123
```
100124

101-
## Customizing rendered shorthand
125+
## Function as value (_deprecated_)
102126

103-
There is another use case when render function is very useful for - this is the case where custom element's tree should be rendered for the shorthand. As you might recall, there is a problem that might happen when React Element is provided directly as shorthand value - in that case, props are not propagated to rendered. In order to avoid that the following strategy should be considered:
127+
<Message warning>
128+
This usage is deprecated and will be removed in v3, please use render props instead.
129+
</Message>
130+
131+
Providing function as a shorthand value is the most involving but, at the same time, the most powerful option for customizing component's shorthand. The only requirements for this function are:
132+
133+
- it should finish synchronously
134+
- it should return React Element as a result
135+
136+
Thus, in its simplest form, it could be used the following way:
104137

105138
```jsx
106-
<Button content='Like' icon={(Component, componentProps) => <Label basic>+1</Label>} />
139+
<Button
140+
content='Like'
141+
icon={(Component, componentProps) => <Component {...componentProps} color='red' name='like' />}
142+
/>
107143
```

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"test": "cross-env NODE_ENV=test node -r @babel/register ./node_modules/karma/bin/karma start karma.conf.babel.js",
4040
"test:watch": "yarn test --no-single-run",
4141
"test:umd": "gulp build:dist:umd && node test/umd.js",
42-
"tsd:test": "gulp build:dist:commonjs:tsd && tsc -p ./ && rimraf test/typings.js"
42+
"tsd:test": "gulp build:dist:commonjs:tsd && tsc -p ./ --noEmit"
4343
},
4444
"husky": {
4545
"hooks": {

src/generic.d.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,31 @@ export interface StrictHtmlSpanProps {
5656
// Types
5757
// ======================================================
5858

59+
/**
60+
* @deprecated Will be removed in v3
61+
*/
5962
export type SemanticShorthandItemFunc<TProps> = (
60-
component: React.ReactType<TProps>,
63+
component: React.ElementType<TProps>,
6164
props: TProps,
6265
children?: React.ReactNode | React.ReactNodeArray,
6366
) => React.ReactElement<any> | null
6467

68+
export type ShorthandRenderFunction<C extends React.ElementType, P> = (
69+
Component: C,
70+
props: P,
71+
) => React.ReactNode
72+
6573
export type SemanticShorthandCollection<TProps> = SemanticShorthandItem<TProps>[]
6674
export type SemanticShorthandContent = React.ReactNode
67-
export type SemanticShorthandItem<TProps> =
75+
export type SemanticShorthandItem<TProps extends Record<string, any>> =
6876
| React.ReactNode
69-
| TProps
7077
| SemanticShorthandItemFunc<TProps>
78+
| (Omit<TProps, 'children'> & {
79+
// Not all TProps can have `children`, without this condition it will match to "any"
80+
children?: TProps extends { children: any }
81+
? TProps['children'] | ShorthandRenderFunction<React.ElementType<TProps>, TProps>
82+
: ShorthandRenderFunction<React.ElementType<TProps>, TProps>
83+
})
7184

7285
// ======================================================
7386
// Styling

src/lib/factories.js

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import _ from 'lodash'
21
import cx from 'clsx'
2+
import _ from 'lodash'
33
import * as React from 'react'
44

5+
const DEPRECATED_CALLS = {}
6+
57
// ============================================================
68
// Factories
79
// ============================================================
@@ -22,8 +24,11 @@ export function createShorthand(Component, mapValueToProps, val, options = {}) {
2224
if (typeof Component !== 'function' && typeof Component !== 'string') {
2325
throw new Error('createShorthand() Component must be a string or function.')
2426
}
27+
2528
// short circuit noop values
26-
if (_.isNil(val) || _.isBoolean(val)) return null
29+
if (_.isNil(val) || _.isBoolean(val)) {
30+
return null
31+
}
2732

2833
const valIsString = _.isString(val)
2934
const valIsNumber = _.isNumber(val)
@@ -108,15 +113,35 @@ export function createShorthand(Component, mapValueToProps, val, options = {}) {
108113
// ----------------------------------------
109114

110115
// Clone ReactElements
111-
if (valIsReactElement) return React.cloneElement(val, props)
116+
if (valIsReactElement) {
117+
return React.cloneElement(val, props)
118+
}
119+
120+
if (typeof props.children === 'function') {
121+
return props.children(Component, { ...props, children: undefined })
122+
}
112123

113124
// Create ReactElements from built up props
114125
if (valIsPrimitiveValue || valIsPropsObject) {
115126
return React.createElement(Component, props)
116127
}
117128

118129
// Call functions with args similar to createElement()
119-
if (valIsFunction) return val(Component, props, props.children)
130+
// TODO: V3 remove the implementation
131+
if (valIsFunction) {
132+
if (process.env.NODE_ENV !== 'production') {
133+
if (!DEPRECATED_CALLS[Component]) {
134+
DEPRECATED_CALLS[Component] = true
135+
136+
// eslint-disable-next-line no-console
137+
console.warn(
138+
`Warning: There is a deprecated shorthand function usage for "${Component}". It is deprecated and will be removed in v3 release. Please follow our upgrade guide: https://github.com/Semantic-Org/Semantic-UI-React/pull/4029`,
139+
)
140+
}
141+
}
142+
143+
return val(Component, props, props.children)
144+
}
120145
/* eslint-enable react/prop-types */
121146
}
122147

test/specs/lib/factories-test.js

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -433,44 +433,80 @@ describe('factories', () => {
433433
itOverridesDefaultPropsWithFalseyProps('props object', {
434434
value: { undef: undefined, nil: null, zero: 0, empty: '' },
435435
})
436+
437+
describe('children', () => {
438+
it('is called once', () => {
439+
const children = sandbox.spy()
440+
441+
getShorthand({ value: { children } })
442+
children.should.have.been.calledOnce()
443+
})
444+
445+
it('is called with Component, props, children', () => {
446+
const children = sandbox.spy(() => <div />)
447+
448+
getShorthand({ Component: 'p', value: { children } })
449+
children.should.have.been.calledWithExactly('p', { children: undefined })
450+
})
451+
452+
it('receives defaultProps in its props argument', () => {
453+
const children = sandbox.spy(() => <div />)
454+
const defaultProps = { defaults: true }
455+
456+
getShorthand({ Component: 'p', defaultProps, value: { children } })
457+
children.should.have.been.calledWithExactly('p', { ...defaultProps, children: undefined })
458+
})
459+
460+
it('receives overrideProps in its props argument', () => {
461+
const children = sandbox.spy(() => <div />)
462+
const overrideProps = { overrides: true }
463+
464+
getShorthand({ Component: 'p', overrideProps, value: { children } })
465+
children.should.have.been.calledWithExactly('p', {
466+
...overrideProps,
467+
children: undefined,
468+
})
469+
})
470+
})
436471
})
437472

473+
// TODO: V3 remove this test
438474
describe('from a function', () => {
475+
beforeEach(() => {
476+
consoleUtil.disableOnce()
477+
})
478+
439479
itReturnsAValidElement(() => <div />)
440480
itDoesNotIncludePropsFromMapValueToProps(() => <div />)
441481

442482
it('is called once', () => {
443483
const spy = sandbox.spy()
444484

445485
getShorthand({ value: spy })
446-
447486
spy.should.have.been.calledOnce()
448487
})
449488

450489
it('is called with Component, props, children', () => {
451-
const spy = sandbox.spy(() => <div />)
452-
453-
getShorthand({ Component: 'p', value: spy })
490+
const value = sandbox.spy(() => <div />)
454491

455-
spy.should.have.been.calledWithExactly('p', {}, undefined)
492+
getShorthand({ Component: 'p', value })
493+
value.should.have.been.calledWithExactly('p', {}, undefined)
456494
})
457495

458496
it('receives defaultProps in its props argument', () => {
459-
const spy = sandbox.spy(() => <div />)
497+
const value = sandbox.spy(() => <div />)
460498
const defaultProps = { defaults: true }
461499

462-
getShorthand({ Component: 'p', defaultProps, value: spy })
463-
464-
spy.should.have.been.calledWithExactly('p', defaultProps, undefined)
500+
getShorthand({ Component: 'p', defaultProps, value })
501+
value.should.have.been.calledWithExactly('p', defaultProps, undefined)
465502
})
466503

467504
it('receives overrideProps in its props argument', () => {
468-
const spy = sandbox.spy(() => <div />)
505+
const value = sandbox.spy(() => <div />)
469506
const overrideProps = { overrides: true }
470507

471-
getShorthand({ Component: 'p', overrideProps, value: spy })
472-
473-
spy.should.have.been.calledWithExactly('p', overrideProps, undefined)
508+
getShorthand({ Component: 'p', overrideProps, value })
509+
value.should.have.been.calledWithExactly('p', overrideProps, undefined)
474510
})
475511
})
476512

test/specs/modules/Search/Search-test.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import SearchCategory from 'src/modules/Search/SearchCategory'
88
import SearchResult from 'src/modules/Search/SearchResult'
99
import SearchResults from 'src/modules/Search/SearchResults'
1010
import * as common from 'test/specs/commonTests'
11-
import { domEvent, sandbox } from 'test/utils'
11+
import { consoleUtil, domEvent, sandbox } from 'test/utils'
1212

1313
let attachTo
1414
let options
@@ -743,6 +743,9 @@ describe('Search', () => {
743743
})
744744

745745
it(`will not merge for a function`, () => {
746+
// TODO: V3 remove this test and simplify the implementation
747+
consoleUtil.disableOnce()
748+
746749
wrapperMount(<Search input={{ input: (Component, props) => <Component {...props} /> }} />)
747750
const input = wrapper.find('input')
748751

test/typings.tsx

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,49 @@
11
import * as React from 'react'
22
import { Button, Dropdown } from '../index'
33

4-
export const BasicAssert = () => <Button />
4+
export const BasicAssert = () => (
5+
<>
6+
<Button />
7+
<Button content='Foo' />
8+
</>
9+
)
510

611
export const ShorthandItemElementAssert = () => (
712
<Dropdown additionLabel={<i style={{ color: 'red' }}>Custom Language: </i>} />
813
)
914

1015
export const ShorthandItemFuncAssert = () => (
11-
<Button
12-
content='Foo'
13-
icon={(Component, props) => (
14-
<div className='bar'>
15-
<Component name={props.name} />
16-
</div>
17-
)}
18-
/>
16+
<>
17+
<Button
18+
icon={{
19+
children: (Component, props) => (
20+
<div className='bar'>
21+
<Component name={props.name} />
22+
</div>
23+
),
24+
}}
25+
/>
26+
<Button
27+
label={{
28+
children: (Component, props) => (
29+
<div className='bar'>
30+
<Component active={props.active}>{props.children}</Component>
31+
</div>
32+
),
33+
}}
34+
/>
35+
<Button label={{ children: <div className='bar' /> }} />
36+
</>
1937
)
2038

21-
export const ShorthandItemFuncChildren = () => (
22-
<Button
23-
content='Foo'
24-
label={(Component, props, children) => (
25-
<div className='bar'>
26-
<Component active={props.active}>{children}</Component>
27-
</div>
28-
)}
29-
/>
39+
export const ShorthandItemFuncNullAssert = () => (
40+
<Button content='Foo' icon={{ children: () => null }} />
3041
)
3142

32-
export const ShorthandItemFuncNullAssert = () => <Button content='Foo' icon={() => null} />
43+
export const ShorthandItemBooleanAssert = () => (
44+
<>
45+
<Button icon />
46+
<Button icon={false} />
47+
<Button label={false} />
48+
</>
49+
)

0 commit comments

Comments
 (0)