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

[RFC] feat(slot): create generic slot component #335

Merged
merged 1 commit into from
Oct 10, 2018
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Features
- Make `content` to be a shorthand prop for `Popup` @kuzhelov ([#322](https://github.com/stardust-ui/react/pull/322))
- 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))

<!--------------------------------[ v0.9.0 ]------------------------------- -->
## [v0.9.0](https://github.com/stardust-ui/react/tree/v0.9.0) (2018-10-07)
Expand Down
9 changes: 9 additions & 0 deletions docs/src/examples/components/Slot/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'

export default () => (
<div>
A <code>Slot</code> is a basic component (no default styles) used mainly as a way to create
shorthand elements as a part of more complex components (e.g.: <code>content</code> prop for{' '}
<code>Button</code> and other components)
</div>
)
7 changes: 5 additions & 2 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as _ from 'lodash'

import { UIComponent, childrenExist, customPropTypes, createShorthandFactory } from '../../lib'
import Icon from '../Icon'
import Slot from '../Slot'
import { buttonBehavior } from '../../lib/accessibility'
import { Accessibility } from '../../lib/accessibility/interfaces'
import { ComponentVariablesInput, ComponentPartStyle } from '../../../types/theme'
Expand All @@ -24,7 +25,7 @@ export interface IButtonProps {
circular?: boolean
className?: string
disabled?: boolean
content?: React.ReactNode
content?: ShorthandValue
fluid?: boolean
icon?: ShorthandValue
iconOnly?: boolean
Expand Down Expand Up @@ -160,7 +161,9 @@ class Button extends UIComponent<Extendable<IButtonProps>, IButtonState> {
>
{hasChildren && children}
{!hasChildren && iconPosition !== 'after' && this.renderIcon(variables, styles)}
{!hasChildren && content && <span className={classes.content}>{content}</span>}
{Slot.create(!hasChildren && content, {
defaultProps: { as: 'span', className: classes.content },
})}
{!hasChildren && iconPosition === 'after' && this.renderIcon(variables, styles)}
</ElementType>
)
Expand Down
59 changes: 59 additions & 0 deletions src/components/Slot/Slot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as React from 'react'
import * as PropTypes from 'prop-types'
import { customPropTypes, UIComponent, childrenExist, createShorthandFactory } from '../../lib'
import { Extendable } from '../../../types/utils'
import { ComponentVariablesInput, ComponentPartStyle } from '../../../types/theme'

export interface ISlotProps {
as?: any
className?: string
content?: any
styles?: ComponentPartStyle<ISlotProps, any>
variables?: ComponentVariablesInput
}

/**
* A Slot is a basic component (no default styles)
*/
class Slot extends UIComponent<Extendable<ISlotProps>, any> {
static create: Function

static className = 'ui-slot'

static displayName = 'Slot'

static propTypes = {
/** An element type to render as (string or function). */
as: customPropTypes.as,

/** Additional CSS class name(s) to apply. */
className: PropTypes.string,

/** Shorthand for primary content. */
content: PropTypes.any,

/** Additional CSS styles to apply to the component instance. */
styles: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),

/** Override for theme site variables to allow modifications of component styling via themes. */
variables: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
}

static defaultProps = {
as: 'div',
}

renderComponent({ ElementType, classes, rest }) {
const { children, content } = this.props

return (
<ElementType {...rest} className={classes.root}>
{childrenExist(children) ? children : content}
</ElementType>
)
}
}

Slot.create = createShorthandFactory(Slot, content => ({ content }))

export default Slot
1 change: 1 addition & 0 deletions src/components/Slot/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './Slot'
38 changes: 17 additions & 21 deletions test/specs/commonTests/isConformant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { mount as enzymeMount } from 'enzyme'
import * as ReactDOMServer from 'react-dom/server'
import { ThemeProvider } from 'react-fela'

import isExportedAtTopLevel from './isExportedAtTopLevel'
import { assertBodyContains, consoleUtil, syntheticEvent } from 'test/utils'
import helpers from './commonHelpers'

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

export interface IConformant {
eventTargets?: object
requiredProps?: object
exportedAtTopLevel?: boolean
rendersPortal?: boolean
}

export const mount = (node, options?) => {
return enzymeMount(
<ThemeProvider theme={{ renderer: felaRenderer }}>{node}</ThemeProvider>,
Expand All @@ -24,11 +32,17 @@ export const mount = (node, options?) => {
* @param {React.Component|Function} Component A component that should conform.
* @param {Object} [options={}]
* @param {Object} [options.eventTargets={}] Map of events and the child component to target.
* @param {boolean} [options.exportedAtTopLevel=false] Is this component exported as top level API
* @param {boolean} [options.rendersPortal=false] Does this component render a Portal powered component?
* @param {Object} [options.requiredProps={}] Props required to render Component without errors or warnings.
*/
export default (Component, options: any = {}) => {
const { eventTargets = {}, requiredProps = {}, rendersPortal = false } = options
export default (Component, options: IConformant = {}) => {
const {
eventTargets = {},
exportedAtTopLevel = true,
requiredProps = {},
rendersPortal = false,
} = options
const { throwError } = helpers('isConformant', Component)

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

// ----------------------------------------
// Is exported or private
// ----------------------------------------
// detect components like: stardust.H1
const isTopLevelAPIProp = _.has(stardust, constructorName)

// find the apiPath in the stardust object
const foundAsSubcomponent = _.isFunction(_.get(stardust, info.apiPath))

// require all components to be exported at the top level
test('is exported at the top level', () => {
const message = [
`'${info.displayName}' must be exported at top level.`,
"Export it in 'src/index.js'.",
].join(' ')

expect({ isTopLevelAPIProp, message }).toEqual({
message,
isTopLevelAPIProp: true,
})
})

exportedAtTopLevel && isExportedAtTopLevel(constructorName, info.displayName)
if (info.isChild) {
test('is a static component on its parent', () => {
const message =
Expand Down
23 changes: 23 additions & 0 deletions test/specs/commonTests/isExportedAtTopLevel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as _ from 'lodash'
import * as stardust from 'src/'

// ----------------------------------------
// Is exported or private
// ----------------------------------------
// detect components like: stardust.H1
export default (constructorName: string, displayName: string) => {
const isTopLevelAPIProp = _.has(stardust, constructorName)

// require all components to be exported at the top level
test('is exported at the top level', () => {
const message = [
`'${displayName}' must be exported at top level.`,
"Export it in 'src/index.js'.",
].join(' ')

expect({ isTopLevelAPIProp, message }).toEqual({
message,
isTopLevelAPIProp: true,
})
})
}
6 changes: 6 additions & 0 deletions test/specs/components/Slot/Slot-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { isConformant } from 'test/specs/commonTests'
import Slot from 'src/components/Slot'

describe('Slot', () => {
isConformant(Slot, { exportedAtTopLevel: false })
})