diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4fcb9e4371..f38f96c3da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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](https://github.com/stardust-ui/react/tree/v0.9.0) (2018-10-07)
diff --git a/docs/src/examples/components/Slot/index.tsx b/docs/src/examples/components/Slot/index.tsx
new file mode 100644
index 0000000000..d8cf2be34c
--- /dev/null
+++ b/docs/src/examples/components/Slot/index.tsx
@@ -0,0 +1,9 @@
+import React from 'react'
+
+export default () => (
+
+ A Slot 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.: content prop for{' '}
+ Button and other components)
+
+)
diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx
index 6029afc75b..2a00e23e16 100644
--- a/src/components/Button/Button.tsx
+++ b/src/components/Button/Button.tsx
@@ -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'
@@ -24,7 +25,7 @@ export interface IButtonProps {
circular?: boolean
className?: string
disabled?: boolean
- content?: React.ReactNode
+ content?: ShorthandValue
fluid?: boolean
icon?: ShorthandValue
iconOnly?: boolean
@@ -160,7 +161,9 @@ class Button extends UIComponent, IButtonState> {
>
{hasChildren && children}
{!hasChildren && iconPosition !== 'after' && this.renderIcon(variables, styles)}
- {!hasChildren && content && {content}}
+ {Slot.create(!hasChildren && content, {
+ defaultProps: { as: 'span', className: classes.content },
+ })}
{!hasChildren && iconPosition === 'after' && this.renderIcon(variables, styles)}
)
diff --git a/src/components/Slot/Slot.tsx b/src/components/Slot/Slot.tsx
new file mode 100644
index 0000000000..0228b1fd61
--- /dev/null
+++ b/src/components/Slot/Slot.tsx
@@ -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
+ variables?: ComponentVariablesInput
+}
+
+/**
+ * A Slot is a basic component (no default styles)
+ */
+class Slot extends UIComponent, 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 (
+
+ {childrenExist(children) ? children : content}
+
+ )
+ }
+}
+
+Slot.create = createShorthandFactory(Slot, content => ({ content }))
+
+export default Slot
diff --git a/src/components/Slot/index.ts b/src/components/Slot/index.ts
new file mode 100644
index 0000000000..b3cab8387c
--- /dev/null
+++ b/src/components/Slot/index.ts
@@ -0,0 +1 @@
+export { default } from './Slot'
diff --git a/test/specs/commonTests/isConformant.tsx b/test/specs/commonTests/isConformant.tsx
index 989069a903..cae25f0cc5 100644
--- a/test/specs/commonTests/isConformant.tsx
+++ b/test/specs/commonTests/isConformant.tsx
@@ -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'
@@ -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(
{node},
@@ -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
@@ -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 =
diff --git a/test/specs/commonTests/isExportedAtTopLevel.tsx b/test/specs/commonTests/isExportedAtTopLevel.tsx
new file mode 100644
index 0000000000..b576ad3d75
--- /dev/null
+++ b/test/specs/commonTests/isExportedAtTopLevel.tsx
@@ -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,
+ })
+ })
+}
diff --git a/test/specs/components/Slot/Slot-test.ts b/test/specs/components/Slot/Slot-test.ts
new file mode 100644
index 0000000000..6354dcac11
--- /dev/null
+++ b/test/specs/components/Slot/Slot-test.ts
@@ -0,0 +1,6 @@
+import { isConformant } from 'test/specs/commonTests'
+import Slot from 'src/components/Slot'
+
+describe('Slot', () => {
+ isConformant(Slot, { exportedAtTopLevel: false })
+})