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 }) +})