Material-ui: [Accordion] Additional actions in summary

Created on 7 Dec 2017  路  38Comments  路  Source: mui-org/material-ui

The ExpansionPanelSummary handles click to expand/collapse details panel. But I want to put some actions buttons to it. But it's not possible to disable click on the whole summary panel and handle only expandIcon click.

Accordion enhancement good first issue hacktoberfest important

Most helpful comment

There is a way to get exactly the behavior you guys want (should also solve your problem @chathuraa):

<ExpansionPanel expanded={this.state.expansionPanelOpen}> <ExpansionPanelSummary expandIcon={<ExpandMoreIcon onClick={() => { this.setState({ expansionPanelOpen: !this.state.expansionPanelOpen }); }}/>}> ....

You can manage the ExpansionPanel expanded state yourself, and by using the onClick event of the Icon which is passed to the ExpansionPanelSummary (which is the header line) you can open/close the expansion panel only via the icon)

All 38 comments

For a workaround use event.stopPropagation() at the click event of controls which are on the summary panel.

Also please remove next style .MuiExpansionPanelSummary-content-35 > :last-child {
/* padding-right: 32px; */
}

@futbolistua were you able to work around this? I'm also trying to prevent the expansion by clicking on the summary (I just want to expand when you click on the icon).

@futbolistua were you able to find a solution? I'm having the same issue

You can do what @codeskills-nl mentioned

  clickSummary = event => {
    event.stopPropagation();
  };

...

<ExpansionPanel onClick={this.clickSummary}>

Hi @stevewillard,

Thanks for the suggestion. I have an MUI checkbox in the summary, so when I do stopPropagation, it's messing up check/unchecking the checkbox.

Thanks,
Chat

There is a way to get exactly the behavior you guys want (should also solve your problem @chathuraa):

<ExpansionPanel expanded={this.state.expansionPanelOpen}> <ExpansionPanelSummary expandIcon={<ExpandMoreIcon onClick={() => { this.setState({ expansionPanelOpen: !this.state.expansionPanelOpen }); }}/>}> ....

You can manage the ExpansionPanel expanded state yourself, and by using the onClick event of the Icon which is passed to the ExpansionPanelSummary (which is the header line) you can open/close the expansion panel only via the icon)

Amazing workaround Loktor!

This is pretty common in even regular JS. One way to mitigate this that we're doing is ensuring all input components have a common wrapper. Then you can add the onClick handler to the wrapping element and once the click event reaches it, you can stop propagating.

/* see: https://github.com/mui-org/material-ui/issues/9427 */
const stopPropagation = (e) => e.stopPropagation();
const InputWrapper = ({ children }) =>
  <div onClick={stopPropagation}>
    {children}
  </div>

// usage:

<InputWrapper>
  <MyCoolInput />
</InputWrapper>

This would be a decent way to handle it, if you need to have the summary be clickable. This lets you control propagation selectively if you wish too. Otherwise @Loktor's method works well too, you just might need to override the cursor styles and some attributes because the Summary is still tabbable and has aria roles on it.

Thanks @kamranayub your solution is the best here.
It would be nice to have "actions" in the ExpansionPanelSummary (that could be aligned left or right maybe?) where the default behaviour is like this, even if it's easier now with this solution.

There is a way to get exactly the behavior you guys want (should also solve your problem @chathuraa):

<ExpansionPanel expanded={this.state.expansionPanelOpen}>
             <ExpansionPanelSummary expandIcon={<ExpandMoreIcon onClick={() => {
               this.setState({
                 expansionPanelOpen: !this.state.expansionPanelOpen
               });
             }}/>}>
               ....

You can manage the ExpansionPanel expanded state yourself, and by using the onClick event of the Icon which is passed to the ExpansionPanelSummary (which is the header line) you can open/close the expansion panel only via the icon)

This works fine. But I have an array which I populate using map function. Let's say I have 5 expansion panels and after clicking on the icon button, I change the state as mentioned in your answer. However, the problem with the approach is that it expands all expansion panels but I only want to expand the expansion panel being clicked

There is a way to get exactly the behavior you guys want (should also solve your problem @chathuraa):

<ExpansionPanel expanded={this.state.expansionPanelOpen}>
             <ExpansionPanelSummary expandIcon={<ExpandMoreIcon onClick={() => {
               this.setState({
                 expansionPanelOpen: !this.state.expansionPanelOpen
               });
             }}/>}>
               ....

You can manage the ExpansionPanel expanded state yourself, and by using the onClick event of the Icon which is passed to the ExpansionPanelSummary (which is the header line) you can open/close the expansion panel only via the icon)

This works fine. But I have an array which I populate using map function. Let's say I have 5 expansion panels and after clicking on the icon button, I change the state as mentioned in your answer. However, the problem with the approach is that it expands all expansion panels but I only want to expand the expansion panel being clicked

@Usama-Tahir

You can use an arbitrary condition based on a local state to decide what panel will be opened. For example:

state = {
  panel: ''
};

const handleStateChange = panel => () => this.setState({ panel});

// This would be your list of available panels, 
// which I suppose you go through to create your panels.
const panels = {
  panelOne: 'panelOne',
  panelTwo: 'panelTwo',
};

Object.keys(panels).map(panel => (
  <ExpansionPanel expanded={this.state.panel === panel}>
    <ExpansionPanelSummary
      expandIcon={<ExpandMoreIcon onClick={handleStateChange(panel)} />}
    >
      ...
    </ExpansionPanelSummary>
    ...
  </ExpansionPanel>
));

@b-ferreira thanks

I have a checkbox inside ExpansionPanel

I'm doing this on onChange of Checkbox

event.stopPropagation();
event.preventDefault();

but the ExpansionPanel keep expanding

can I stop this behaviour?

Also please remove next style .MuiExpansionPanelSummary-content-35 > :last-child {
/* padding-right: 32px; */
}

Done: #14828.

let's say I want to open expansion panel by clicking anywhere on the panel (which is the default behavior). But there is a button inside my expansion panel. When I click on that button, I don't want to open the expansion panel. How Can I achieve this? I tried to use z-index but I didn't help.

let's say I want to open expansion panel by clicking anywhere on the panel (which is the default behavior). But there is a button inside my expansion panel. When I click on that button, I don't want to open the expansion panel. How Can I achieve this? I tried to use z-index but I didn't help.

@Usama-Tahir it was already covered by this comment from @kamranayub.

I don't know how that solves my problem.

I would try to do the distinction between event.currentTarget and event.target.

@Usama-Tahir if you are talking about having a button into ExpansionPanelSummary which should not open the ExpansionPanel when it's being clicked, then the comment from @kamranayub totally solves your problem.

It's related to the event.stopPropagation() method.

const _handleButtonClick = event => {
  event.stopPropagation();
  ... Do your stuff after here.
}

<ExpansionPanel>
  <ExpansionPanelSummary>
    <Button onClick={_handleButtonClick}>Click me</Button>
  </ExpansionPanelSummary>
  ...
</ExpansionPanel>

This should avoid the ExpansionPanel to being opened when you click onto the Button.

Does that make sense to your issue?

yes, That solved my problem partially. Actually, I render expansion panels from an array. Initially, I was using expanded prop inside <ExpansionPanel /> to open it. So when I clicked on another expansion panel, it would close any other opened expansion panel as well. I can't figure out how to achieve this with the current solution you provided?

What do you guys think of this approach?

--- a/docs/src/pages/demos/expansion-panels/ControlledExpansionPanels.js
+++ b/docs/src/pages/demos/expansion-panels/ControlledExpansionPanels.js
@@ -1,8 +1,8 @@
 import React from 'react';
-import { makeStyles } from '@material-ui/core/styles';
+import { makeStyles, withStyles } from '@material-ui/core/styles';
 import ExpansionPanel from '@material-ui/core/ExpansionPanel';
 import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
-import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
+import MuiExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
 import Typography from '@material-ui/core/Typography';
 import ExpandMoreIcon from '@material-ui/icons/ExpandMore';

@@ -21,18 +21,28 @@ const useStyles = makeStyles(theme => ({
   },
 }));

+const ExpansionPanelSummary = withStyles({
+  root: {
+    cursor: 'default',
+  },
+})(MuiExpansionPanelSummary);
+
 function ControlledExpansionPanels() {
   const classes = useStyles();
   const [expanded, setExpanded] = React.useState(null);

-  const handleChange = panel => (event, isExpanded) => {
-    setExpanded(isExpanded ? panel : false);
+  const handleChange = panel => () => {
+    const isExpanded = expanded === panel;
+    setExpanded(isExpanded ? false : panel);
   };

   return (
     <div className={classes.root}>
-      <ExpansionPanel expanded={expanded === 'panel1'} onChange={handleChange('panel1')}>
+      <ExpansionPanel expanded={expanded === 'panel1'}>
         <ExpansionPanelSummary
+          IconButtonProps={{
+            onClick: handleChange('panel1'),
+          }}
           expandIcon={<ExpandMoreIcon />}
           aria-controls="panel1bh-content"
           id="panel1bh-header"
@@ -47,8 +57,11 @@ function ControlledExpansionPanels() {
           </Typography>
         </ExpansionPanelDetails>
       </ExpansionPanel>
-      <ExpansionPanel expanded={expanded === 'panel2'} onChange={handleChange('panel2')}>
+      <ExpansionPanel expanded={expanded === 'panel2'}>
         <ExpansionPanelSummary
+          IconButtonProps={{
+            onClick: handleChange('panel2'),
+          }}
           expandIcon={<ExpandMoreIcon />}
           aria-controls="panel2bh-content"
           id="panel2bh-header"
@@ -65,8 +78,11 @@ function ControlledExpansionPanels() {
           </Typography>
         </ExpansionPanelDetails>
       </ExpansionPanel>
-      <ExpansionPanel expanded={expanded === 'panel3'} onChange={handleChange('panel3')}>
+      <ExpansionPanel expanded={expanded === 'panel3'}>
         <ExpansionPanelSummary
+          IconButtonProps={{
+            onClick: handleChange('panel3'),
+          }}
           expandIcon={<ExpandMoreIcon />}
           aria-controls="panel3bh-content"
           id="panel3bh-header"
@@ -83,8 +99,11 @@ function ControlledExpansionPanels() {
           </Typography>
         </ExpansionPanelDetails>
       </ExpansionPanel>
-      <ExpansionPanel expanded={expanded === 'panel4'} onChange={handleChange('panel4')}>
+      <ExpansionPanel expanded={expanded === 'panel4'}>
         <ExpansionPanelSummary
+          IconButtonProps={{
+            onClick: handleChange('panel4'),
+          }}
           expandIcon={<ExpandMoreIcon />}
           aria-controls="panel4bh-content"
           id="panel4bh-header"
diff --git a/packages/material-ui/src/ExpansionPanelSummary/ExpansionPanelSummary.js b/packages/material-ui/src/ExpansionPanelSummary/ExpansionPanelSummary.js
index 388667124..4ea5a86f1 100644
--- a/packages/material-ui/src/ExpansionPanelSummary/ExpansionPanelSummary.js
+++ b/packages/material-ui/src/ExpansionPanelSummary/ExpansionPanelSummary.js
@@ -19,9 +19,7 @@ export const styles = theme => {
       minHeight: 8 * 6,
       transition: theme.transitions.create(['min-height', 'background-color'], transition),
       padding: '0 24px 0 24px',
-      '&:hover:not($disabled)': {
-        cursor: 'pointer',
-      },
+      cursor: 'pointer',
       '&$expanded': {
         minHeight: 64,
       },
@@ -30,6 +28,7 @@ export const styles = theme => {
       },
       '&$disabled': {
         opacity: 0.38,
+        cursor: 'default',
       },
     },
     /* Styles applied to the root element, children wrapper element and `IconButton` component if `expanded={true}`. */

https://codesandbox.io/s/jjy809l6ny

I think that it would make a great demo. Does anyone want to handle it?

So when I clicked on another expansion panel, it would close any other opened expansion panel as well.

Oh, in this case, maybe we should add another property to handle the style change and click behavior. It would make it even simpler. I don't know. At least we can simplify the point style override:

--- a/packages/material-ui/src/ExpansionPanelSummary/ExpansionPanelSummary.js
+++ b/packages/material-ui/src/ExpansionPanelSummary/ExpansionPanelSummary.js
@@ -19,9 +19,7 @@ export const styles = theme => {
       minHeight: 8 * 6,
       transition: theme.transitions.create(['min-height', 'background-color'], transition),
       padding: '0 24px 0 24px',
-      '&:hover:not($disabled)': {
-        cursor: 'pointer',
-      },
+      cursor: 'pointer',
       '&$expanded': {
         minHeight: 64,
       },
@@ -30,6 +28,7 @@ export const styles = theme => {
       },
       '&$disabled': {
         opacity: 0.38,
+        cursor: 'default',
       },
     },
     /* Styles applied to the root element, children wrapper element and `IconButton` component if `expanded={true}`. */

@0maxxam0 has funded $2.00 to this issue.


This issue is still a problem.
Expansion panel summary it's a button, hence it is intercepting all kind of click events. This is even worse on mobile platforms.
I think that the container should have a way to provide controls to the expansion panel, I think is something common.

@danielo515 What do you think of my proposed solution?

@oliviertassinari your proposals seems to be focused on solving a different problem.
My problem is that the ExpanionPanelSummary is a giant button. This makes impossible that any of it's children detect click events properly because his parent is "absorbing" them.

@danielo515 I believe we are talking about the same problem.

How would one make it where the first panel is open on default and then all children panels as they are clicked on would "open/close" by clicking only on the icon however not close the first panel? Any examples with a class based component? Thanks

@rgautier2003 It鈥檚 better to ask those kind of questions on stackoverflow

@Loktor's solution is great, but unfortunately since it was posted there has been a new warning added to material-ui warning that the clicks on the icon won't be seen by Firefox. Here's an alternative solution:

`const CustomExpansionPanel = ({children, className, initiallyExpanded = false}) => {
const [expanded, setExpanded] = useState(initiallyExpanded);

return (
    <ExpansionPanel
        expanded={expanded}
        className={className}
        onClick={(event) => {
            /* Allow only the <ExpandIcon> related to this Expansion panel to expand it. */
            if (event.target.parentElement
                && event.target.parentElement.parentElement
                && event.target.parentElement.parentElement.parentElement
                && event.target.parentElement.parentElement.parentElement.parentElement) {
                if (((event.target.parentElement.parentElement.parentElement.parentElement === event.currentTarget)
                    && (event.target.nodeName === 'svg')) ||
                    // eslint-disable-next-line max-len
                    ((event.target.parentElement.parentElement.parentElement.parentElement.parentElement === event.currentTarget)
                && (event.target.nodeName === 'path'))) {
                    setExpanded(!expanded);
                }
            }
        }}
    >
        { children }
    </ExpansionPanel>
);

};

@drericrobinson Could you explain the behavior you are looking for with this logic? I would hope we can simplify it. A significant amount of people have interacted with the issue, it seems to be an important concern.

https://github.com/mui-org/material-ui/issues/9427#issuecomment-350444133 already had a simple but potentially inaccessible solution

We can add a demo (without changing the ExpansionPanel implementation) but need to document how to improve a11y as much as possible: https://codesandbox.io/s/material-ui-expansionpanel-nested-action-u2bm6 should fix most of the a11y issues introduced by simply stopping click propagation of the nested actions.

@eps1lon This sounds like a great approach, it works with a checkbox too: https://codesandbox.io/s/material-ui-expansionpanel-nested-action-9pkk5. I'm all in for adding a demo for the "action inside panel summary" case.

Looking at the comment history, it seems that people also ask for an alternative where the expandIcon is the only element that can open or close the panel. It's related to https://github.com/mui-org/material-ui/issues/9427#issuecomment-475182230 but it would also need to handle keyboard interactions, need to check the a11y aspect too. Maybe we would need a prop this time.

So all that's missing is a reasonable use case for that (not just dummy text). If someone can work on a real example I'd be happy to review PR that adds a demo that incorporates some key aspects from https://codesandbox.io/s/material-ui-expansionpanel-nested-action-u2bm6:

  • click propagation is stopped in the nested action
  • focus propagation is stopped in the nested action (in react focus events bubble)
  • label each action with aria-label or aria-labelledby. Otherwise the label for nested actions will be included in the label of the parent button

Giving this one a hacktoberfest shot here: https://github.com/mui-org/material-ui/pull/17969 :P

You can change the material-ui source code in the ExpansionPanelSummary.js file

return React.createElement(ButtonBase, _extends({ focusRipple: false, disableRipple: true, disabled: disabled, component: "div", "aria-expanded": expanded, className: clsx(classes.root, className, disabled && classes.disabled, expanded && classes.expanded, focusedState && classes.focused), onFocusVisible: handleFocusVisible, onBlur: handleBlur, //onClick: handleChange, This makes the entire expansion panel clickable comment out ref: ref }, other), React.createElement("div", { className: clsx(classes.content, expanded && classes.expanded) }, children), expandIcon && React.createElement(IconButton, _extends({ disabled: disabled, className: clsx(classes.expandIcon, expanded && classes.expanded), edge: "end", component: "div", onClick: handleChange, //put the onClick event here so only expandIcon button expands panel tabIndex: -1, "aria-hidden": true }, IconButtonProps), expandIcon)); });

I used the method mentioned by @Loktor and it worked perfectly for me as my expansion panels are in separate components.

My only qualm is the fact that I'm getting this warning/error:
Capture

I've tested my app out in Firefox and it's working as expected, maybe misses the initial click at times but works after that. This'll be a mobile web app so it'll only be running on Chrome and Safari anyhow. Is there anyway I can get rid of this error? And is it fine for me to proceed with the method I'm using?

Below is the snippet of code I'm using; an attribute inside ExpansionPanelSummary:
expandIcon={<ExpandMoreIcon onClick={() => setPanel(!isPanelOpen)}

Was this page helpful?
0 / 5 - 0 ratings

Related issues

mb-copart picture mb-copart  路  3Comments

TimoRuetten picture TimoRuetten  路  3Comments

iamzhouyi picture iamzhouyi  路  3Comments

revskill10 picture revskill10  路  3Comments

reflog picture reflog  路  3Comments