Skip to content

Commit 5572b13

Browse files
eps1lonoliviertassinari
authored andcommitted
[ListItem] Improve ListItemSecondaryAction DX (#14350)
* [ListItem] Warn against wrong position for secondary action * [docs] Improve documentation of ListItemSecondaryAction interactions * Update ListItem.js * Update ListItem.test.js * Update list-item.md
1 parent f5b14b3 commit 5572b13

File tree

5 files changed

+68
-11
lines changed

5 files changed

+68
-11
lines changed

packages/material-ui/src/ListItem/ListItem.js

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
33
import classNames from 'classnames';
4-
import { componentPropType } from '@material-ui/utils';
4+
import { chainPropTypes, componentPropType } from '@material-ui/utils';
55
import withStyles from '../styles/withStyles';
66
import ButtonBase from '../ButtonBase';
77
import { isMuiElement } from '../utils/reactHelpers';
@@ -83,6 +83,9 @@ export const styles = theme => ({
8383
selected: {},
8484
});
8585

86+
/**
87+
* Uses an additional container component if `ListItemSecondaryAction` is the last child.
88+
*/
8689
function ListItem(props) {
8790
const {
8891
alignItems,
@@ -179,9 +182,35 @@ ListItem.propTypes = {
179182
*/
180183
button: PropTypes.bool,
181184
/**
182-
* The content of the component.
185+
* The content of the component. If a `ListItemSecondaryAction` is used it must
186+
* be the last child.
183187
*/
184-
children: PropTypes.node,
188+
children: chainPropTypes(PropTypes.node, props => {
189+
const children = React.Children.toArray(props.children);
190+
191+
// React.Children.toArray(props.children).findLastIndex(isListItemSecondaryAction)
192+
let secondaryActionIndex = -1;
193+
for (let i = children.length - 1; i >= 0; i -= 1) {
194+
const child = children[i];
195+
if (isMuiElement(child, ['ListItemSecondaryAction'])) {
196+
secondaryActionIndex = i;
197+
break;
198+
}
199+
}
200+
201+
// is ListItemSecondaryAction the last child of ListItem
202+
if (secondaryActionIndex !== -1 && secondaryActionIndex !== children.length - 1) {
203+
return new Error(
204+
'Material-UI: you used an element after ListItemSecondaryAction. ' +
205+
'For ListItem to detect that it has a secondary action ' +
206+
`you must pass it has the last children to ListItem.${
207+
process.env.NODE_ENV === 'test' ? Date.now() : ''
208+
}`,
209+
);
210+
}
211+
212+
return null;
213+
}),
185214
/**
186215
* Override or extend the styles applied to the component.
187216
* See [CSS API](#css-api) below for more details.
@@ -198,12 +227,11 @@ ListItem.propTypes = {
198227
*/
199228
component: componentPropType,
200229
/**
201-
* The container component used when a `ListItemSecondaryAction` is rendered.
230+
* The container component used when a `ListItemSecondaryAction` is the last child.
202231
*/
203232
ContainerComponent: componentPropType,
204233
/**
205-
* Properties applied to the container element when the component
206-
* is used to display a `ListItemSecondaryAction`.
234+
* Properties applied to the container component if used.
207235
*/
208236
ContainerProps: PropTypes.object,
209237
/**

packages/material-ui/src/ListItem/ListItem.test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import { assert } from 'chai';
33
import { getClasses, createMount, findOutermostIntrinsic } from '@material-ui/core/test-utils';
4+
import consoleErrorMock from 'test/utils/consoleErrorMock';
45
import ListItemText from '../ListItemText';
56
import ListItemSecondaryAction from '../ListItemSecondaryAction';
67
import ListItem from './ListItem';
@@ -166,6 +167,31 @@ describe('<ListItem />', () => {
166167
assert.strictEqual(listItem.hasClass(classes.container), true);
167168
assert.strictEqual(listItem.hasClass('bubu'), true);
168169
});
170+
171+
describe('warnings', () => {
172+
beforeEach(() => {
173+
consoleErrorMock.spy();
174+
});
175+
176+
afterEach(() => {
177+
consoleErrorMock.reset();
178+
});
179+
180+
it('warns if it cant detect the secondary action properly', () => {
181+
mount(
182+
<ListItem>
183+
<ListItemSecondaryAction>I should have come last :(</ListItemSecondaryAction>
184+
<ListItemText>My position doesn not matter.</ListItemText>
185+
</ListItem>,
186+
);
187+
188+
assert.strictEqual(consoleErrorMock.callCount(), 1);
189+
assert.include(
190+
consoleErrorMock.args()[0][0],
191+
'Warning: Failed prop type: Material-UI: you used an element',
192+
);
193+
});
194+
});
169195
});
170196

171197
describe('prop: focusVisibleClassName', () => {

packages/material-ui/src/ListItemSecondaryAction/ListItemSecondaryAction.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export const styles = {
1313
},
1414
};
1515

16+
/**
17+
* Must be used as the last child of ListItem to function properly.
18+
*/
1619
function ListItemSecondaryAction(props) {
1720
const { children, classes, className, ...other } = props;
1821

pages/api/list-item-secondary-action.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ filename: /packages/material-ui/src/ListItemSecondaryAction/ListItemSecondaryAct
1212
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
1313
```
1414

15-
15+
Must be used as the last child of ListItem to function properly.
1616

1717
## Props
1818

pages/api/list-item.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,19 @@ filename: /packages/material-ui/src/ListItem/ListItem.js
1212
import ListItem from '@material-ui/core/ListItem';
1313
```
1414

15-
15+
Uses an additional container component if `ListItemSecondaryAction` is the last child.
1616

1717
## Props
1818

1919
| Name | Type | Default | Description |
2020
|:-----|:-----|:--------|:------------|
2121
| <span class="prop-name">alignItems</span> | <span class="prop-type">enum:&nbsp;'flex-start'&nbsp;&#124;<br>&nbsp;'center'<br></span> | <span class="prop-default">'center'</span> | Defines the `align-items` style property. |
2222
| <span class="prop-name">button</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the list item will be a button (using `ButtonBase`). |
23-
| <span class="prop-name">children</span> | <span class="prop-type">node</span> |   | The content of the component. |
23+
| <span class="prop-name">children</span> | <span class="prop-type">node</span> |   | The content of the component. If a `ListItemSecondaryAction` is used it must be the last child. |
2424
| <span class="prop-name">classes</span> | <span class="prop-type">object</span> |   | Override or extend the styles applied to the component. See [CSS API](#css-api) below for more details. |
2525
| <span class="prop-name">component</span> | <span class="prop-type">Component</span> |   | The component used for the root node. Either a string to use a DOM element or a component. By default, it's a `li` when `button` is `false` and a `div` when `button` is `true`. |
26-
| <span class="prop-name">ContainerComponent</span> | <span class="prop-type">Component</span> | <span class="prop-default">'li'</span> | The container component used when a `ListItemSecondaryAction` is rendered. |
27-
| <span class="prop-name">ContainerProps</span> | <span class="prop-type">object</span> |   | Properties applied to the container element when the component is used to display a `ListItemSecondaryAction`. |
26+
| <span class="prop-name">ContainerComponent</span> | <span class="prop-type">Component</span> | <span class="prop-default">'li'</span> | The container component used when a `ListItemSecondaryAction` is the last child. |
27+
| <span class="prop-name">ContainerProps</span> | <span class="prop-type">object</span> |   | Properties applied to the container component if used. |
2828
| <span class="prop-name">dense</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, compact vertical padding designed for keyboard and mouse input will be used. |
2929
| <span class="prop-name">disabled</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the list item will be disabled. |
3030
| <span class="prop-name">disableGutters</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the left and right padding is removed. |

0 commit comments

Comments
 (0)