diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b396e4015..d4c9ca7591 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add `avatar` prop to `Chat.Message` subcomponent @Bugaa92 ([#159](https://github.com/stardust-ui/react/pull/159))
- add `iconOnly` prop to `Button` @mnajdova ([#182](https://github.com/stardust-ui/react/pull/182))
- Add Label `image` and `imagePosition`, removed `onIconClick` prop @mnajdova ([#55](https://github.com/stardust-ui/react/pull/55/))
+- Add `ButtonGroup` component @mnajdova ([#179](https://github.com/stardust-ui/react/pull/179))
## [v0.5.0](https://github.com/stardust-ui/react/tree/v0.5.0) (2018-08-30)
diff --git a/build/gulp/plugins/util/getComponentInfo.ts b/build/gulp/plugins/util/getComponentInfo.ts
index b48e4cea7b..626d1cedc6 100644
--- a/build/gulp/plugins/util/getComponentInfo.ts
+++ b/build/gulp/plugins/util/getComponentInfo.ts
@@ -82,13 +82,12 @@ const getComponentInfo = filepath => {
: info.displayName
// class name for the component
- // example, the "button" in class="ui button"
+ // example, the "button" in class="ui-button"
// name of the component, sub component, or plural parent for sub component groups
info.componentClassName = (info.isChild
- ? `ui-${info.parentDisplayName}__${info.subcomponentName.replace(
- /Group$/,
- `${info.parentDisplayName}s`,
- )}`
+ ? _.includes(info.subcomponentName, 'Group')
+ ? `ui-${info.parentDisplayName}s`
+ : `ui-${info.parentDisplayName}__${info.subcomponentName}`
: `ui-${info.displayName}`
).toLowerCase()
diff --git a/docs/src/examples/components/Button/Groups/ButtonGroupCircularExample.shorthand.tsx b/docs/src/examples/components/Button/Groups/ButtonGroupCircularExample.shorthand.tsx
new file mode 100644
index 0000000000..4044b4544f
--- /dev/null
+++ b/docs/src/examples/components/Button/Groups/ButtonGroupCircularExample.shorthand.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { Button } from '@stardust-ui/react'
+
+const ButtonGroupCircularExampleShorthand = () => (
+
+)
+
+export default ButtonGroupCircularExampleShorthand
diff --git a/docs/src/examples/components/Button/Groups/ButtonGroupExample.shorthand.tsx b/docs/src/examples/components/Button/Groups/ButtonGroupExample.shorthand.tsx
new file mode 100644
index 0000000000..88e24307ba
--- /dev/null
+++ b/docs/src/examples/components/Button/Groups/ButtonGroupExample.shorthand.tsx
@@ -0,0 +1,14 @@
+import React from 'react'
+import { Button } from '@stardust-ui/react'
+
+const ButtonGroupExampleShorthand = () => (
+
+)
+
+export default ButtonGroupExampleShorthand
diff --git a/docs/src/examples/components/Button/Groups/index.tsx b/docs/src/examples/components/Button/Groups/index.tsx
new file mode 100644
index 0000000000..a7710de27b
--- /dev/null
+++ b/docs/src/examples/components/Button/Groups/index.tsx
@@ -0,0 +1,20 @@
+import React from 'react'
+import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample'
+import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection'
+
+const Groups = () => (
+
+
+
+
+)
+
+export default Groups
diff --git a/docs/src/examples/components/Button/index.tsx b/docs/src/examples/components/Button/index.tsx
index e28853e499..965ba00469 100644
--- a/docs/src/examples/components/Button/index.tsx
+++ b/docs/src/examples/components/Button/index.tsx
@@ -2,12 +2,14 @@ import React from 'react'
import Types from './Types'
import Variations from './Variations'
import States from './States'
+import Groups from './Groups'
const ButtonExamples = () => (
+
)
diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx
index fab437ffc4..e1cebd5f81 100644
--- a/src/components/Button/Button.tsx
+++ b/src/components/Button/Button.tsx
@@ -1,7 +1,7 @@
import * as PropTypes from 'prop-types'
import * as React from 'react'
-import { UIComponent, childrenExist, customPropTypes } from '../../lib'
+import { UIComponent, childrenExist, customPropTypes, createShorthandFactory } from '../../lib'
import Icon from '../Icon'
import { ButtonBehavior } from '../../lib/accessibility'
import { Accessibility } from '../../lib/accessibility/interfaces'
@@ -12,6 +12,7 @@ import {
ReactChildren,
ComponentEventHandler,
} from '../../../types/utils'
+import ButtonGroup from './ButtonGroup'
export interface IButtonProps {
as?: any
@@ -43,6 +44,8 @@ export interface IButtonProps {
* - if button includes icon only, textual representation needs to be provided by using 'title', 'aria-label', or 'aria-labelledby' attributes
*/
class Button extends UIComponent, any> {
+ static create: Function
+
public static displayName = 'Button'
public static className = 'ui-button'
@@ -121,6 +124,8 @@ class Button extends UIComponent, any> {
accessibility: ButtonBehavior as Accessibility,
}
+ static Group = ButtonGroup
+
public renderComponent({
ElementType,
classes,
@@ -179,4 +184,6 @@ class Button extends UIComponent, any> {
}
}
+Button.create = createShorthandFactory(Button, content => ({ content }))
+
export default Button
diff --git a/src/components/Button/ButtonGroup.tsx b/src/components/Button/ButtonGroup.tsx
new file mode 100644
index 0000000000..ba7ac5dad5
--- /dev/null
+++ b/src/components/Button/ButtonGroup.tsx
@@ -0,0 +1,118 @@
+import * as PropTypes from 'prop-types'
+import * as React from 'react'
+import * as _ from 'lodash'
+
+import { UIComponent, childrenExist, customPropTypes } from '../../lib'
+import { ComponentVariablesInput, IComponentPartStylesInput } from '../../../types/theme'
+import { Extendable, ItemShorthand, ReactChildren } from '../../../types/utils'
+import Button from './Button'
+
+export interface IButtonGroupProps {
+ as?: any
+ children?: ReactChildren
+ circular?: boolean
+ className?: string
+ content?: React.ReactNode
+ buttons?: ItemShorthand[]
+ styles?: IComponentPartStylesInput
+ variables?: ComponentVariablesInput
+}
+
+/**
+ * A button group.
+ */
+class ButtonGroup extends UIComponent, any> {
+ public static displayName = 'ButtonGroup'
+
+ public static className = 'ui-buttons'
+
+ public static propTypes = {
+ /** An element type to render as (string or function). */
+ as: customPropTypes.as,
+
+ /** A button can take the width of its container. */
+ buttons: customPropTypes.collectionShorthand,
+
+ /** Primary content. */
+ children: PropTypes.node,
+
+ /** Additional classes. */
+ className: PropTypes.string,
+
+ /** The buttons inside group can appear circular. */
+ circular: PropTypes.bool,
+
+ /** Shorthand for primary content. */
+ content: customPropTypes.contentShorthand,
+
+ /** Custom styles to be applied for component. */
+ styles: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
+
+ /** Custom variables to be applied for component. */
+ variables: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
+ }
+
+ static handledProps = [
+ 'as',
+ 'buttons',
+ 'children',
+ 'circular',
+ 'className',
+ 'content',
+ 'styles',
+ 'variables',
+ ]
+
+ public static defaultProps = {
+ as: 'div',
+ }
+
+ public renderComponent({
+ ElementType,
+ classes,
+ accessibility,
+ variables,
+ styles,
+ rest,
+ }): React.ReactNode {
+ const { children, content, buttons, circular } = this.props
+ if (_.isNil(buttons)) {
+ return (
+
+ {childrenExist(children) ? children : content}
+
+ )
+ }
+
+ return (
+
+ {_.map(buttons, (button, idx) =>
+ Button.create(button, {
+ defaultProps: {
+ circular,
+ styles: {
+ root: this.getStyleForButtonIndex(styles, idx === 0, idx === buttons.length - 1),
+ },
+ },
+ }),
+ )}
+
+ )
+ }
+
+ getStyleForButtonIndex = (styles, isFirst, isLast) => {
+ let resultStyles = {}
+ if (isFirst) {
+ resultStyles = styles.firstButton
+ }
+ if (isLast) {
+ resultStyles = { ...resultStyles, ...styles.lastButton }
+ }
+ if (!isFirst && !isLast) {
+ resultStyles = styles.middleButton
+ }
+ return resultStyles
+ }
+}
+
+export default ButtonGroup
diff --git a/src/components/Button/index.ts b/src/components/Button/index.ts
index 3389ecb836..5947882a6f 100644
--- a/src/components/Button/index.ts
+++ b/src/components/Button/index.ts
@@ -1 +1,2 @@
export { default } from './Button'
+export { default as ButtonGroup } from './ButtonGroup'
diff --git a/src/components/List/List.tsx b/src/components/List/List.tsx
index 340501bf04..2d903b1003 100644
--- a/src/components/List/List.tsx
+++ b/src/components/List/List.tsx
@@ -41,7 +41,7 @@ class List extends UIComponent, any> {
debug: PropTypes.bool,
/** Shorthand array of props for ListItem. */
- items: PropTypes.arrayOf(PropTypes.any),
+ items: customPropTypes.collectionShorthand,
/** A selection list formats list items as possible choices. */
selection: PropTypes.bool,
diff --git a/src/index.ts b/src/index.ts
index fba413fa45..4ef2d0f467 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,8 +3,9 @@ export { themes }
export { default as Accordion } from './components/Accordion'
export { default as Button } from './components/Button'
+export { ButtonGroup } from './components/Button'
export { default as Chat } from './components/Chat'
-export { default as ChatMessage } from './components/Chat'
+export { ChatMessage } from './components/Chat'
export { default as Divider } from './components/Divider'
export { default as Grid } from './components/Grid'
export { default as Image } from './components/Image'
diff --git a/src/themes/teams/componentStyles.ts b/src/themes/teams/componentStyles.ts
index 1cb7516ed9..a1ac3be585 100644
--- a/src/themes/teams/componentStyles.ts
+++ b/src/themes/teams/componentStyles.ts
@@ -5,6 +5,7 @@ export { default as AccordionTitle } from './components/Accordion/accordionTitle
export { default as Avatar } from './components/Avatar/avatarStyles'
export { default as Button } from './components/Button/buttonStyles'
+export { default as ButtonGroup } from './components/Button/buttonGroupStyles'
export { default as Chat } from './components/Chat/chatStyles'
export { default as ChatMessage } from './components/Chat/chatMessageStyles'
diff --git a/src/themes/teams/componentVariables.ts b/src/themes/teams/componentVariables.ts
index 107bca360d..a129cdb742 100644
--- a/src/themes/teams/componentVariables.ts
+++ b/src/themes/teams/componentVariables.ts
@@ -3,6 +3,7 @@ export { default as AccordionContent } from './components/Accordion/accordionCon
export { default as Avatar } from './components/Avatar/avatarVariables'
export { default as Button } from './components/Button/buttonVariables'
+export { default as ButtonGroup } from './components/Button/buttonVariables'
export { default as ChatMessage } from './components/Chat/chatMessageVariables'
diff --git a/src/themes/teams/components/Button/buttonGroupStyles.ts b/src/themes/teams/components/Button/buttonGroupStyles.ts
new file mode 100644
index 0000000000..66ecaa362e
--- /dev/null
+++ b/src/themes/teams/components/Button/buttonGroupStyles.ts
@@ -0,0 +1,32 @@
+import { IComponentPartStylesInput, ICSSInJSStyle } from '../../../../../types/theme'
+import { IButtonGroupProps } from '../../../../components/Button/ButtonGroup'
+
+const commonButtonsStyles = (circular: boolean) => ({
+ ...(!circular && {
+ margin: '0px',
+ borderRadius: '0px',
+ }),
+})
+
+const buttonGroupStyles: IComponentPartStylesInput = {
+ root: (): ICSSInJSStyle => ({}),
+ middleButton: ({ props: p }: { props: IButtonGroupProps; variables: any }) => ({
+ ...commonButtonsStyles(p.circular),
+ }),
+ firstButton: ({ props: p, variables: v }: { props: IButtonGroupProps; variables: any }) => ({
+ ...commonButtonsStyles(p.circular),
+ ...(!p.circular && {
+ borderTopLeftRadius: v.borderRadius,
+ borderBottomLeftRadius: v.borderRadius,
+ }),
+ }),
+ lastButton: ({ props: p, variables: v }: { props: IButtonGroupProps; variables: any }) => ({
+ ...commonButtonsStyles(p.circular),
+ ...(!p.circular && {
+ borderTopRightRadius: v.borderRadius,
+ borderBottomRightRadius: v.borderRadius,
+ }),
+ }),
+}
+
+export default buttonGroupStyles
diff --git a/src/themes/teams/components/Button/buttonStyles.ts b/src/themes/teams/components/Button/buttonStyles.ts
index 9fa8e9572c..31dc01ebee 100644
--- a/src/themes/teams/components/Button/buttonStyles.ts
+++ b/src/themes/teams/components/Button/buttonStyles.ts
@@ -12,6 +12,7 @@ const buttonStyles: IComponentPartStylesInput = {
height,
minWidth,
maxWidth,
+ borderRadius,
color,
backgroundColor,
backgroundColorHover,
@@ -33,6 +34,7 @@ const buttonStyles: IComponentPartStylesInput = {
maxWidth,
color,
backgroundColor,
+ borderRadius,
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
@@ -40,7 +42,6 @@ const buttonStyles: IComponentPartStylesInput = {
padding: `0 ${pxToRem(paddingLeftRightValue)}`,
margin: `0 ${pxToRem(8)} 0 0`,
verticalAlign: 'middle',
- borderRadius: pxToRem(2),
borderWidth: `${secondary ? (circular ? 1 : 2) : 0}px`,
cursor: 'pointer',
diff --git a/src/themes/teams/components/Button/buttonVariables.ts b/src/themes/teams/components/Button/buttonVariables.ts
index 87fae0d70f..7805c48f38 100644
--- a/src/themes/teams/components/Button/buttonVariables.ts
+++ b/src/themes/teams/components/Button/buttonVariables.ts
@@ -6,6 +6,7 @@ export interface IButtonVariables {
height: string
minWidth: string
maxWidth: string
+ borderRadius: string
color: string
backgroundColor: string
backgroundColorHover: string
@@ -26,6 +27,7 @@ export default (siteVars: any): IButtonVariables => {
height: pxToRem(32),
minWidth: pxToRem(96),
maxWidth: pxToRem(280),
+ borderRadius: pxToRem(2),
color: siteVars.black,
backgroundColor: siteVars.gray08,
backgroundColorHover: siteVars.gray06,
diff --git a/test/specs/commonTests/implementsCollectionShorthandProp.tsx b/test/specs/commonTests/implementsCollectionShorthandProp.tsx
new file mode 100644
index 0000000000..9689abb82a
--- /dev/null
+++ b/test/specs/commonTests/implementsCollectionShorthandProp.tsx
@@ -0,0 +1,65 @@
+import * as React from 'react'
+import { mount } from './isConformant'
+import * as _ from 'lodash'
+import { DefaultShorthandTestOptions, ShorthandTestOptions } from './implementsShorthandProp'
+
+export default Component => {
+ return function implementsCollectionShorthandProp(
+ shorthandPropertyName: string,
+ ShorthandComponent: React.ComponentType,
+ options: ShorthandTestOptions = DefaultShorthandTestOptions,
+ ) {
+ const { mapsValueToProp } = options
+
+ describe(`shorthand property for '${ShorthandComponent.displayName}'`, () => {
+ test(`is defined`, () => {
+ expect(Component.propTypes[shorthandPropertyName]).toBeTruthy()
+ })
+
+ test(`array of string values is spread as ${
+ ShorthandComponent.displayName
+ }s' ${mapsValueToProp}`, () => {
+ const shorthandValue = ['some value', 'some other value']
+ const props = { [shorthandPropertyName]: shorthandValue }
+ const wrapper = mount()
+
+ const shorthandComponents = wrapper.find(ShorthandComponent.displayName)
+
+ expect(shorthandComponents.first().prop(mapsValueToProp)).toEqual(_.first(shorthandValue))
+ expect(shorthandComponents.last().prop(mapsValueToProp)).toEqual(_.last(shorthandValue))
+ })
+
+ test(`object value is spread as ${ShorthandComponent.displayName}'s props`, () => {
+ const shorthandValue = [
+ { key: 'first', foo: 'foo value', bar: 'bar value' },
+ { key: 'last', foo: 'foo last value', bar: 'bar last value' },
+ ]
+
+ const props = { [shorthandPropertyName]: shorthandValue }
+ const wrapper = mount()
+
+ const shorthandComponents = wrapper.find(ShorthandComponent.displayName)
+
+ const allShorthandPropertiesArePassedToFirstShorthandComponent = Object.keys(
+ _.first(shorthandValue),
+ ).every(
+ propertyName =>
+ propertyName === 'key' ||
+ _.first(shorthandValue)[propertyName] ===
+ shorthandComponents.first().prop(propertyName),
+ )
+
+ const allShorthandPropertiesArePassedToLastShorthandComponent = Object.keys(
+ _.last(shorthandValue),
+ ).every(
+ propertyName =>
+ propertyName === 'key' ||
+ _.last(shorthandValue)[propertyName] === shorthandComponents.last().prop(propertyName),
+ )
+
+ expect(allShorthandPropertiesArePassedToFirstShorthandComponent).toBe(true)
+ expect(allShorthandPropertiesArePassedToLastShorthandComponent).toBe(true)
+ })
+ })
+ }
+}
diff --git a/test/specs/commonTests/implementsShorthandProp.tsx b/test/specs/commonTests/implementsShorthandProp.tsx
index 473a994248..90f31b1fb5 100644
--- a/test/specs/commonTests/implementsShorthandProp.tsx
+++ b/test/specs/commonTests/implementsShorthandProp.tsx
@@ -1,11 +1,11 @@
import * as React from 'react'
import { mount } from './isConformant'
-type ShorthandTestOptions = {
+export type ShorthandTestOptions = {
mapsValueToProp?: string
}
-const DefaultShorthandTestOptions: ShorthandTestOptions = {
+export const DefaultShorthandTestOptions: ShorthandTestOptions = {
mapsValueToProp: 'content',
}
diff --git a/test/specs/components/Button/ButtonGroup-test.tsx b/test/specs/components/Button/ButtonGroup-test.tsx
new file mode 100644
index 0000000000..06372d767a
--- /dev/null
+++ b/test/specs/components/Button/ButtonGroup-test.tsx
@@ -0,0 +1,13 @@
+import * as React from 'react'
+
+import { isConformant } from 'test/specs/commonTests'
+import ButtonGroup from 'src/components/Button/ButtonGroup'
+import implementsCollectionShorthandProp from '../../commonTests/implementsCollectionShorthandProp'
+import Button from 'src/components/Button'
+
+const buttonGroupImplementsCollectionShorthandProp = implementsCollectionShorthandProp(ButtonGroup)
+
+describe('ButtonGroup', () => {
+ isConformant(ButtonGroup)
+ buttonGroupImplementsCollectionShorthandProp('buttons', Button)
+})
diff --git a/test/specs/components/List/List-test.ts b/test/specs/components/List/List-test.ts
index 6f29d0dcdf..01de79309c 100644
--- a/test/specs/components/List/List-test.ts
+++ b/test/specs/components/List/List-test.ts
@@ -1,8 +1,13 @@
import { isConformant, handlesAccessibility } from 'test/specs/commonTests'
import List from 'src/components/List/List'
+import implementsCollectionShorthandProp from '../../commonTests/implementsCollectionShorthandProp'
+import ListItem from 'src/components/List/ListItem'
+
+const listImplementsCollectionShorthandProp = implementsCollectionShorthandProp(List)
describe('List', () => {
isConformant(List)
handlesAccessibility(List, { defaultRootRole: 'list' })
+ listImplementsCollectionShorthandProp('items', ListItem, { mapsValueToProp: 'main' })
})
diff --git a/test/specs/components/Menu/Menu-test.tsx b/test/specs/components/Menu/Menu-test.tsx
index 6f20df9eda..58cb32726a 100644
--- a/test/specs/components/Menu/Menu-test.tsx
+++ b/test/specs/components/Menu/Menu-test.tsx
@@ -4,9 +4,14 @@ import Menu from 'src/components/Menu/Menu'
import { isConformant, handlesAccessibility, getRenderedAttribute } from 'test/specs/commonTests'
import { mountWithProvider, getTestingRenderedComponent } from 'test/utils'
import { ToolbarBehavior, TabListBehavior } from '../../../../src/lib/accessibility'
+import implementsCollectionShorthandProp from '../../commonTests/implementsCollectionShorthandProp'
+import MenuItem from 'src/components/Menu/MenuItem'
+
+const menuImplementsCollectionShorthandProp = implementsCollectionShorthandProp(Menu)
describe('Menu', () => {
isConformant(Menu)
+ menuImplementsCollectionShorthandProp('items', MenuItem)
const getItems = () => [
{ key: 'home', content: 'home', onClick: jest.fn(), 'data-foo': 'something' },