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

Commit 6da13dc

Browse files
committed
feat(slot): create generic slot component
1 parent 14b860e commit 6da13dc

File tree

8 files changed

+121
-23
lines changed

8 files changed

+121
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1919

2020
### Features
2121
- Make `content` to be a shorthand prop for `Popup` @kuzhelov ([#322](https://github.com/stardust-ui/react/pull/322))
22+
- Add generic `Slot` component (used internally) and use it as shorthand for `Button` `content` prop @Bugaa92 ([#335](https://github.com/stardust-ui/react/pull/335))
2223

2324
<!--------------------------------[ v0.9.0 ]------------------------------- -->
2425
## [v0.9.0](https://github.com/stardust-ui/react/tree/v0.9.0) (2018-10-07)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react'
2+
3+
export default () => (
4+
<div>
5+
A <code>Slot</code> is a basic component (no default styles) used mainly as a way to create
6+
shorthand elements as a part of more complex components (e.g.: <code>content</code> prop for{' '}
7+
<code>Button</code> and other components)
8+
</div>
9+
)

src/components/Button/Button.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as _ from 'lodash'
44

55
import { UIComponent, childrenExist, customPropTypes, createShorthandFactory } from '../../lib'
66
import Icon from '../Icon'
7+
import Slot from '../Slot'
78
import { buttonBehavior } from '../../lib/accessibility'
89
import { Accessibility } from '../../lib/accessibility/interfaces'
910
import { ComponentVariablesInput, ComponentPartStyle } from '../../../types/theme'
@@ -24,7 +25,7 @@ export interface IButtonProps {
2425
circular?: boolean
2526
className?: string
2627
disabled?: boolean
27-
content?: React.ReactNode
28+
content?: ShorthandValue
2829
fluid?: boolean
2930
icon?: ShorthandValue
3031
iconOnly?: boolean
@@ -160,7 +161,9 @@ class Button extends UIComponent<Extendable<IButtonProps>, IButtonState> {
160161
>
161162
{hasChildren && children}
162163
{!hasChildren && iconPosition !== 'after' && this.renderIcon(variables, styles)}
163-
{!hasChildren && content && <span className={classes.content}>{content}</span>}
164+
{Slot.create(!hasChildren && content, {
165+
defaultProps: { as: 'span', className: classes.content },
166+
})}
164167
{!hasChildren && iconPosition === 'after' && this.renderIcon(variables, styles)}
165168
</ElementType>
166169
)

src/components/Slot/Slot.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as React from 'react'
2+
import * as PropTypes from 'prop-types'
3+
import { customPropTypes, UIComponent, childrenExist, createShorthandFactory } from '../../lib'
4+
import { Extendable } from '../../../types/utils'
5+
import { ComponentVariablesInput, ComponentPartStyle } from '../../../types/theme'
6+
7+
export interface ISlotProps {
8+
as?: any
9+
className?: string
10+
content?: any
11+
styles?: ComponentPartStyle<ISlotProps, any>
12+
variables?: ComponentVariablesInput
13+
}
14+
15+
/**
16+
* A Slot is a basic component (no default styles)
17+
*/
18+
class Slot extends UIComponent<Extendable<ISlotProps>, any> {
19+
static create: Function
20+
21+
static className = 'ui-slot'
22+
23+
static displayName = 'Slot'
24+
25+
static propTypes = {
26+
/** An element type to render as (string or function). */
27+
as: customPropTypes.as,
28+
29+
/** Additional CSS class name(s) to apply. */
30+
className: PropTypes.string,
31+
32+
/** Shorthand for primary content. */
33+
content: PropTypes.any,
34+
35+
/** Additional CSS styles to apply to the component instance. */
36+
styles: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
37+
38+
/** Override for theme site variables to allow modifications of component styling via themes. */
39+
variables: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
40+
}
41+
42+
static defaultProps = {
43+
as: 'div',
44+
}
45+
46+
renderComponent({ ElementType, classes, rest }) {
47+
const { children, content } = this.props
48+
49+
return (
50+
<ElementType {...rest} className={classes.root}>
51+
{childrenExist(children) ? children : content}
52+
</ElementType>
53+
)
54+
}
55+
}
56+
57+
Slot.create = createShorthandFactory(Slot, content => ({ content }))
58+
59+
export default Slot

src/components/Slot/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './Slot'

test/specs/commonTests/isConformant.tsx

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { mount as enzymeMount } from 'enzyme'
44
import * as ReactDOMServer from 'react-dom/server'
55
import { ThemeProvider } from 'react-fela'
66

7+
import isExportedAtTopLevel from './isExportedAtTopLevel'
78
import { assertBodyContains, consoleUtil, syntheticEvent } from 'test/utils'
89
import helpers from './commonHelpers'
910

@@ -12,6 +13,13 @@ import { felaRenderer } from 'src/lib'
1213
import { FocusZone } from 'src/lib/accessibility/FocusZone'
1314
import { FOCUSZONE_WRAP_ATTRIBUTE } from 'src/lib/accessibility/FocusZone/focusUtilities'
1415

16+
export interface IConformant {
17+
eventTargets?: object
18+
requiredProps?: object
19+
exportedAtTopLevel?: boolean
20+
rendersPortal?: boolean
21+
}
22+
1523
export const mount = (node, options?) => {
1624
return enzymeMount(
1725
<ThemeProvider theme={{ renderer: felaRenderer }}>{node}</ThemeProvider>,
@@ -24,11 +32,17 @@ export const mount = (node, options?) => {
2432
* @param {React.Component|Function} Component A component that should conform.
2533
* @param {Object} [options={}]
2634
* @param {Object} [options.eventTargets={}] Map of events and the child component to target.
35+
* @param {boolean} [options.exportedAtTopLevel=false] Is this component exported as top level API
2736
* @param {boolean} [options.rendersPortal=false] Does this component render a Portal powered component?
2837
* @param {Object} [options.requiredProps={}] Props required to render Component without errors or warnings.
2938
*/
30-
export default (Component, options: any = {}) => {
31-
const { eventTargets = {}, requiredProps = {}, rendersPortal = false } = options
39+
export default (Component, options: IConformant = {}) => {
40+
const {
41+
eventTargets = {},
42+
exportedAtTopLevel = true,
43+
requiredProps = {},
44+
rendersPortal = false,
45+
} = options
3246
const { throwError } = helpers('isConformant', Component)
3347

3448
const componentType = typeof Component
@@ -100,28 +114,10 @@ export default (Component, options: any = {}) => {
100114
expect(constructorName).toEqual(info.filenameWithoutExt)
101115
})
102116

103-
// ----------------------------------------
104-
// Is exported or private
105-
// ----------------------------------------
106-
// detect components like: stardust.H1
107-
const isTopLevelAPIProp = _.has(stardust, constructorName)
108-
109117
// find the apiPath in the stardust object
110118
const foundAsSubcomponent = _.isFunction(_.get(stardust, info.apiPath))
111119

112-
// require all components to be exported at the top level
113-
test('is exported at the top level', () => {
114-
const message = [
115-
`'${info.displayName}' must be exported at top level.`,
116-
"Export it in 'src/index.js'.",
117-
].join(' ')
118-
119-
expect({ isTopLevelAPIProp, message }).toEqual({
120-
message,
121-
isTopLevelAPIProp: true,
122-
})
123-
})
124-
120+
exportedAtTopLevel && isExportedAtTopLevel(constructorName, info.displayName)
125121
if (info.isChild) {
126122
test('is a static component on its parent', () => {
127123
const message =
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as _ from 'lodash'
2+
import * as stardust from 'src/'
3+
4+
// ----------------------------------------
5+
// Is exported or private
6+
// ----------------------------------------
7+
// detect components like: stardust.H1
8+
export default (constructorName: string, displayName: string) => {
9+
const isTopLevelAPIProp = _.has(stardust, constructorName)
10+
11+
// require all components to be exported at the top level
12+
test('is exported at the top level', () => {
13+
const message = [
14+
`'${displayName}' must be exported at top level.`,
15+
"Export it in 'src/index.js'.",
16+
].join(' ')
17+
18+
expect({ isTopLevelAPIProp, message }).toEqual({
19+
message,
20+
isTopLevelAPIProp: true,
21+
})
22+
})
23+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { isConformant } from 'test/specs/commonTests'
2+
import Slot from 'src/components/Slot'
3+
4+
describe('Slot', () => {
5+
isConformant(Slot, { exportedAtTopLevel: false })
6+
})

0 commit comments

Comments
 (0)