Repository: uber-web/baseui
Pull request #674 (2018-12-19 17:53:00)

docs(popover): update popover and tooltip docs [skip ci]
Looked at the popover docs again the other day and realized I wasn't super thorough or accurate when writing them the first time around. This PR attempts to fix this and make sure all available props for each export are thoroughly documented.


Patch:
diff --git a/proposals/button-group-component.md b/proposals/button-group-component.md
deleted file mode 100644
index 10b60d59f..000000000
--- a/proposals/button-group-component.md
+++ /dev/null
@@ -1,89 +0,0 @@
-# Button Group Component
-
-## Usage
-
-### Basic usage
-
-```javascript
-import * as React from 'react';
-import {Button, ButtonGroup} from 'baseui/button-group';
-
-export default () => {
-  return
-    <ButtonGroup>
-      <Button>Copy</Button>
-      <Button>Cut</Button>
-      <Button>Paste</Button>
-    </ButtonGroup>
-}
-```
-
-### Advanced usage
-
-```javascript
-import * as React from 'react';
-import {Button, ButtonGroup} from 'baseui/button-group';
-
-export default () => {
-  return
-    <ButtonGroup
-      defaultChecked={[0, 1]}
-      mode="checkbox">
-      <Button>Bold</Button>
-      <Button>Italic</Button>
-      <Button>Underline</Button>
-    </ButtonGroup>
-}
-```
-
-## Exports
-
-* `Button`
-* `ButtonGroup`
-* `StatefulButtonGroup`
-* `StyledRoot`
-
-## `Button` API
-
-Same as `{Button} from baseui/button`, with the additional property:
-
-* `selected: boolean` - Optional:
-  Defines if the button is in a selected state. Defaults to `false`.
-
-## `ButtonGroup` API
-
-* `children: (props: Props) => React.Node` - Required
-  This can be an SVG icon as well.
-* `ariaLabel: string` - Optional:
-  Accessible label
-* `mode: string` - Optional:
-  Use the `mode` prop to render toggleable Buttons:
-  * the value `radio` will cause Buttons to behave like radio buttons,
-  * the value `checkbox` will cause Buttons to behave like checkboxes.
-* `checked: number | Array<number>` - Optional
-  Index or array of indices of the selected Button(s).
-  Primarily for use with controlled components with a `mode` prop defined.
-* `disabled: boolean` - Optional:
-  Defines if the button group disabled.
-* `size: 'default' | 'compact'`
-  Defines the size of the buttons in the button group.
-* `shape: 'default' | 'round' | 'square'`
-  Defines the shape of the buttons in the button group.
-* `kind: 'primary' | 'secondary' | 'tertiary' | 'minimal'`
-  Defines the kind (purpose) of the buttons in the button group.
-* `onChange: func` - Optinal:
-  Called when a toggleable Button is selected
-* `onClick: func` - Optional:
-  Called with the click event
-
-## `StatefulButtonGroup` API
-
-Extends the `ButtonGroup` API with this additional props:
-
-* `initialState: {checked: number | Array<number>}`
-  Index or array of indices of the selected Button(s).
-  Primarily for use with uncontrolled components with a `mode` prop defined.
-
-## Dependencies
-
-None.
diff --git a/screener.config.js b/screener.config.js
index e3e5eeadb..a58a68bf7 100644
--- a/screener.config.js
+++ b/screener.config.js
@@ -10,6 +10,7 @@ LICENSE file in the root directory of this source tree.
 
 const Accordion = require('./src/accordion/examples-list');
 const Button = require('./src/button/examples-list');
+const ButtonGroup = require('./src/button-group/examples-list');
 const Breadcrumbs = require('./src/breadcrumbs/examples-list');
 const Card = require('./src/card/examples-list');
 const Checkbox = require('./src/checkbox/examples-list');
@@ -37,6 +38,7 @@ module.exports = {
     new RegExp(`${Accordion.ACCORDION_EXAMPLE}$`),
     new RegExp(Button.BUTTON_COMPACT_WITH_ENHANCERS),
     new RegExp(Button.BUTTON_WITH_ENHANCERS),
+    new RegExp(`${ButtonGroup.BUTTON_GROUP}$`),
     new RegExp(Breadcrumbs.DEFAULT),
     new RegExp(Card.TEXT_IMAGE_LINK),
     new RegExp(Card.TEXT_ONLY),
diff --git a/src/button-group/README.md b/src/button-group/README.md
new file mode 100644
index 000000000..11e59bd4d
--- /dev/null
+++ b/src/button-group/README.md
@@ -0,0 +1,124 @@
+# Button Group Component
+
+Groups a collection of buttons together. Can also operate as a radio group or checkbox group.
+
+## Usage
+
+### Basic usage
+
+```javascript
+import React from 'react';
+import {Button} from 'baseui/button';
+import {StatefulButtonGroup} from 'baseui/button-group';
+
+export default () => {
+  return
+    <StatefulButtonGroup>
+      <Button>Copy</Button>
+      <Button>Cut</Button>
+      <Button>Paste</Button>
+    </StatefulButtonGroup>
+}
+```
+
+### Advanced usage
+
+```javascript
+import React from 'react';
+import {Button} from 'baseui/button';
+import {ButtonGroup} from 'baseui/button-group';
+
+export default class MyCustomLogic extends React.Component {
+  state = {selected: [0, 1]};
+
+  handleClick = (event, index) => {
+    if (!this.state.selected.includes(index)) {
+      this.setState({selected: [...this.state.selected, index]});
+    } else {
+      this.setState({
+        selected: this.state.selected.filter(value => value !== index),
+      });
+    }
+  }
+
+  handleBold = (event) => {
+    // event handler only for the 'bold' button
+  }
+
+  render() {
+    return (
+      <ButtonGroup
+        selected={this.state.selected}
+        mode="checkbox"
+        onClick={this.handleClick}
+      >
+        <Button onClick={this.handleBold}>Bold</Button>
+        <Button>Italic</Button>
+        <Button>Underline</Button>
+      </ButtonGroup>
+    )
+  }
+}
+```
+
+## Exports
+
+* `ButtonGroup`
+* `StatefulButtonGroup`
+* `StatefulContainer`
+* `StyledRoot`
+* `MODE`
+* `STATE_CHANGE_TYPE`
+
+## `ButtonGroup` API
+
+* `children: (props: Props) => React.Node` - Required.
+  * Set of more than one `Button` components.
+* `ariaLabel?: string`
+  * Accessible label.
+* `mode?: string`
+  * Use the `mode` prop to render toggleable Buttons:
+    * the value `radio` will cause Buttons to behave like radio buttons,
+    * the value `checkbox` will cause Buttons to behave like checkboxes.
+* `selected?: number | Array<number>`
+  * Index or array of indices of the selected Button(s).
+  * Primarily for use with controlled components with a `mode` prop defined.
+* `disabled?: boolean = false`
+  * Defines if the button group is disabled.
+* `size?: SIZE[string] = 'default'`
+  * Reference [`SIZE` constant](https://github.com/uber-web/baseui/blob/master/src/button/constants.js)
+  * Defines the size of the buttons in the button group.
+* `shape?: SHAPE[string] = 'default'`
+  * Reference [`SHAPE` constant](https://github.com/uber-web/baseui/blob/master/src/button/constants.js)
+  * Defines the shape of the buttons in the button group.
+* `kind?: KIND[string] = 'secondary'`
+  * Reference [`KIND` constant](https://github.com/uber-web/baseui/blob/master/src/button/constants.js)
+  * Defines the kind (purpose) of the buttons in the button group.
+* `onClick?: (event: Event, index: number) => mixed`
+  * Called with click events from children. If a child button has its own click handler, the local handler will be called first, then this handler will trigger.
+
+## `StatefulButtonGroup` API
+
+* Maintains same API as `ButtonGroup` except:
+* Does not include `selected` prop.
+* `initialState?: {selected: number | Array<number>} = {selected: []}`
+  * Sets the initial selected items. Relevant if `mode` is set.
+* `stateReducer?: (type: STATE_CHANGE_TYPE[string], nextState: {}, currentState: {}) => nextState`
+  * A state change handler. Used to override default state transitions.
+    * `type` - State change type.
+    * `nextState` - A new state, provided by component transition, that will be set.
+    * `currentState` - Current state of the component.
+
+## `StatefulContainer` API
+
+* Maintains same API as `StatefulButtonGroup` except:
+* `children: (props: { onClick: (event: Event, index: number), selected: number | Array<number>}) => React.Node` - Required.
+
+## `MODE` Constant
+
+* `checkbox`
+* `radio`
+
+## `STATE_CHANGE_TYPE` Constant
+
+* `change`
diff --git a/src/button-group/button-group.js b/src/button-group/button-group.js
new file mode 100644
index 000000000..7648a4dea
--- /dev/null
+++ b/src/button-group/button-group.js
@@ -0,0 +1,99 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import React from 'react';
+
+import {KIND, SIZE, SHAPE} from '../button/index.js';
+import {getOverrides} from '../helpers/overrides.js';
+
+import {StyledRoot} from './styled-components.js';
+import type {PropsT} from './types.js';
+
+function isSelected(selected, index) {
+  if (!selected) {
+    return false;
+  }
+
+  if (Array.isArray(selected)) {
+    return selected.includes(index);
+  }
+
+  return selected === index;
+}
+
+function getBorderRadii(index, length) {
+  if (index === 0) {
+    return {
+      borderTopRightRadius: 0,
+      borderBottomRightRadius: 0,
+    };
+  }
+
+  if (index === length - 1) {
+    return {
+      borderTopLeftRadius: 0,
+      borderBottomLeftRadius: 0,
+    };
+  }
+
+  return {
+    borderTopRightRadius: 0,
+    borderBottomRightRadius: 0,
+    borderTopLeftRadius: 0,
+    borderBottomLeftRadius: 0,
+  };
+}
+
+export default function ButtonGroup(props: PropsT) {
+  const {overrides = {}} = props;
+  const [Root, rootProps] = getOverrides(overrides.Root, StyledRoot);
+
+  return (
+    <Root aria-label={props.ariaLabel} {...rootProps}>
+      {React.Children.map(props.children, (child, index) => {
+        if (!React.isValidElement(child)) {
+          return null;
+        }
+
+        return React.cloneElement(child, {
+          disabled: props.disabled ? true : child.props.disabled,
+          isSelected: isSelected(props.selected, index),
+          kind: props.kind,
+          onClick: event => {
+            if (props.disabled) {
+              return;
+            }
+
+            if (child.props.onClick) {
+              child.props.onClick(event);
+            }
+
+            if (props.onClick) {
+              props.onClick(event, index);
+            }
+          },
+          overrides: {
+            BaseButton: {style: getBorderRadii(index, props.children.length)},
+            ...child.props.overrides,
+          },
+          shape: props.shape,
+          size: props.size,
+        });
+      })}
+    </Root>
+  );
+}
+
+ButtonGroup.defaultProps = {
+  ariaLabel: 'button group',
+  disabled: false,
+  kind: KIND.primary,
+  onClick: () => {},
+  shape: SHAPE.default,
+  size: SIZE.default,
+};
diff --git a/src/button-group/constants.js b/src/button-group/constants.js
new file mode 100644
index 000000000..a95be71ec
--- /dev/null
+++ b/src/button-group/constants.js
@@ -0,0 +1,16 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+export const MODE = Object.freeze({
+  radio: 'radio',
+  checkbox: 'checkbox',
+});
+
+export const STATE_CHANGE_TYPE = Object.freeze({
+  change: 'change',
+});
diff --git a/src/button-group/examples-list.js b/src/button-group/examples-list.js
new file mode 100644
index 000000000..7f804b1d1
--- /dev/null
+++ b/src/button-group/examples-list.js
@@ -0,0 +1,17 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+
+/* eslint-env node */
+/* eslint-disable flowtype/require-valid-file-annotation */
+
+module.exports = {
+  BUTTON_GROUP: 'Button Group',
+  RADIO_MODE: 'Radio Mode',
+  CHECKBOX_MODE: 'Checkbox Mode',
+  DISABLED: 'Disabled',
+  CONTROLLED: 'Controlled Group',
+};
diff --git a/src/button-group/examples.js b/src/button-group/examples.js
new file mode 100644
index 000000000..c9fa31971
--- /dev/null
+++ b/src/button-group/examples.js
@@ -0,0 +1,141 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import React from 'react';
+
+import {Block} from '../block/index.js';
+import {Button} from '../button/index.js';
+import Upload from '../icon/upload.js';
+
+import {ButtonGroup, StatefulButtonGroup} from './index.js';
+import examples from './examples-list.js';
+
+class ControlledButtonGroup extends React.Component<*, *> {
+  state = {selected: 1, boldClickCount: 0};
+
+  handleClick = (event, index) => {
+    if (this.state.selected !== index) {
+      this.setState({selected: index});
+    }
+  };
+
+  handleBold = event => {
+    this.setState(prevState => ({
+      boldClickCount: prevState.boldClickCount + 1,
+    }));
+  };
+
+  render() {
+    return (
+      <React.Fragment>
+        <ButtonGroup
+          selected={this.state.selected}
+          mode="radio"
+          onClick={this.handleClick}
+        >
+          <Button onClick={this.handleBold}>Bold</Button>
+          <Button>Normal</Button>
+        </ButtonGroup>
+
+        <Block
+          font={this.state.selected === 0 ? 'font450' : 'font400'}
+          paddingTop="scale400"
+        >
+          Lorem ipsum dolor sit amet, ea insolens deseruisse mnesarchum mea. An
+          munere utroque mentitum vis, ea rebum inani iudicabit has. Cu his
+          dolorum perpetua. Mea atqui tation partem ne, ei vim etiam volumus
+          nominavi. Cum id atqui cotidieque, quaeque nostrum salutandi cum at,
+          idque scaevola platonem mei ad.
+        </Block>
+
+        <Block paddingTop="scale800">
+          The Bold option has been selected {this.state.boldClickCount} times
+        </Block>
+      </React.Fragment>
+    );
+  }
+}
+
+export default {
+  [examples.BUTTON_GROUP]: function SimpleStory() {
+    return (
+      <React.Fragment>
+        <ButtonGroup kind="tertiary">
+          <Button>Label</Button>
+          <Button>Label</Button>
+          <Button>Label</Button>
+        </ButtonGroup>
+        <br />
+        <ButtonGroup kind="tertiary">
+          <Button>
+            <Upload size={24} />
+          </Button>
+          <Button>
+            <Upload size={24} />
+          </Button>
+          <Button>
+            <Upload size={24} />
+          </Button>
+        </ButtonGroup>
+        <br />
+        <ButtonGroup kind="tertiary">
+          <Button startEnhancer={() => <Upload size={24} />}>Label</Button>
+          <Button startEnhancer={() => <Upload size={24} />}>Label</Button>
+          <Button startEnhancer={() => <Upload size={24} />}>Label</Button>
+        </ButtonGroup>
+      </React.Fragment>
+    );
+  },
+
+  [examples.RADIO_MODE]: function RadioStory() {
+    return (
+      <StatefulButtonGroup mode="radio" initialState={{selected: 0}}>
+        <Button>Label</Button>
+        <Button>Label</Button>
+      </StatefulButtonGroup>
+    );
+  },
+
+  [examples.CHECKBOX_MODE]: function CheckboxStory() {
+    return (
+      <StatefulButtonGroup
+        mode="checkbox"
+        kind="secondary"
+        initialState={{selected: [0, 1]}}
+      >
+        <Button>Label</Button>
+        <Button>Label</Button>
+        <Button>Label</Button>
+      </StatefulButtonGroup>
+    );
+  },
+
+  [examples.DISABLED]: function DisabledStory() {
+    return (
+      <React.Fragment>
+        <Block paddingBottom="scale200">All disabled</Block>
+        <ButtonGroup disabled kind="minimal">
+          <Button>Label</Button>
+          <Button>Label</Button>
+          <Button>Label</Button>
+        </ButtonGroup>
+        <br />
+        <Block paddingBottom="scale200">Single disabled</Block>
+        <ButtonGroup kind="minimal">
+          <Button disabled>Label</Button>
+          <Button>Label</Button>
+          <Button>Label</Button>
+        </ButtonGroup>
+      </React.Fragment>
+    );
+  },
+
+  [examples.CONTROLLED]: function ControlledStory() {
+    return <ControlledButtonGroup />;
+  },
+};
diff --git a/src/button-group/index.js b/src/button-group/index.js
new file mode 100644
index 000000000..ac6bec578
--- /dev/null
+++ b/src/button-group/index.js
@@ -0,0 +1,19 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+export {default as ButtonGroup} from './button-group.js';
+export {default as StatefulButtonGroup} from './stateful-button-group.js';
+export {default as StatefulContainer} from './stateful-container.js';
+
+export {MODE, STATE_CHANGE_TYPE} from './constants.js';
+
+// Styled elements
+export {StyledRoot} from './styled-components.js';
+
+// Types
+export * from './types.js';
diff --git a/src/button-group/stateful-button-group.js b/src/button-group/stateful-button-group.js
new file mode 100644
index 000000000..dee874109
--- /dev/null
+++ b/src/button-group/stateful-button-group.js
@@ -0,0 +1,24 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import React from 'react';
+
+import ButtonGroup from './button-group.js';
+import StatefulContainer from './stateful-container.js';
+import type {StatefulPropsT} from './types.js';
+
+export default function StatefulButtonGroup(props: StatefulPropsT) {
+  const {children, initialState, ...restProps} = props;
+  return (
+    <StatefulContainer initialState={initialState} {...restProps}>
+      {({...containerProps}) => (
+        <ButtonGroup {...containerProps}>{props.children}</ButtonGroup>
+      )}
+    </StatefulContainer>
+  );
+}
diff --git a/src/button-group/stateful-container.js b/src/button-group/stateful-container.js
new file mode 100644
index 000000000..4144d0bb0
--- /dev/null
+++ b/src/button-group/stateful-container.js
@@ -0,0 +1,97 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import * as React from 'react';
+
+import {MODE, STATE_CHANGE_TYPE} from './constants.js';
+
+import type {StatefulContainerPropsT, StateT} from './types.js';
+
+// handles the case where selected = 0
+function isSelectedDefined(selected) {
+  return Array.isArray(selected) || typeof selected === 'number';
+}
+
+function defaultStateReducer(
+  type: $Values<typeof STATE_CHANGE_TYPE>,
+  nextState: StateT,
+  currentState: StateT,
+) {
+  return nextState;
+}
+
+export default class StatefulContainer extends React.Component<
+  StatefulContainerPropsT,
+  StateT,
+> {
+  static defaultProps = {
+    initialState: {selected: []},
+    stateReducer: defaultStateReducer,
+  };
+
+  constructor(props: StatefulContainerPropsT) {
+    super(props);
+
+    const {initialState = {}} = props;
+    const {selected = []} = initialState;
+
+    this.state = {
+      selected: isSelectedDefined(selected) ? [].concat(selected) : [],
+    };
+  }
+
+  changeState = (nextState: StateT) => {
+    if (this.props.stateReducer) {
+      this.setState(
+        this.props.stateReducer(
+          STATE_CHANGE_TYPE.change,
+          nextState,
+          this.state,
+        ),
+      );
+    } else {
+      this.setState(nextState);
+    }
+  };
+
+  onClick = (event: SyntheticEvent<HTMLButtonElement>, index: number) => {
+    if (this.props.mode === MODE.radio) {
+      if (
+        this.state.selected.length === 0 ||
+        this.state.selected[0] !== index
+      ) {
+        this.changeState({selected: [index]});
+      } else {
+        this.changeState({selected: []});
+      }
+    }
+
+    if (this.props.mode === MODE.checkbox) {
+      if (!this.state.selected.includes(index)) {
+        this.changeState({selected: [...this.state.selected, index]});
+      } else {
+        this.changeState({
+          selected: this.state.selected.filter(value => value !== index),
+        });
+      }
+    }
+
+    if (this.props.onClick) {
+      this.props.onClick(event, index);
+    }
+  };
+
+  render() {
+    const {initialState, stateReducer, ...props} = this.props;
+    return this.props.children({
+      ...props,
+      onClick: this.onClick,
+      selected: this.state.selected,
+    });
+  }
+}
diff --git a/src/button-group/stories.js b/src/button-group/stories.js
new file mode 100644
index 000000000..b588dae46
--- /dev/null
+++ b/src/button-group/stories.js
@@ -0,0 +1,21 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+/* global module */
+
+import {storiesOf} from '@storybook/react';
+import {withReadme} from 'storybook-readme';
+import examples from './examples.js';
+
+import ButtonGroupReadme from './README.md';
+
+Object.entries(examples).forEach(([description, example]) =>
+  storiesOf('ButtonGroup', module)
+    .addDecorator(withReadme(ButtonGroupReadme))
+    // $FlowFixMe
+    .add(description, example),
+);
diff --git a/src/button-group/styled-components.js b/src/button-group/styled-components.js
new file mode 100644
index 000000000..cb9891581
--- /dev/null
+++ b/src/button-group/styled-components.js
@@ -0,0 +1,12 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import {styled} from '../styles/index.js';
+
+export const StyledRoot = styled('div', {display: 'flex'});
+StyledRoot.displayName = 'StyledRoot';
diff --git a/src/button-group/types.js b/src/button-group/types.js
new file mode 100644
index 000000000..d0ad28ec3
--- /dev/null
+++ b/src/button-group/types.js
@@ -0,0 +1,75 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import * as React from 'react';
+
+import {KIND, SIZE, SHAPE} from '../button/index.js';
+import type {OverrideT} from '../helpers/overrides.js';
+import type {ThemeT} from '../styles/index.js';
+
+import {MODE, STATE_CHANGE_TYPE} from './constants.js';
+
+// styled-components
+export type StylePropsT = {
+  $theme: ThemeT,
+};
+
+// button-group
+export type PropsT = {|
+  ariaLabel?: string,
+  children: Array<React.Node>,
+  disabled?: boolean,
+  kind?: $Values<typeof KIND>,
+  mode?: $Values<typeof MODE>,
+  onClick?: ClickHandlerT,
+  overrides?: OverridesT<StylePropsT>,
+  selected?: number | Array<number>,
+  shape?: $Values<typeof SHAPE>,
+  size?: $Values<typeof SIZE>,
+|};
+
+type OverridesT<T> = {
+  Root?: OverrideT<T>,
+};
+
+// stateful-group
+// eslint-disable-next-line flowtype/generic-spacing
+export type StatefulPropsT = $Diff<
+  {|
+    ...PropsT,
+    initialState?: {selected: number | Array<number>},
+    stateReducer?: StateReducerT,
+  |},
+  {selected: mixed}, // excluded from type definition
+>;
+
+// stateful-container
+export type StatefulContainerPropsT = {|
+  ...StatefulPropsT,
+  children: (props: {
+    ...$Diff<PropsT, {children: mixed}>,
+    onClick: ClickHandlerT,
+    selected: number | Array<number>,
+  }) => React.Node,
+|};
+
+export type StateT = {
+  selected: Array<number>,
+};
+
+export type StateReducerT = (
+  stateType: $Values<typeof STATE_CHANGE_TYPE>,
+  nextState: StateT,
+  currentState: StateT,
+) => StateT;
+
+// general
+type ClickHandlerT = (
+  event: SyntheticEvent<HTMLButtonElement>,
+  index: number,
+) => mixed;
diff --git a/src/button/README.md b/src/button/README.md
index bb54cccb0..6ea0a6984 100644
--- a/src/button/README.md
+++ b/src/button/README.md
@@ -89,6 +89,8 @@ Rough DOM composition:
   * Defines the kind (purpose) of a button
 * `isLoading?: boolean = false`
   * Show loading button style and spinner.
+* `isSelected?: boolean = false`
+  * Indicates that the button is selected. Changes background color.
 * `...rest`
   * Passed to BaseButton as props. These can include `accesskey`, `formtarget`, `type`, `value` and more.
 
diff --git a/src/button/button-internals.js b/src/button/button-internals.js
index f953ecd53..db0e0a0b4 100644
--- a/src/button/button-internals.js
+++ b/src/button/button-internals.js
@@ -49,10 +49,11 @@ export default function ButtonInternals(props: ButtonPropsT) {
 }
 
 ButtonInternals.defaultProps = {
-  overrides: {},
-  size: SIZE.default,
+  disabled: false,
+  isLoading: false,
+  isSelected: false,
   kind: KIND.primary,
+  overrides: {},
   shape: SHAPE.default,
-  isLoading: false,
-  disabled: false,
+  size: SIZE.default,
 };
diff --git a/src/button/button.js b/src/button/button.js
index 02c6c078c..2bc1fae18 100644
--- a/src/button/button.js
+++ b/src/button/button.js
@@ -37,6 +37,7 @@ export default class Button extends React.Component<ButtonPropsT> {
       kind,
       shape,
       isLoading,
+      isSelected,
       // Removing from restProps
       startEnhancer,
       endEnhancer,
diff --git a/src/button/styled-components.js b/src/button/styled-components.js
index cf823524d..dd9b8a368 100644
--- a/src/button/styled-components.js
+++ b/src/button/styled-components.js
@@ -14,9 +14,28 @@ type StylePropsT = SharedStylePropsT & {
   $theme: ThemeT,
 };
 
+function getBorderRadii({$shape, $theme}: StylePropsT) {
+  let value = '0px';
+
+  if ($shape === SHAPE.round) {
+    value = '50%';
+  }
+
+  if ($theme.borders.useRoundedCorners) {
+    value = $theme.borders.radius200;
+  }
+
+  return {
+    borderTopRightRadius: value,
+    borderBottomRightRadius: value,
+    borderTopLeftRadius: value,
+    borderBottomLeftRadius: value,
+  };
+}
+
 export const BaseButton = styled(
   'button',
-  ({$theme, $size, $kind, $shape, $isLoading}: StylePropsT) => ({
+  ({$theme, $size, $kind, $shape, $isLoading, $isSelected}: StylePropsT) => ({
     position: 'relative',
     ...($size === SIZE.compact
       ? $theme.typography.font250
@@ -25,12 +44,7 @@ export const BaseButton = styled(
     alignItems: 'center',
     justifyContent: 'center',
     border: 'none',
-    borderRadius:
-      $shape === SHAPE.round
-        ? '50%'
-        : $theme.borders.useRoundedCorners
-          ? $theme.borders.radius200
-          : '0px',
+    ...getBorderRadii({$shape, $theme}),
     textDecoration: 'none',
     outline: 'none',
     WebkitAppearance: 'none',
@@ -46,7 +60,7 @@ export const BaseButton = styled(
     // Padding For Shape and Size
     ...getStyleForShape({$theme, $shape, $size}),
     // Kind style override
-    ...getStyleForKind({$theme, $kind, $isLoading}),
+    ...getStyleForKind({$theme, $kind, $isLoading, $isSelected}),
   }),
 );
 BaseButton.displayName = 'StyledBaseButton';
@@ -163,12 +177,19 @@ export function getStyleForShape({$theme, $shape, $size}: StylePropsT) {
   }
 }
 
-export function getStyleForKind({$theme, $kind, $isLoading}: StylePropsT) {
+export function getStyleForKind({
+  $theme,
+  $isLoading,
+  $isSelected,
+  $kind,
+}: StylePropsT) {
   switch ($kind) {
     case KIND.primary:
       return {
         color: $theme.colors.buttonPrimaryText,
-        backgroundColor: $theme.colors.buttonPrimaryFill,
+        backgroundColor: $isSelected
+          ? $theme.colors.buttonPrimaryHover
+          : $theme.colors.buttonPrimaryFill,
         ':hover:enabled': {
           backgroundColor: $isLoading
             ? $theme.colors.buttonPrimaryActive
@@ -186,7 +207,9 @@ export function getStyleForKind({$theme, $kind, $isLoading}: StylePropsT) {
     case KIND.secondary:
       return {
         color: $theme.colors.buttonSecondaryText,
-        backgroundColor: $theme.colors.buttonSecondaryFill,
+        backgroundColor: $isSelected
+          ? $theme.colors.buttonSecondaryHover
+          : $theme.colors.buttonSecondaryFill,
         ':hover:enabled': {
           backgroundColor: $isLoading
             ? $theme.colors.buttonSecondaryActive
@@ -204,7 +227,9 @@ export function getStyleForKind({$theme, $kind, $isLoading}: StylePropsT) {
     case KIND.tertiary:
       return {
         color: $theme.colors.buttonTertiaryText,
-        backgroundColor: $theme.colors.buttonTertiaryFill,
+        backgroundColor: $isSelected
+          ? $theme.colors.buttonTertiaryHover
+          : $theme.colors.buttonTertiaryFill,
         ':hover:enabled': {
           backgroundColor: $isLoading
             ? $theme.colors.buttonTertiaryActive
@@ -222,7 +247,9 @@ export function getStyleForKind({$theme, $kind, $isLoading}: StylePropsT) {
     case KIND.minimal:
       return {
         color: $theme.colors.buttonMinimalText,
-        backgroundColor: $theme.colors.buttonMinimalFill,
+        backgroundColor: $isSelected
+          ? $theme.colors.buttonMinimalHover
+          : $theme.colors.buttonMinimalFill,
         ':hover:enabled': {
           backgroundColor: $isLoading
             ? $theme.colors.buttonMinimalActive
diff --git a/src/button/types.js b/src/button/types.js
index f183064f9..221e3f776 100644
--- a/src/button/types.js
+++ b/src/button/types.js
@@ -17,22 +17,24 @@ export type OverridesT = {
 };
 
 export type ButtonPropsT = {
-  overrides: OverridesT,
-  size: $Keys<typeof SIZE>,
-  kind: $Keys<typeof KIND>,
-  shape: $Keys<typeof SHAPE>,
-  isLoading: boolean,
+  children?: React$Node,
   disabled: boolean,
-  startEnhancer?: (*) => React$Node | React$Node,
   endEnhancer?: (*) => React$Node | React$Node,
-  children?: React$Node,
+  isLoading: boolean,
+  isSelected?: boolean,
+  kind: $Keys<typeof KIND>,
   onClick?: (SyntheticEvent<HTMLButtonElement>) => void,
+  overrides: OverridesT,
+  shape: $Keys<typeof SHAPE>,
+  size: $Keys<typeof SIZE>,
+  startEnhancer?: (*) => React$Node | React$Node,
 };
 
 export type SharedStylePropsT = {
-  $size?: $Keys<typeof SIZE>,
   $kind?: $Keys<typeof KIND>,
+  $isSelected?: boolean,
   $shape?: $Keys<typeof SHAPE>,
+  $size?: $Keys<typeof SIZE>,
   $isLoading?: boolean,
   $disabled?: boolean,
 };
diff --git a/src/button/utils.js b/src/button/utils.js
index ea1a649cf..e94e0d936 100644
--- a/src/button/utils.js
+++ b/src/button/utils.js
@@ -8,17 +8,19 @@ LICENSE file in the root directory of this source tree.
 import type {ButtonPropsT} from './types.js';
 
 export function getSharedProps({
+  disabled,
+  isLoading,
+  isSelected,
   kind,
-  size,
   shape,
-  isLoading,
-  disabled,
+  size,
 }: ButtonPropsT) {
   return {
-    $size: size,
+    $disabled: disabled,
+    $isLoading: isLoading,
+    $isSelected: isSelected,
     $kind: kind,
     $shape: shape,
-    $isLoading: isLoading,
-    $disabled: disabled,
+    $size: size,
   };
 }
diff --git a/src/popover/README.md b/src/popover/README.md
index 26fd5d94d..82f28a582 100644
--- a/src/popover/README.md
+++ b/src/popover/README.md
@@ -11,14 +11,12 @@ import {StatefulPopover} from 'baseui/popover';
 export default () => {
   const popoverContent = <div>Hello world</div>;
   return (
-    <React.Fragment>
-      <StatefulPopover
-        placement="topLeft"
-        content={popoverContent}
-      >
-        <span>Click me!</span>
-      </StatefulPopover>
-    </React.Fragment>
+    <StatefulPopover
+      placement="topLeft"
+      content={popoverContent}
+    >
+      <span>Click me!</span>
+    </StatefulPopover>
   );
 };
 ```
@@ -28,27 +26,27 @@ export default () => {
 ```javascript
 import * as React from 'react';
 import {StatefulPopover, StyledBody} from 'baseui/popover';
-import {withStyle} from 'styletron-react';
-
-const CustomPopoverBody = withStyle(StyledBody, {
-  borderColor: 'red',
-});
 
 export default () => {
   const popoverContent = <div>Hello world</div>;
   return (
-    <React.Fragment>
-      <StatefulPopover
-        placement="topLeft"
-        content={popoverContent}
-        triggerType="hover"
-        overrides={{
-          Body: CustomPopoverBody,
-        }}
-      >
-        <span>Hover me!</span>
-      </StatefulPopover>
-    </React.Fragment>
+    <StatefulPopover
+      placement="topLeft"
+      triggerType="hover"
+      content={({close}) => (
+        <div>
+          <h2>Hello world</h2>
+          <button onClick={close}>Close</button>
+        </div>
+      )}
+      overrides={{
+        Body: {
+          style: {borderColor: 'red'},
+        },
+      }}
+    >
+      <span>Hover me!</span>
+    </StatefulPopover>
   );
 };
 ```
@@ -67,11 +65,25 @@ export default () => {
 * `TRIGGER_TYPE`
 * `STATE_CHANGE_TYPE`
 
-## `StatefulContainer` API
+## `StatefulPopover` API
 
+* `placement?: PLACEMENT[string] = PLACEMENT.auto`
+  * How to position the popover relative to the target.
+* `content?: React.Node | ({close: () => void}) => React.Node = null`
+  * Content to render within the popover when it's shown.
+* `children: React.Node` - Required.
+  * Content that should trigger the popover to be shown (also acts as the anchor against which the popover will be positioned).
+* `triggerType?: TRIGGER_TYPE[string] = TRIGGER_TYPE.click`
+  * Whether to toggle the popover when trigger is clicked or hovered.
+* `showArrow?: boolean = false`
+  * Whether or not to show the arrow pointing from the popover to the trigger.
+* `dismissOnClickOutside?: boolean = true`
+  * Whether to hide the popover when the user clicks anywhere outside the trigger/popover.
+* `dismissOnEsc?: boolean = true`
+  * Whether to hide the popover when the user presses the escape key.
 * `initialState?: {isOpen: boolean} = {isOpen: false}`
   * Initial state of an uncontrolled popover component.
-* `stateReducer?: (type: open|close, nextState: {}, currentState: {}, e: any) => nextState`
+* `stateReducer?: (type: STATE_CHANGE_TYPE, nextState: {}, currentState: {}, e: any) => nextState`
   A state change handler.
   * `type` - state change type
   * `changes` - a new state changes that will be set
@@ -80,24 +92,16 @@ export default () => {
   * Event handler when popover is shown.
 * `onClose?: () => {}`
   * Event handler when popover is hidden.
-* `placement?: PLACEMENT[string] = PLACEMENT.auto`
-  * How to position the popover relative to the target.
-* `content?: React.Node | () => React.Node = null`
-  * Content to render within the popover when it's shown.
-* `children: React.Node` - Required.
-  * Content that should trigger the popover to be shown (also acts as the anchor against which the popover will be positioned).
 * `onMouseEnterDelay?: number = 200`
   * Number of milliseconds to wait before showing the popover after mousing enters the trigger elment (for triggerType `hover`).
 * `onMouseLeaveDelay?: number = 200`
   * Number of milliseconds to wait before hiding the popover after the mouse leaves the trigger element (for triggerType `hover`).
-* `showArrow?: boolean = false`
-  * Whether or not to show the arrow pointing from the popover to the trigger.
-* `dismissOnClickOutside?: boolean = true`
-  * Whether to hide the popover when the user clicks anywhere outside the trigger/popover.
-* `dismissOnEsc?: boolean = true`
-  * Whether to hide the popover when the user presses the escape key.
-* `triggerType?: 'click' | 'hover' = 'click'`
-  * Whether to toggle the popover when trigger is clicked or hovered.
+* `accessibilityType?: ACCESSIBILITY_TYPE[string] = ACCESSIBILITY_TYPE.menu`
+  * Controls how this popover behaves for screen readers and other assistive devices. See the A11Y section at the bottom of this document for more details.
+* `overrides?: {Arrow, Body, Inner} = {}`
+  * `Arrow?: ReactComponent | {props: {}, style: {}, component: ReactComponent}`
+  * `Body?: ReactComponent | {props: {}, style: {}, component: ReactComponent}`
+  * `Inner?: ReactComponent | {props: {}, style: {}, component: ReactComponent}`
 
 ## `Popover` API
 
@@ -109,6 +113,8 @@ export default () => {
   * Content to render within the popover when it's shown.
 * `children: React.Node` - Required.
   * Content that should trigger the popover to be shown (also acts as the anchor against which the popover will be positioned).
+* `triggerType?: TRIGGER_TYPE[string] = TRIGGER_TYPE.click`
+  * Whether to toggle the popover when trigger is clicked or hovered.
 * `showArrow?: boolean = false`
   * Whether or not to show the arrow pointing from the popover to the trigger.
 * `onClick: Event => void = null`
@@ -117,15 +123,36 @@ export default () => {
   * Handler for mouseenter events on trigger element.
 * `onMouseLeave: () => void = null`
   * Handler for mouseleave events on trigger element.
+* `onFocus: () => void = null`
+  * Handler for focus events on trigger element.
+* `onBlur: () => void = null`
+  * Handler for blur events on trigger element.
 * `onClickOutside: () => void = null`
   * Handler for clicks outside the anchor/popover elements.
 * `onEsc: () => void = null`
   * Handler for 'Escape' keypress events.
+* `onMouseEnterDelay?: number = 200`
+  * Number of milliseconds to wait before showing the popover after mousing enters the trigger elment (for triggerType `hover`).
+* `onMouseLeaveDelay?: number = 200`
+  * Number of milliseconds to wait before hiding the popover after the mouse leaves the trigger element (for triggerType `hover`).
+* `accessibilityType?: ACCESSIBILITY_TYPE[string] = ACCESSIBILITY_TYPE.menu`
+  * Controls how this popover behaves for screen readers and other assistive devices. See the A11Y section at the bottom of this document for more details.
 * `overrides?: {Arrow, Body, Inner} = {}`
   * `Arrow?: ReactComponent | {props: {}, style: {}, component: ReactComponent}`
   * `Body?: ReactComponent | {props: {}, style: {}, component: ReactComponent}`
   * `Inner?: ReactComponent | {props: {}, style: {}, component: ReactComponent}`
 
+## `StatefulContainer` API
+
+Useful if you want the same behavior as a popover with your own rendering behavior. Supports same props as `StatefulPopover` with one change:
+
+* `children: (popoverProps) => React.Node` - Required
+  * The function passed as children will receive the props that would normally be passed to `Popover`, but you can use them to render whatever content you'd like.
+
+## `StyledPadding` API
+
+This is just a regular utility styled component that allows you to easily add padding in your popover.
+
 ## Presentational components props API
 
 These properties are passed to every presentational (styled) component that is exported.
@@ -250,9 +277,9 @@ export default () => (
 
 ### Default styles
 
-The popover element will have zero padding by default. There are many valid use cases for both padded and non-padded popovers, so I'd argue that starting with zero padding and also exporting an optional `StyledPadding` is a better solution than forcing users to use a style override API to reset padding to zero.
+The popover element has zero padding by default. There are many valid use cases for both padded and non-padded popovers, so we've chosen to start with zero padding and provide an optional `StyledPadding` instead of forcing users to override styles and reset padding to zero.
 
-When used as a tooltip, it will have padding by default.
+When using the `Tooltip` component, padding is added by default.
 
 ## Accessibility
 
@@ -277,12 +304,12 @@ When `accessibilityType` is set to `none`, nothing will be added so the user can
 
 If we find other common use cases we can add different `accessibilityType`s over time.
 
-More fun reading:
-[Long-standing Bootstrap popover accessibility issue](https://github.com/twbs/bootstrap/issues/18618)
-[Stack Overflow: what can aria-haspopup be used for?](https://stackoverflow.com/questions/20380488/accessibility-what-can-aria-haspopup-be-used-for)
-[Level Access: Expanding the use of aria-haspopup](https://www.levelaccess.com/expanding-the-use-of-the-aria-haspopup-property/)
-[Why aria-controls is supposedly bad](http://www.heydonworks.com/article/aria-controls-is-poop)
-[Using the aria-owns attribute](https://tink.uk/using-the-aria-owns-attribute/)
-[W3 wai-aria menu button example](https://www.w3.org/TR/wai-aria-practices/examples/menu-button/menu-button-links.html)
-[W3 wai-aria role="tooltip"](https://www.w3.org/TR/wai-aria-1.1/#tooltip)
-[W3 wai-aria role="dialog"](https://www.w3.org/TR/wai-aria-1.1/#dialog)
+More reading on popover accessibility:
+* [Long-standing Bootstrap popover accessibility issue](https://github.com/twbs/bootstrap/issues/18618)
+* [Stack Overflow: what can aria-haspopup be used for?](https://stackoverflow.com/questions/20380488/accessibility-what-can-aria-haspopup-be-used-for)
+* [Level Access: Expanding the use of aria-haspopup](https://www.levelaccess.com/expanding-the-use-of-the-aria-haspopup-property/)
+* [Why aria-controls is supposedly bad](http://www.heydonworks.com/article/aria-controls-is-poop)
+* [Using the aria-owns attribute](https://tink.uk/using-the-aria-owns-attribute/)
+* [W3 wai-aria menu button example](https://www.w3.org/TR/wai-aria-practices/examples/menu-button/menu-button-links.html)
+* [W3 wai-aria role="tooltip"](https://www.w3.org/TR/wai-aria-1.1/#tooltip)
+* [W3 wai-aria role="dialog"](https://www.w3.org/TR/wai-aria-1.1/#dialog)
diff --git a/src/tooltip/README.md b/src/tooltip/README.md
index c00ef5d72..01a58e16b 100644
--- a/src/tooltip/README.md
+++ b/src/tooltip/README.md
@@ -1,12 +1,12 @@
 # Tooltip Component
 
-The tooltip component is essentially a more opinionated `Popover`. Implementation-wise it will just be a thin wrapper around a `Popover` with some defaults:
+The tooltip component is essentially a more opinionated `Popover`. Implementation-wise it is just a thin wrapper around a `Popover` with some defaults:
 
 * Darker theme (see Base UI docs)
 * `triggerType` default to `'hover'`
 * Adds padding to your content by default
 
-Accordingly, the RFC below will direct you to refer back to `src/popover/README.md` in many places.
+Accordingly, the docs below will direct you to `src/popover/README.md` in many places.
 
 ## Usage
 
@@ -27,12 +27,7 @@ export default () => (
 
 ```javascript
 import * as React from 'react';
-import {StatefulTooltip, StyledTooltip} from 'baseui/tooltip';
-import {withStyle} from 'styletron-react';
-
-const BigOlTooltip = withStyle(StyledTooltip, {
-  width: '500px',
-});
+import {StatefulTooltip} from 'baseui/tooltip';
 
 export default () => (
   <StatefulTooltip


Test patch:
diff --git a/src/button-group/__tests__/__snapshots__/styled-components.test.js.snap b/src/button-group/__tests__/__snapshots__/styled-components.test.js.snap
new file mode 100644
index 000000000..987baab12
--- /dev/null
+++ b/src/button-group/__tests__/__snapshots__/styled-components.test.js.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ButtonGroup styled components default styled: StyledRoot has expected default styles 1`] = `
+Object {
+  "display": "flex",
+}
+`;
+
+exports[`ButtonGroup styled components first: StyledRoot has expected styles when first element in group 1`] = `
+Object {
+  "display": "flex",
+}
+`;
+
+exports[`ButtonGroup styled components last: StyledRoot has expected styles when last element in group 1`] = `
+Object {
+  "display": "flex",
+}
+`;
diff --git a/src/button-group/__tests__/button-group.test.js b/src/button-group/__tests__/button-group.test.js
new file mode 100644
index 000000000..41bd07bdf
--- /dev/null
+++ b/src/button-group/__tests__/button-group.test.js
@@ -0,0 +1,138 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import React from 'react';
+import {shallow} from 'enzyme';
+
+import {Button} from '../../button/index.js';
+
+import {ButtonGroup} from '../index.js';
+
+function buildSimpleWrapper(props = {}) {
+  return shallow(
+    <ButtonGroup {...props}>
+      <Button />
+      <Button />
+      <Button />
+    </ButtonGroup>,
+  );
+}
+
+describe('ButtonGroup', () => {
+  it('propogates kind to children', () => {
+    const kind = 'tertiary';
+    const wrapper = buildSimpleWrapper({kind});
+    wrapper.children().forEach(child => {
+      expect(child).toHaveProp('kind', kind);
+    });
+  });
+
+  it('propogates shape to children', () => {
+    const shape = 'round';
+    const wrapper = buildSimpleWrapper({shape});
+    wrapper.children().forEach(child => {
+      expect(child).toHaveProp('shape', shape);
+    });
+  });
+
+  it('propogates size to children', () => {
+    const size = 'compact';
+    const wrapper = buildSimpleWrapper({size});
+    wrapper.children().forEach(child => {
+      expect(child).toHaveProp('size', size);
+    });
+  });
+
+  it('propogates disabled to children', () => {
+    const wrapper = buildSimpleWrapper({disabled: true});
+    wrapper.children().forEach(child => {
+      expect(child).toHaveProp('disabled', true);
+    });
+  });
+
+  it('click event on child element triggers parent handler', () => {
+    const handler = jest.fn();
+    const wrapper = buildSimpleWrapper({onClick: handler});
+    wrapper.children().forEach((child, index) => {
+      child.simulate('click');
+      expect(handler).toHaveBeenCalledTimes(index + 1);
+    });
+  });
+
+  it('does not clobber click handler on child element', () => {
+    const parentHandler = jest.fn();
+    const childHandler = jest.fn();
+    const wrapper = shallow(
+      <ButtonGroup onClick={parentHandler}>
+        <button onClick={childHandler} />
+        <button />
+      </ButtonGroup>,
+    );
+
+    const childWithHandler = wrapper.children().first();
+    const childWithoutHandler = wrapper.children().last();
+
+    childWithHandler.simulate('click');
+    expect(parentHandler).toHaveBeenCalledTimes(1);
+    expect(childHandler).toHaveBeenCalledTimes(1);
+
+    childWithoutHandler.simulate('click');
+    expect(parentHandler).toHaveBeenCalledTimes(2);
+    expect(childHandler).toHaveBeenCalledTimes(1);
+  });
+
+  it('does not clobber disabled prop on child element', () => {
+    const wrapper = shallow(
+      <ButtonGroup>
+        <button disabled />
+        <button />
+      </ButtonGroup>,
+    );
+
+    const child = wrapper.children().first();
+    expect(child).toHaveProp('disabled', true);
+  });
+
+  it('if disabled, click events do not call provided handler', () => {
+    const handler = jest.fn();
+    const wrapper = buildSimpleWrapper({disabled: true, onClick: handler});
+    const child = wrapper.children().first();
+    child.simulate('click');
+    expect(handler).toHaveBeenCalledTimes(0);
+  });
+
+  it('sets no children as selected if selected prop is null value', () => {
+    const wrapper = buildSimpleWrapper({selected: null});
+    wrapper.children().forEach(child => {
+      expect(child).toHaveProp('isSelected', false);
+    });
+  });
+
+  it('sets no children as selected if selected prop is empty array', () => {
+    const wrapper = buildSimpleWrapper({selected: []});
+    wrapper.children().forEach(child => {
+      expect(child).toHaveProp('isSelected', false);
+    });
+  });
+
+  it('sets appropriate child as selected if selected prop is a number', () => {
+    const selectedIndex = 2;
+    const wrapper = buildSimpleWrapper({selected: selectedIndex});
+    wrapper.children().forEach((child, index) => {
+      expect(child).toHaveProp('isSelected', selectedIndex === index);
+    });
+  });
+
+  it('sets appropriate child as selected if selected prop is an array', () => {
+    const selectedIndicies = [0, 2];
+    const wrapper = buildSimpleWrapper({selected: selectedIndicies});
+    wrapper.children().forEach((child, index) => {
+      expect(child).toHaveProp('isSelected', selectedIndicies.includes(index));
+    });
+  });
+});
diff --git a/src/button-group/__tests__/stateful-button-group.test.js b/src/button-group/__tests__/stateful-button-group.test.js
new file mode 100644
index 000000000..e958374a8
--- /dev/null
+++ b/src/button-group/__tests__/stateful-button-group.test.js
@@ -0,0 +1,153 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import React from 'react';
+import {mount} from 'enzyme';
+
+import {Button} from '../../button/index.js';
+
+import {StatefulButtonGroup, MODE} from '../index.js';
+
+function buildSimpleWrapper(props = {}) {
+  return mount(
+    <StatefulButtonGroup {...props}>
+      <Button />
+      <Button />
+      <Button />
+    </StatefulButtonGroup>,
+  );
+}
+
+function clickChildAtIndex(wrapper, index) {
+  const childAtIndex = wrapper.find('div').childAt(index);
+  childAtIndex.simulate('click');
+}
+
+function expectSelectedValueAtIndex(wrapper, index, value) {
+  const childAtIndex = wrapper.find('div').childAt(index);
+  expect(childAtIndex).toHaveProp('isSelected', value);
+}
+
+describe('StatefulButtonGroup', () => {
+  it('renders with default props', () => {
+    const wrapper = buildSimpleWrapper();
+    expect(wrapper).toExist();
+    expect(wrapper.find('div').first()).toExist();
+  });
+
+  describe('default mode', () => {
+    it('does not update state when clicked', () => {
+      const wrapper = buildSimpleWrapper();
+      const index = 0;
+      clickChildAtIndex(wrapper, index);
+      expectSelectedValueAtIndex(wrapper, index, false);
+    });
+  });
+
+  describe('radio mode', () => {
+    it('sets button selected when initially clicked', () => {
+      const wrapper = buildSimpleWrapper({mode: MODE.radio});
+      const index = 0;
+      clickChildAtIndex(wrapper, index);
+      expectSelectedValueAtIndex(wrapper, index, true);
+    });
+
+    it('sets button unselected on second click', () => {
+      const wrapper = buildSimpleWrapper({mode: MODE.radio});
+      const index = 1;
+      clickChildAtIndex(wrapper, index);
+      expectSelectedValueAtIndex(wrapper, index, true);
+      clickChildAtIndex(wrapper, index);
+      expectSelectedValueAtIndex(wrapper, index, false);
+    });
+
+    it('sets button unselected when another child is clicked', () => {
+      const wrapper = buildSimpleWrapper({mode: MODE.radio});
+      const first = 0;
+      const last = 2;
+      clickChildAtIndex(wrapper, first);
+      expectSelectedValueAtIndex(wrapper, first, true);
+      clickChildAtIndex(wrapper, last);
+      expectSelectedValueAtIndex(wrapper, first, false);
+      expectSelectedValueAtIndex(wrapper, last, true);
+    });
+
+    it('sets initial selected', () => {
+      const wrapper = buildSimpleWrapper({
+        mode: MODE.radio,
+        initialState: {selected: 0},
+      });
+      expectSelectedValueAtIndex(wrapper, 0, true);
+      expectSelectedValueAtIndex(wrapper, 1, false);
+      expectSelectedValueAtIndex(wrapper, 2, false);
+    });
+
+    it('handles provided state reducer', () => {
+      const wrapper = buildSimpleWrapper({
+        mode: MODE.radio,
+        // always sets the first child selected
+        stateReducer: () => ({selected: [0]}),
+      });
+
+      expectSelectedValueAtIndex(wrapper, 0, false);
+      clickChildAtIndex(wrapper, 2);
+      expectSelectedValueAtIndex(wrapper, 0, true);
+    });
+  });
+
+  describe('checkbox mode', () => {
+    it('sets button selected when initially clicked', () => {
+      const wrapper = buildSimpleWrapper({mode: MODE.checkbox});
+      const index = 0;
+      clickChildAtIndex(wrapper, index);
+      expectSelectedValueAtIndex(wrapper, index, true);
+    });
+
+    it('sets button unselected on second click', () => {
+      const wrapper = buildSimpleWrapper({mode: MODE.checkbox});
+      const index = 1;
+      clickChildAtIndex(wrapper, index);
+      expectSelectedValueAtIndex(wrapper, index, true);
+      clickChildAtIndex(wrapper, index);
+      expectSelectedValueAtIndex(wrapper, index, false);
+    });
+
+    it('maintains selection when another child is clicked', () => {
+      const wrapper = buildSimpleWrapper({mode: MODE.checkbox});
+      const first = 0;
+      const last = 2;
+      clickChildAtIndex(wrapper, first);
+      expectSelectedValueAtIndex(wrapper, first, true);
+      clickChildAtIndex(wrapper, last);
+      expectSelectedValueAtIndex(wrapper, first, true);
+      expectSelectedValueAtIndex(wrapper, last, true);
+    });
+
+    it('sets initial selected', () => {
+      const wrapper = buildSimpleWrapper({
+        mode: MODE.checkbox,
+        initialState: {selected: [0, 1]},
+      });
+      expectSelectedValueAtIndex(wrapper, 0, true);
+      expectSelectedValueAtIndex(wrapper, 1, true);
+      expectSelectedValueAtIndex(wrapper, 2, false);
+    });
+
+    it('handles provided state reducer', () => {
+      const wrapper = buildSimpleWrapper({
+        mode: MODE.checkbox,
+        // always sets the first child selected
+        stateReducer: () => ({selected: [0]}),
+      });
+
+      expectSelectedValueAtIndex(wrapper, 0, false);
+      clickChildAtIndex(wrapper, 2);
+      expectSelectedValueAtIndex(wrapper, 0, true);
+    });
+  });
+});
diff --git a/src/button-group/__tests__/stateful-container.test.js b/src/button-group/__tests__/stateful-container.test.js
new file mode 100644
index 000000000..c28af9b35
--- /dev/null
+++ b/src/button-group/__tests__/stateful-container.test.js
@@ -0,0 +1,71 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import React from 'react';
+import {mount} from 'enzyme';
+
+import {StatefulContainer, MODE} from '../index.js';
+
+describe('ButtonGroup StatefulContainer', () => {
+  it('provides expected props to children render function', () => {
+    const children = jest.fn(() => <div>children</div>);
+    mount(<StatefulContainer>{children}</StatefulContainer>);
+
+    const actual = children.mock.calls[0][0];
+    expect(actual).toHaveProperty('onClick');
+    expect(actual).toHaveProperty('selected', []);
+  });
+
+  it('calls provided click handler', () => {
+    const onClick = jest.fn();
+    const wrapper = mount(
+      <StatefulContainer onClick={onClick}>
+        {childProps => <div {...childProps}>children</div>}
+      </StatefulContainer>,
+    );
+
+    const element = wrapper.find('div').first();
+    element.simulate('click');
+
+    expect(onClick).toHaveBeenCalledTimes(1);
+  });
+
+  it('does not call state reducer if mode is not set', () => {
+    const onClick = jest.fn();
+    const stateReducer = jest.fn();
+    const wrapper = mount(
+      <StatefulContainer onClick={onClick} stateReducer={stateReducer}>
+        {childProps => <div {...childProps}>children</div>}
+      </StatefulContainer>,
+    );
+
+    const element = wrapper.find('div').first();
+    element.simulate('click');
+
+    expect(stateReducer).toHaveBeenCalledTimes(0);
+  });
+
+  it('calls state reducer if mode is set', () => {
+    const onClick = jest.fn();
+    const stateReducer = jest.fn();
+    const wrapper = mount(
+      <StatefulContainer
+        mode={MODE.radio}
+        onClick={onClick}
+        stateReducer={stateReducer}
+      >
+        {childProps => <div {...childProps}>children</div>}
+      </StatefulContainer>,
+    );
+
+    const element = wrapper.find('div').first();
+    element.simulate('click');
+
+    expect(stateReducer).toHaveBeenCalledTimes(1);
+  });
+});
diff --git a/src/button-group/__tests__/styled-components.test.js b/src/button-group/__tests__/styled-components.test.js
new file mode 100644
index 000000000..185a65530
--- /dev/null
+++ b/src/button-group/__tests__/styled-components.test.js
@@ -0,0 +1,37 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import * as React from 'react';
+import {shallow} from 'enzyme';
+
+import {StyledRoot} from '../index.js';
+
+describe('ButtonGroup styled components', () => {
+  const styledComponents = [[StyledRoot, 'StyledRoot']];
+
+  test.each(styledComponents)('default styled', (Component, name) => {
+    const component = shallow(<Component />);
+    expect(component.instance().getStyles()).toMatchSnapshot(
+      `${name} has expected default styles`,
+    );
+  });
+
+  test.each(styledComponents)('first', (Component, name) => {
+    const component = shallow(<Component $first />);
+    expect(component.instance().getStyles()).toMatchSnapshot(
+      `${name} has expected styles when first element in group`,
+    );
+  });
+
+  test.each(styledComponents)('last', (Component, name) => {
+    const component = shallow(<Component $last />);
+    expect(component.instance().getStyles()).toMatchSnapshot(
+      `${name} has expected styles when last element in group`,
+    );
+  });
+});
diff --git a/src/button/__tests__/__snapshots__/styled-components.test.js.snap b/src/button/__tests__/__snapshots__/styled-components.test.js.snap
index 587b74c3c..55c499d48 100644
--- a/src/button/__tests__/__snapshots__/styled-components.test.js.snap
+++ b/src/button/__tests__/__snapshots__/styled-components.test.js.snap
@@ -10,7 +10,10 @@ Object {
   "WebkitAppearance": "none",
   "alignItems": "center",
   "border": "none",
-  "borderRadius": "$theme.borders.radius200",
+  "borderBottomLeftRadius": "$theme.borders.radius200",
+  "borderBottomRightRadius": "$theme.borders.radius200",
+  "borderTopLeftRadius": "$theme.borders.radius200",
+  "borderTopRightRadius": "$theme.borders.radius200",
   "cursor": "pointer",
   "display": "inline-flex",
   "fontFamily": "$theme.typography.font450.fontFamily",
@@ -41,7 +44,10 @@ Object {
   "WebkitAppearance": "none",
   "alignItems": "center",
   "border": "none",
-  "borderRadius": "$theme.borders.radius200",
+  "borderBottomLeftRadius": "$theme.borders.radius200",
+  "borderBottomRightRadius": "$theme.borders.radius200",
+  "borderTopLeftRadius": "$theme.borders.radius200",
+  "borderTopRightRadius": "$theme.borders.radius200",
   "cursor": "pointer",
   "display": "inline-flex",
   "fontFamily": "$theme.typography.font450.fontFamily",
@@ -72,7 +78,10 @@ Object {
   "WebkitAppearance": "none",
   "alignItems": "center",
   "border": "none",
-  "borderRadius": "$theme.borders.radius200",
+  "borderBottomLeftRadius": "$theme.borders.radius200",
+  "borderBottomRightRadius": "$theme.borders.radius200",
+  "borderTopLeftRadius": "$theme.borders.radius200",
+  "borderTopRightRadius": "$theme.borders.radius200",
   "cursor": "pointer",
   "display": "inline-flex",
   "fontFamily": "$theme.typography.font450.fontFamily",
diff --git a/src/button/__tests__/styled-components.test.js b/src/button/__tests__/styled-components.test.js
index 70c9de739..ee8ac6bcb 100644
--- a/src/button/__tests__/styled-components.test.js
+++ b/src/button/__tests__/styled-components.test.js
@@ -84,7 +84,14 @@ describe('Button Styled Components', () => {
       borders: {useRoundedCorners: false},
     });
     const rendered = shallow(<BaseButton $theme={theme}>Test</BaseButton>);
-    expect(rendered.instance().getStyles().borderRadius).toMatch('0px');
+    expect(rendered.instance().getStyles().borderBottomLeftRadius).toMatch(
+      '0px',
+    );
+    expect(rendered.instance().getStyles().borderBottomRightRadius).toMatch(
+      '0px',
+    );
+    expect(rendered.instance().getStyles().borderTopLeftRadius).toMatch('0px');
+    expect(rendered.instance().getStyles().borderTopRightRadius).toMatch('0px');
   });
 
   makeTest({
diff --git a/src/pagination/__tests__/pagination.test.js b/src/pagination/__tests__/pagination.test.js
index af4a4743a..f1f79ee0f 100644
--- a/src/pagination/__tests__/pagination.test.js
+++ b/src/pagination/__tests__/pagination.test.js
@@ -51,6 +51,7 @@ describe('Pagination Stateless', () => {
         $ref,
         $disabled,
         $isLoading,
+        $isSelected,
         initialState,
         onItemSelect,
         ...restProps