Repository: mui/material-ui (TypeScript)
Date: 2019-05-13 12:47:24+00:00
Task type: Feature

Issue:
[Tabs] Support "auto" as "scrollable" value
When using `<Tabs scrollable>` with few tabs, the first tab seems not properly aligned to the left. I need the ability to auto-set the `scrollable` behavior based on the screen width and the number of tabs.

<!-- Checked checkbox should look like this: [x] -->
- [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

I would like tabs that are scrollable when there isn't enough width, but not scrollable otherwise. The `scrollable` prop should be automatically enabled based on whether the screen is large enough to display all the tabs or not. Therefore, setting `scrollable` to `true` or `false` doesn't work - I'd need an `auto` value.

I know I can use `withWidth` to do that in userland, but it's just such a common need that I believe it should be implemented in core.

## Current Behavior

`scrollable` can only be set to `true` or `false`, so the tabs always look bad:

- if I have `scrollable` but a very large screen, the first tab isn't properly aligned
- if I have `scrollable=false` but a narrow screen, I can't get to the last tab

## Context

I'm developing a component that can contain an arbitrary number of tabs - from a few to many. In the latter case, the `scrollable` option is a must. But when enabled in the first case, the first tab isn't properly aligned on the left:

![2018-08-01_1523](https://user-images.githubusercontent.com/99944/43524185-e59a7abe-959e-11e8-80fe-447ce20f0159.png)




Issue discussion:
@fzaninotto I'm personally using `scrollable={true} scrollButtons="off"` most of the time. Do you need to display the scroll buttons?
IMO It's not ideal on desktop screens to hide buttons, but I can live with that. Thanks for your answer.
> @fzaninotto I'm personally using `scrollable={true} scrollButtons="off"` most of the time. Do you need to display the scroll buttons?

@oliviertassinari I just set my app to the above and on desktop how to you scroll the tabs? I can click a tab that is clipped and it will scroll into view but how can I get to tabs that are completely off screen? 

I know both issues were closed but I think a lot of people would appreciate the enhancement. The UI just doesn't look right with no scroll button and whitespace for the first tab. Several people here noticed it right away. 
> how to you scroll the tabs? 

@MikeSuiter It depends on the platform, on Windows you have a scrollbar, on MacOs you can use two fingers in the trackpad.

The first issue was closed because it's pretty much the same pain point than here. So better group the discussion.
@oliviertassinari This screenshot is from Chrome 69 on Windows 10 and once a tab is completely off screen there is no way to get back to it. Should I have a scrollbar here? I tried click dragging and that doesn't work. 

![image](https://user-images.githubusercontent.com/5289509/45652223-8c375800-ba99-11e8-927f-1edb3ee496d1.png)

Here is the same screenshot with wider browser - you can see the Home tab here which you can't get back to when browser is narrower. 

![image](https://user-images.githubusercontent.com/5289509/45652462-4dee6880-ba9a-11e8-8ab6-5df6064c3f0c.png)

This is using these props:
`<Tabs value={selected} onChange={onTabSelect} scrollable scrollButtons="off">`

Thanks for reopening this as I think others will benefit from it and I agree consolidating my request into this one. 
IMO this is still an issue from a UX perspective, since without any affordance indicating the tabs are scrollable, users have no reliable way to know there are more tabs off screen. On the other hand, the additional space mentioned by the OP required with scroll="auto" makes desktop UIs look broken. Could this issue be reopened so it can be revisited?

In the meantime, here's a hacky workaround I came up with to hide/show the scroll buttons when the tab width exceeds that of the container (on window resize and when adding tabs): 
https://codesandbox.io/s/q34yxjkmwj

Maybe this could be added to the Tabs impl, controlled by a prop like scrollHide="auto|on|off", for ex?
@gino-m This is a good workaround, thanks!
Not to be nitpicking nor criticizing but why the 'scrollable'=auto doesn't detect and compare the widths of scroll container and its parent width based on which the buttons would appear? And to take it further, based on the x scroll value to display hide the scroll buttons.
I just don't think that having either **a blank, padded space** or an **actual button** or **no scroll functionality** is a good UI. Thanks for the great library btw.

Solution patch:
diff --git a/packages/material-ui/src/Tabs/TabScrollButton.js b/packages/material-ui/src/Tabs/TabScrollButton.js
--- a/packages/material-ui/src/Tabs/TabScrollButton.js
+++ b/packages/material-ui/src/Tabs/TabScrollButton.js
@@ -12,7 +12,7 @@ export const styles = {
   /* Styles applied to the root element. */
   root: {
     color: 'inherit',
-    width: 56,
+    width: 40,
     flexShrink: 0,
   },
 };
@@ -39,7 +39,11 @@ const TabScrollButton = React.forwardRef(function TabScrollButton(props, ref) {
       tabIndex={null}
       {...other}
     >
-      {direction === 'left' ? <KeyboardArrowLeft /> : <KeyboardArrowRight />}
+      {direction === 'left' ? (
+        <KeyboardArrowLeft fontSize="small" />
+      ) : (
+        <KeyboardArrowRight fontSize="small" />
+      )}
     </ButtonBase>
   );
 });
diff --git a/packages/material-ui/src/Tabs/Tabs.d.ts b/packages/material-ui/src/Tabs/Tabs.d.ts
--- a/packages/material-ui/src/Tabs/Tabs.d.ts
+++ b/packages/material-ui/src/Tabs/Tabs.d.ts
@@ -11,7 +11,7 @@ declare const Tabs: OverridableComponent<{
     indicatorColor?: 'secondary' | 'primary' | string;
     onChange?: (event: React.ChangeEvent<{}>, value: any) => void;
     ScrollButtonComponent?: React.ElementType;
-    scrollButtons?: 'auto' | 'on' | 'off';
+    scrollButtons?: 'auto' | 'desktop' | 'on' | 'off';
     TabIndicatorProps?: Partial<TabIndicatorProps>;
     textColor?: 'secondary' | 'primary' | 'inherit' | string;
     value: any;
@@ -30,7 +30,7 @@ export type TabsClassKey =
   | 'scrollable'
   | 'centered'
   | 'scrollButtons'
-  | 'scrollButtonsAuto'
+  | 'scrollButtonsDesktop'
   | 'indicator';
 
 export interface TabsActions {
diff --git a/packages/material-ui/src/Tabs/Tabs.js b/packages/material-ui/src/Tabs/Tabs.js
--- a/packages/material-ui/src/Tabs/Tabs.js
+++ b/packages/material-ui/src/Tabs/Tabs.js
@@ -47,8 +47,8 @@ export const styles = theme => ({
   },
   /* Styles applied to the `ScrollButtonComponent` component. */
   scrollButtons: {},
-  /* Styles applied to the `ScrollButtonComponent` component if `scrollButtons="auto"`. */
-  scrollButtonsAuto: {
+  /* Styles applied to the `ScrollButtonComponent` component if `scrollButtons="auto"` or scrollButtons="desktop"`. */
+  scrollButtonsDesktop: {
     [theme.breakpoints.down('xs')]: {
       display: 'none',
     },
@@ -114,32 +114,38 @@ class Tabs extends React.Component {
 
   getConditionalElements = () => {
     const { classes, ScrollButtonComponent, scrollButtons, theme, variant } = this.props;
+    const { showLeftScroll, showRightScroll } = this.state;
     const conditionalElements = {};
     const scrollable = variant === 'scrollable';
     conditionalElements.scrollbarSizeListener = scrollable ? (
       <ScrollbarSize onChange={this.handleScrollbarSizeChange} />
     ) : null;
 
-    const showScrollButtons = scrollable && (scrollButtons === 'auto' || scrollButtons === 'on');
+    const scrollButtonsActive = showLeftScroll || showRightScroll;
+    const showScrollButtons =
+      scrollable &&
+      ((scrollButtons === 'auto' && scrollButtonsActive) ||
+        scrollButtons === 'desktop' ||
+        scrollButtons === 'on');
 
     conditionalElements.scrollButtonLeft = showScrollButtons ? (
       <ScrollButtonComponent
-        direction={theme && theme.direction === 'rtl' ? 'right' : 'left'}
+        direction={theme.direction === 'rtl' ? 'right' : 'left'}
         onClick={this.handleLeftScrollClick}
-        visible={this.state.showLeftScroll}
+        visible={showLeftScroll}
         className={clsx(classes.scrollButtons, {
-          [classes.scrollButtonsAuto]: scrollButtons === 'auto',
+          [classes.scrollButtonsDesktop]: scrollButtons !== 'on',
         })}
       />
     ) : null;
 
     conditionalElements.scrollButtonRight = showScrollButtons ? (
       <ScrollButtonComponent
-        direction={theme && theme.direction === 'rtl' ? 'left' : 'right'}
+        direction={theme.direction === 'rtl' ? 'left' : 'right'}
         onClick={this.handleRightScrollClick}
-        visible={this.state.showRightScroll}
+        visible={showRightScroll}
         className={clsx(classes.scrollButtons, {
-          [classes.scrollButtonsAuto]: scrollButtons === 'auto',
+          [classes.scrollButtonsDesktop]: scrollButtons !== 'on',
         })}
       />
     ) : null;
@@ -453,11 +459,13 @@ Tabs.propTypes = {
   ScrollButtonComponent: PropTypes.elementType,
   /**
    * Determine behavior of scroll buttons when tabs are set to scroll
-   * `auto` will only present them on medium and larger viewports
-   * `on` will always present them
-   * `off` will never present them
+   *
+   * - `auto` will only present them when not all the items are visible.
+   * - `desktop` will only present them on medium and larger viewports.
+   * - `on` will always present them.
+   * - `off` will never present them.
    */
-  scrollButtons: PropTypes.oneOf(['auto', 'on', 'off']),
+  scrollButtons: PropTypes.oneOf(['auto', 'desktop', 'on', 'off']),
   /**
    * Properties applied to the `TabIndicator` element.
    */
@@ -477,6 +485,7 @@ Tabs.propTypes = {
   value: PropTypes.any,
   /**
    *  Determines additional display behavior of the tabs:
+   *
    *  - `scrollable` will invoke scrolling properties and allow for horizontally
    *  scrolling (or swiping) of the tab bar.
    *  -`fullWidth` will make the tabs grow to use all the available space,
diff --git a/pages/api/tabs.md b/pages/api/tabs.md
--- a/pages/api/tabs.md
+++ b/pages/api/tabs.md
@@ -26,11 +26,11 @@ import Tabs from '@material-ui/core/Tabs';
 | <span class="prop-name">indicatorColor</span> | <span class="prop-type">enum:&nbsp;'secondary'&nbsp;&#124;<br>&nbsp;'primary'<br></span> | <span class="prop-default">'secondary'</span> | Determines the color of the indicator. |
 | <span class="prop-name">onChange</span> | <span class="prop-type">func</span> |  | Callback fired when the value changes.<br><br>**Signature:**<br>`function(event: object, value: any) => void`<br>*event:* The event source of the callback<br>*value:* We default to the index of the child (number) |
 | <span class="prop-name">ScrollButtonComponent</span> | <span class="prop-type">elementType</span> | <span class="prop-default">TabScrollButton</span> | The component used to render the scroll buttons. |
-| <span class="prop-name">scrollButtons</span> | <span class="prop-type">enum:&nbsp;'auto'&nbsp;&#124;<br>&nbsp;'on'&nbsp;&#124;<br>&nbsp;'off'<br></span> | <span class="prop-default">'auto'</span> | Determine behavior of scroll buttons when tabs are set to scroll `auto` will only present them on medium and larger viewports `on` will always present them `off` will never present them |
+| <span class="prop-name">scrollButtons</span> | <span class="prop-type">enum:&nbsp;'auto'&nbsp;&#124;<br>&nbsp;'desktop'&nbsp;&#124;<br>&nbsp;'on'&nbsp;&#124;<br>&nbsp;'off'<br></span> | <span class="prop-default">'auto'</span> | Determine behavior of scroll buttons when tabs are set to scroll<br>- `auto` will only present them when not all the items are visible. - `desktop` will only present them on medium and larger viewports. - `on` will always present them. - `off` will never present them. |
 | <span class="prop-name">TabIndicatorProps</span> | <span class="prop-type">object</span> |  | Properties applied to the `TabIndicator` element. |
 | <span class="prop-name">textColor</span> | <span class="prop-type">enum:&nbsp;'secondary'&nbsp;&#124;<br>&nbsp;'primary'&nbsp;&#124;<br>&nbsp;'inherit'<br></span> | <span class="prop-default">'inherit'</span> | Determines the color of the `Tab`. |
 | <span class="prop-name">value</span> | <span class="prop-type">any</span> |  | The value of the currently selected `Tab`. If you don't want any selected `Tab`, you can set this property to `false`. |
-| <span class="prop-name">variant</span> | <span class="prop-type">enum:&nbsp;'standard'&nbsp;&#124;<br>&nbsp;'scrollable'&nbsp;&#124;<br>&nbsp;'fullWidth'<br></span> | <span class="prop-default">'standard'</span> | Determines additional display behavior of the tabs:  - `scrollable` will invoke scrolling properties and allow for horizontally  scrolling (or swiping) of the tab bar.  -`fullWidth` will make the tabs grow to use all the available space,  which should be used for small views, like on mobile.  - `standard` will render the default state. |
+| <span class="prop-name">variant</span> | <span class="prop-type">enum:&nbsp;'standard'&nbsp;&#124;<br>&nbsp;'scrollable'&nbsp;&#124;<br>&nbsp;'fullWidth'<br></span> | <span class="prop-default">'standard'</span> | Determines additional display behavior of the tabs:<br> - `scrollable` will invoke scrolling properties and allow for horizontally  scrolling (or swiping) of the tab bar.  -`fullWidth` will make the tabs grow to use all the available space,  which should be used for small views, like on mobile.  - `standard` will render the default state. |
 
 The `ref` is forwarded to the root element.
 
@@ -51,7 +51,7 @@ This property accepts the following keys:
 | <span class="prop-name">fixed</span> | Styles applied to the tablist element if `!variant="scrollable"`.
 | <span class="prop-name">scrollable</span> | Styles applied to the tablist element if `variant="scrollable"`.
 | <span class="prop-name">scrollButtons</span> | Styles applied to the `ScrollButtonComponent` component.
-| <span class="prop-name">scrollButtonsAuto</span> | Styles applied to the `ScrollButtonComponent` component if `scrollButtons="auto"`.
+| <span class="prop-name">scrollButtonsDesktop</span> | Styles applied to the `ScrollButtonComponent` component if `scrollButtons="auto"` or scrollButtons="desktop"`.
 | <span class="prop-name">indicator</span> | Styles applied to the `TabIndicator` component.
 
 Have a look at the [overriding styles with classes](/customization/components/#overriding-styles-with-classes) section


Test patch:
diff --git a/packages/material-ui/src/Tabs/Tabs.test.js b/packages/material-ui/src/Tabs/Tabs.test.js
--- a/packages/material-ui/src/Tabs/Tabs.test.js
+++ b/packages/material-ui/src/Tabs/Tabs.test.js
@@ -398,14 +398,16 @@ describe('<Tabs />', () => {
 
     it('should render scroll buttons automatically', () => {
       const wrapper = mount(
-        <Tabs width="md" onChange={noop} value={0} variant="scrollable" scrollButtons="auto">
+        <Tabs width="md" onChange={noop} value={0} variant="scrollable" scrollButtons="desktop">
           <Tab />
           <Tab />
         </Tabs>,
       );
       assert.strictEqual(wrapper.find(TabScrollButton).length, 2);
       assert.strictEqual(
-        wrapper.find(TabScrollButton).everyWhere(node => node.hasClass(classes.scrollButtonsAuto)),
+        wrapper
+          .find(TabScrollButton)
+          .everyWhere(node => node.hasClass(classes.scrollButtonsDesktop)),
         true,
       );
     });