Repository: mui/material-ui (TypeScript)
Date: 2018-09-14 16:39:42+00:00
Task type: Feature

Issue:
[TableSortLabel] Remove sort icon when not active
Table sort labels reserve space for the sort icon even when the column isn't the active sort. This prevents layout changes when clicking on a column for sorting, but this also uses horizontal space. I think the icon should be hidden rather than of 0% opacity.

- [x] This is a v1.x issue. <!-- (v0.x is no longer maintained) -->
- [x] I have searched the [issues](https://github.com/mui-org/material-ui/issues) of this repository and believe that this is not a duplicate.

## Expected Behavior

When a table header is not active for sort, the arrow is hidden (with `display: 'none'), i.e. it takes no horizontal space

## Current Behavior

When a table header is not active for sort, the arrow is displayed, but with 0% opacity. If this actually hides the arrow, it also uses horizontal space. 

## Examples

Material Design doesn't specify that the space for the arrow should be reserved. I understand that it helps for a nicer animation (by transitioning the opacity), but the effect on horizontal space it too high a price.

![image](https://user-images.githubusercontent.com/99944/44260187-f7300d80-a213-11e8-95f5-87f5ff9d1993.png)


## Context

Especially for number columns, where the header is often larger than the content, horizontal space is sparse. Imagine a table with many columns, each containing only numbers. the spacing between each column (due to the space reserved for the arrow if active) makes the table too large to fit in one screen without scrolling



Issue discussion:
Note that this is achievable in userland by passing a custom `classes` to the `<TableSortLabel>`:

```js
const styles = {
    icon: {
        display: 'none',
    },
    active: {
        '& $icon': {
            display: 'inline',
        },
    },
};
```

I just think it should be the default.
And another 2c: I understand that having the space for the arrow reserved avoids the table layout to change when the user clicks on a table header. But in many cases (when a table only displays a subset of the content for performance reason), reordering the table will load new content with, and this will modify the table layout anyway. 
@fzaninotto Thanks for opening this issue. It's definitely challenging the current tradeoff. I think that you put it very well in this sentence:

> This prevents layout changes when clicking on a column for sorting, but this also uses horizontal space.

Now, if we change the default, we will have people opening issue about the table layout jumps. In this case, I think that we can only move the discussing forward by trying it out or with a benchmark (looking at the other implementations) as a proxy for understanding what works best. So, let's benchmark:
- [angular material 2](https://material.angular.io/components/table/overview#sorting): reserved space
- [vuetify](https://vuetifyjs.com/en/components/data-tables#usage): reserved space
- [devexpress](https://devexpress.github.io/devextreme-reactive/react/grid/demos/featured/tree-data/): no reserved space
- more idea?

One thing I have learned during this benchmark is how people can leverage `table-layout: fixed;` CSS to get rid of the layout change and to make large tables render faster 🚀 .

I have added the `waiting for users upvotes` tag. Looking at your demo, it seems you would take advantage of rendering less data or making the table overflow to create some space between the columns. Please upvote this issue if you are looking for such change. We will prioritize our effort based on the number of upvotes.
One more thought. We could add a property to enable this behavior. I think that it's generic enough to be considered. What do you think about it?
I agree with the property, makes all sides happy.
>  it seems you would take advantage of rendering less data 

I don't make the decision here, the customer needs all those columns displayed in one screen, without horizontal scroll. 

> We could add a property to enable this behavior.

That'd be great.

Also, why did you close the issue if it's waiting for upvotes?
> Also, why did you close the issue if it's waiting for upvotes?

@fzaninotto Because I believe it's not a direction we should be taking, better be upfront about it than to pretend we might accept a pull request. Yet, I want the community to weight their voice. From my experience, the open / close status of an issue doesn't change much the bottom line.
https://github.com/mui-org/material-ui/issues?q=is%3Aissue+label%3A%22waiting+for+users+upvotes%22+is%3Aclosed+sort%3Areactions-%2B1-desc

> That'd be great.

@afilp Alright, let's add a property for it 👍 
Now it's not *waiting for users upvotes* anymore but it's still closed... What does that mean?
My bad.
Hello @fzaninotto I am new to material-ui and trying to contribute.  I tried to pass in the custom style as you have mentioned by passing in a custom style

```
const styles = {
    icon: {
        display: 'none',
    },
    active: {
        '& $icon': {
            display: 'inline',
        },
    },
};

```
I am trying to do this in the examples and I'm using the EnhancedTable.js example.  Maybe I'm trying it wrong, however what I'm trying to do is the following

```
                <TableSortLabel
                    active={orderBy === row.id}
                    style={styles}
                    direction={order}
                    onClick={this.createSortHandler(row.id)}
                  >
```
This does not work as expected since I'm passing in classes actually.  When I try to pass in the same using classes also it does not work, since classes are expecting class names as strings.  Since I'm new to this its very likely that I'm doing something wrong.  Any help.

Solution patch:
diff --git a/packages/material-ui/src/TableSortLabel/TableSortLabel.d.ts b/packages/material-ui/src/TableSortLabel/TableSortLabel.d.ts
--- a/packages/material-ui/src/TableSortLabel/TableSortLabel.d.ts
+++ b/packages/material-ui/src/TableSortLabel/TableSortLabel.d.ts
@@ -7,6 +7,7 @@ export interface TableSortLabelProps
   extends StandardProps<ButtonBaseProps, TableSortLabelClassKey> {
   active?: boolean;
   direction?: 'asc' | 'desc';
+  hideSortIcon?: boolean;
   IconComponent?: React.ComponentType<SvgIconProps>;
 }
 
diff --git a/packages/material-ui/src/TableSortLabel/TableSortLabel.js b/packages/material-ui/src/TableSortLabel/TableSortLabel.js
--- a/packages/material-ui/src/TableSortLabel/TableSortLabel.js
+++ b/packages/material-ui/src/TableSortLabel/TableSortLabel.js
@@ -56,7 +56,16 @@ export const styles = theme => ({
  * A button based label for placing inside `TableCell` for column sorting.
  */
 function TableSortLabel(props) {
-  const { active, classes, className, children, direction, IconComponent, ...other } = props;
+  const {
+    active,
+    children,
+    classes,
+    className,
+    direction,
+    hideSortIcon,
+    IconComponent,
+    ...other
+  } = props;
 
   return (
     <ButtonBase
@@ -66,9 +75,11 @@ function TableSortLabel(props) {
       {...other}
     >
       {children}
-      <IconComponent
-        className={classNames(classes.icon, classes[`iconDirection${capitalize(direction)}`])}
-      />
+      {hideSortIcon && !active ? null : (
+        <IconComponent
+          className={classNames(classes.icon, classes[`iconDirection${capitalize(direction)}`])}
+        />
+      )}
     </ButtonBase>
   );
 }
@@ -95,6 +106,10 @@ TableSortLabel.propTypes = {
    * The current sort direction.
    */
   direction: PropTypes.oneOf(['asc', 'desc']),
+  /**
+   * Hide sort icon when active is false.
+   */
+  hideSortIcon: PropTypes.bool,
   /**
    * Sort icon to use.
    */
@@ -104,6 +119,7 @@ TableSortLabel.propTypes = {
 TableSortLabel.defaultProps = {
   active: false,
   direction: 'desc',
+  hideSortIcon: false,
   IconComponent: ArrowDownwardIcon,
 };
 
diff --git a/pages/api/table-sort-label.md b/pages/api/table-sort-label.md
--- a/pages/api/table-sort-label.md
+++ b/pages/api/table-sort-label.md
@@ -23,6 +23,7 @@ A button based label for placing inside `TableCell` for column sorting.
 | <span class="prop-name">children</span> | <span class="prop-type">node |   | Label contents, the arrow will be appended automatically. |
 | <span class="prop-name">classes</span> | <span class="prop-type">object |   | Override or extend the styles applied to the component. See [CSS API](#css-api) below for more details. |
 | <span class="prop-name">direction</span> | <span class="prop-type">enum:&nbsp;'asc'&nbsp;&#124;<br>&nbsp;'desc'<br> | <span class="prop-default">'desc'</span> | The current sort direction. |
+| <span class="prop-name">hideSortIcon</span> | <span class="prop-type">bool | <span class="prop-default">false</span> | Hide sort icon when active is false. |
 | <span class="prop-name">IconComponent</span> | <span class="prop-type">func | <span class="prop-default">ArrowDownwardIcon</span> | Sort icon to use. |
 
 Any other properties supplied will be spread to the root element ([ButtonBase](/api/button-base)).


Test patch:
diff --git a/packages/material-ui/src/TableSortLabel/TableSortLabel.test.js b/packages/material-ui/src/TableSortLabel/TableSortLabel.test.js
--- a/packages/material-ui/src/TableSortLabel/TableSortLabel.test.js
+++ b/packages/material-ui/src/TableSortLabel/TableSortLabel.test.js
@@ -65,13 +65,32 @@ describe('<TableSortLabel />', () => {
     });
 
     it('should accept a custom icon for the sort icon', () => {
-      shallow = createShallow({ untilSelector: 'Sort' });
       const wrapper = mount(<TableSortLabel IconComponent={Sort} />);
       assert.strictEqual(wrapper.props().IconComponent, Sort);
       assert.strictEqual(wrapper.find(Sort).length, 1);
     });
   });
 
+  describe('prop: hideSortIcon', () => {
+    it('can hide icon when not active', () => {
+      const wrapper = shallow(<TableSortLabel active={false} hideSortIcon />);
+      const iconChildren = wrapper.find(`.${classes.icon}`).first();
+      assert.strictEqual(iconChildren.length, 0);
+    });
+
+    it('does not hide icon by default when not active', () => {
+      const wrapper = shallow(<TableSortLabel active={false} />);
+      const iconChildren = wrapper.find(`.${classes.icon}`).first();
+      assert.strictEqual(iconChildren.length, 1);
+    });
+
+    it('does not hide icon when active', () => {
+      const wrapper = shallow(<TableSortLabel active hideSortIcon />);
+      const iconChildren = wrapper.find(`.${classes.icon}`).first();
+      assert.strictEqual(iconChildren.length, 1);
+    });
+  });
+
   describe('mount', () => {
     it('should mount without error', () => {
       mount(<TableSortLabel />);