Repository: mlaursen/react-md
Pull request #766 (2018-06-21 14:44:54)

[ListItem] Optional, right aligned action button with delimiter
**Screenshots**

![chrome_2018-06-21_16-42-32](https://user-images.githubusercontent.com/5131112/41726314-295ea632-7572-11e8-8473-19ab46e97732.png)

![chrome_2018-06-21_16-42-25](https://user-images.githubusercontent.com/5131112/41726313-293b778e-7572-11e8-8274-9fddbf69ddac.png)


Patch:
diff --git a/docs/src/components/Components/Lists/Simple.jsx b/docs/src/components/Components/Lists/Simple.jsx
index e45c1120b8..0ac24010a2 100644
--- a/docs/src/components/Components/Lists/Simple.jsx
+++ b/docs/src/components/Components/Lists/Simple.jsx
@@ -6,6 +6,7 @@ import {
   List,
   ListItem,
   Subheader,
+  Button,
 } from 'react-md';
 
 const InfoIcon = () => <FontIcon>info</FontIcon>;
@@ -78,6 +79,23 @@ const Simple = () => (
         threeLines
       />
     </List>
+    <List className="md-cell md-paper md-paper--1">
+      <ListItem primaryText="Additional information" />
+      <ListItem primaryText="Website" renderChildrenOutside>
+        <Button icon primary>public</Button>
+      </ListItem>
+      <ListItem primaryText="Mail" renderChildrenOutside>
+        <Button icon>mail</Button>
+      </ListItem>
+      <ListItem
+        leftAvatar={<Avatar icon={<FontIcon>folder</FontIcon>} />}
+        primaryText="Work"
+        secondaryText="Jan 28, 2014"
+        renderChildrenOutside
+      >
+        <Button icon>share</Button>
+      </ListItem>
+    </List>
   </div>
 );
 
diff --git a/src/js/Lists/ListItem.d.ts b/src/js/Lists/ListItem.d.ts
index dcc28b2748..6ba45680f1 100644
--- a/src/js/Lists/ListItem.d.ts
+++ b/src/js/Lists/ListItem.d.ts
@@ -51,7 +51,11 @@ export interface ListItemProps extends BaseListItemProps, InjectedInkProps {
   activeBoxStyle?: React.CSSProperties;
   activeBoxClassName?: string;
   animateNestedItems?: boolean;
+  renderChildrenOutside?: boolean;
   itemRef?: (ref: React.ReactHTMLElement<any> | null) => null;
+  actionButtonIcon?: string,
+  actionButtonOnClick?: (event: React.MouseEvent<HTMLElement>) => void,
+  actionButtonPrimary?: boolean,
 
   /**
    * @deprecated
diff --git a/src/js/Lists/ListItem.js b/src/js/Lists/ListItem.js
index abb0a8b462..ebf37b678e 100644
--- a/src/js/Lists/ListItem.js
+++ b/src/js/Lists/ListItem.js
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
 import { findDOMNode } from 'react-dom';
 import cn from 'classnames';
 import deprecated from 'react-prop-types/lib/deprecated';
-
 import themeColors from '../utils/themeColors';
 import getCollapserStyles from '../utils/getCollapserStyles';
 import getField from '../utils/getField';
@@ -19,6 +18,7 @@ import TileAddon from './TileAddon';
 import ListItemText from './ListItemText';
 import List from './List';
 import Menu from '../Menus/Menu';
+import Divider from '../Dividers/Divider';
 
 /**
  * The `ListItem` component is used for rendering a `li` tag with text and optional
@@ -323,6 +323,11 @@ export default class ListItem extends PureComponent {
      */
     animateNestedItems: PropTypes.bool,
 
+    /**
+     * Boolean if the children should be rendered outside auf the AccessibleFakeButton component.
+     */
+    renderChildrenOutside: PropTypes.bool,
+
     /**
      * Defines the number of items in the list. This is only required when all items in the
      * list are not present in the DOM.
@@ -383,6 +388,7 @@ export default class ListItem extends PureComponent {
     component: 'div',
     itemComponent: 'li',
     expanderIcon: <FontIcon>keyboard_arrow_down</FontIcon>,
+    renderChildrenOutside: false,
   };
 
   static contextTypes = {
@@ -539,6 +545,30 @@ export default class ListItem extends PureComponent {
     }
   };
 
+  _renderChildrenIfInside() {
+    const { children, renderChildrenOutside } = this.props;
+
+    return !renderChildrenOutside ? children : null;
+  }
+
+  _renderChildrenIfOutside() {
+    const { children, renderChildrenOutside } = this.props;
+
+    if (renderChildrenOutside) {
+      return (
+        <div className="md-list-item--flex">
+          <Divider vertical className="md-list-item--divider-vertical" />
+
+          <div className="md-list-item--children-inline">
+            {children}
+          </div>
+        </div>
+      );
+    }
+
+    return null;
+  }
+
   render() {
     const {
       style,
@@ -567,7 +597,6 @@ export default class ListItem extends PureComponent {
       primaryText,
       secondaryText,
       threeLines,
-      children,
       nestedItems,
       prependNested,
       active,
@@ -584,6 +613,7 @@ export default class ListItem extends PureComponent {
       passPropsToItem,
       'aria-setsize': ariaSize,
       'aria-posinset': ariaPos,
+      renderChildrenOutside,
 
       // deprecated
       isOpen,
@@ -687,6 +717,7 @@ export default class ListItem extends PureComponent {
           'md-list-tile--two-lines': secondaryText && !threeLines,
           'md-list-tile--three-lines': secondaryText && threeLines,
           'md-list-item--inset': inset && !leftIcon && !leftAvatar,
+          'md-list-item--button-grow': renderChildrenOutside,
         }, themeColors({ disabled, text: true }), tileClassName)}
         aria-expanded={nestedList && !cascadingMenu ? visible : null}
       >
@@ -710,7 +741,7 @@ export default class ListItem extends PureComponent {
           secondaryTextClassName={secondaryTextClassName}
         />
         {rightNode}
-        {children}
+        {this._renderChildrenIfInside()}
       </AccessibleFakeInkedButton>
     );
 
@@ -721,6 +752,7 @@ export default class ListItem extends PureComponent {
       className: cn('md-list-item', {
         'md-list-item--nested-container': nestedItems,
         [activeBoxClassName]: activeBoxClassName && active,
+        'md-list-item--flex': renderChildrenOutside,
       }, className),
       'aria-setsize': ariaSize,
       'aria-posinset': ariaPos,
@@ -754,6 +786,7 @@ export default class ListItem extends PureComponent {
       <ItemComponent {...sharedProps}>
         {prependNested ? nestedList : null}
         {tile}
+        {this._renderChildrenIfOutside()}
         {prependNested ? null : nestedList}
       </ItemComponent>
     );
diff --git a/src/js/SelectionControls/SelectionControl.js b/src/js/SelectionControls/SelectionControl.js
index 92a565afc8..3ebf500003 100644
--- a/src/js/SelectionControls/SelectionControl.js
+++ b/src/js/SelectionControls/SelectionControl.js
@@ -57,13 +57,6 @@ export default class SelectionControl extends PureComponent {
       PropTypes.string,
     ]),
 
-    /**
-     * An optional id of an element that describes this selection control.
-     * In the case of a fieldset, this should be the id of the fieldset legend which ensures
-     * screen readers provides additional context about the selection control
-     */
-    'aria-describedby': PropTypes.string,
-
     /**
      * An optional style to apply to the selection control's container.
      */
@@ -323,7 +316,6 @@ export default class SelectionControl extends PureComponent {
       disabledInteractions,
       'aria-label': ariaLabel,
       'aria-labelledby': ariaLabelledBy,
-      'aria-describedby': ariaDescribedBy,
       /* eslint-disable no-unused-vars */
       label: propLabel,
       checked: propChildren,
@@ -352,7 +344,7 @@ export default class SelectionControl extends PureComponent {
 
     const checked = getField(this.props, this.state, 'checked');
     const isSwitch = type === 'switch';
-    const label = this.props.label && <span id={`${id}-label`}>{this.props.label}</span>;
+    const label = this.props.label && <span>{this.props.label}</span>;
 
     let control;
     if (isSwitch) {
@@ -369,9 +361,6 @@ export default class SelectionControl extends PureComponent {
             secondary: checked,
           }))}
           aria-checked={checked}
-          aria-label={ariaLabel}
-          aria-labelledby={ariaLabelledBy || `${id}-label`}
-          aria-describedby={ariaDescribedBy}
           tabIndex={tabIndex}
           disabled={disabled}
         >
@@ -403,6 +392,8 @@ export default class SelectionControl extends PureComponent {
           name={name}
           value={value}
           aria-hidden
+          aria-label={ariaLabel}
+          aria-labelledby={ariaLabelledBy}
         />
         <label
           htmlFor={id}
diff --git a/src/js/SelectionControls/SelectionControlGroup.js b/src/js/SelectionControls/SelectionControlGroup.js
index 16f1b12bb7..b5992a7278 100644
--- a/src/js/SelectionControls/SelectionControlGroup.js
+++ b/src/js/SelectionControls/SelectionControlGroup.js
@@ -429,7 +429,6 @@ export default class SelectionControlGroup extends PureComponent {
         uncheckedRadioIcon,
         checkedCheckboxIcon,
         uncheckedCheckboxIcon,
-        'aria-describedby': `${id}-group-label`,
         ...control,
         style,
         className: cn(controlClassName, control.className),
@@ -440,7 +439,7 @@ export default class SelectionControlGroup extends PureComponent {
 
     let ariaLabel;
     if (label) {
-      ariaLabel = <LabelComponent className={labelClassName} id={`${id}-group-label`}>{label}</LabelComponent>;
+      ariaLabel = <LabelComponent className={labelClassName}>{label}</LabelComponent>;
     }
 
     return (
diff --git a/src/scss/components/_lists.scss b/src/scss/components/_lists.scss
index e8894bb1d3..96381c3f63 100644
--- a/src/scss/components/_lists.scss
+++ b/src/scss/components/_lists.scss
@@ -294,7 +294,7 @@ $md-list-addon-line-height: $md-line-height !default;
       flex-shrink: 0;
     }
 
-    .md-divider {
+    .md-divider:not(.md-list-item--divider-vertical) {
       margin-bottom: $md-list-divider-margin;
       margin-top: $md-list-divider-margin;
     }
@@ -322,8 +322,32 @@ $md-list-addon-line-height: $md-line-height !default;
     }
   }
 
-  .md-list-item--inset {
-    padding-left: $md-list-left-padding;
+  .md-list-item {
+    &--inset {
+      padding-left: $md-list-left-padding;
+    }
+
+    &--flex {
+      display: flex;
+    }
+
+    &--button-grow {
+      flex-grow: 1;
+    }
+
+    &--divider-vertical {
+      flex-shrink: 0;
+      height: calc(100% - 4px);
+      margin-left: 2px;
+      margin-right: 2px;
+      margin-top: 2px;
+      width: 2px;
+    }
+
+    &--children-inline {
+      align-self: center;
+      padding-right: 2px;
+    }
   }
 
   .md-tile-content {


Test patch:
diff --git a/src/js/Lists/__tests__/ListItem.js b/src/js/Lists/__tests__/ListItem.js
index 09dd18b00f..7356431268 100644
--- a/src/js/Lists/__tests__/ListItem.js
+++ b/src/js/Lists/__tests__/ListItem.js
@@ -12,6 +12,7 @@ import {
 import ListItem from '../ListItem';
 import FontIcon from '../../FontIcons';
 import AccessibleFakeInkedButton from '../../Helpers/AccessibleFakeInkedButton';
+import Divider from '../../Dividers/Divider';
 
 describe('ListItem', () => {
   const StarIcon = () => <FontIcon>star</FontIcon>;
@@ -236,4 +237,15 @@ describe('ListItem', () => {
     const tree = renderer.create(<ListItem primaryText="Test" />).toJSON();
     expect(tree).toMatchSnapshot();
   });
+
+  it('should use the "renderChildrenOutside" property', () => {
+    const props = {
+      primaryText: 'Test renderChildrenOutside prop',
+      renderChildrenOutside: true,
+    };
+    const listItem = mount(<ListItem {...props} />);
+    const divider = listItem.find(Divider);
+
+    expect(divider).toMatchSnapshot();
+  });
 });
diff --git a/src/js/Lists/__tests__/__snapshots__/ListItem.js.snap b/src/js/Lists/__tests__/__snapshots__/ListItem.js.snap
index cae2afba84..6ab10c909e 100644
--- a/src/js/Lists/__tests__/__snapshots__/ListItem.js.snap
+++ b/src/js/Lists/__tests__/__snapshots__/ListItem.js.snap
@@ -42,3 +42,14 @@ exports[`ListItem should render correctly with a snapshot tester 1`] = `
   </div>
 </li>
 `;
+
+exports[`ListItem should use the "renderChildrenOutside" property 1`] = `
+<Divider
+  className="md-list-item--divider-vertical"
+  vertical={true}
+>
+  <div
+    className="md-divider md-divider--vertical md-list-item--divider-vertical"
+  />
+</Divider>
+`;