mui / material-ui

Material UI: Comprehensive React component library that implements Google's Material Design. Free forever.
https://mui.com/material-ui/
MIT License
93.42k stars 32.15k forks source link

Tabs indicator custom width and position #10465

Closed jody-zeitler closed 6 years ago

jody-zeitler commented 6 years ago

Expected Behavior

I'd like the ability to transform the indicator style (namely width and left) within the Tabs component based on the default value. This would be in the form of an optional function prop that looks like this:

function transformIndicatorStyle({left, width}) {
    return {
        left: left + 6,
        width: width - 12,
    };
}

This function would be invoked either before setting the indicatorStyle state value, or before the style is passed to the TabIndicator child during render.

Current Behavior

The Tabs indicator is locked to the left and width of the currently selected tab's getBoundingClientRect.

Steps to Reproduce (for bugs)

This sandbox demonstrates how far I'm able to go for indicator customization. I've modified the height and margins of the indicator using class overrides, but I'm not able to access or modify the width because it's a function of the selected tab size. Notice the overhang on the right side of the selection.

https://codesandbox.io/s/6j663n24zw

Context

We are creating a component that wraps AppBar and Tabs. It has a segmented option which places the indicator underneath the tab (z-axis) at full height, similar to what was done for #10123, with some margin around the sides. Style overrides allow us to mostly accomplish what we want, but we're unable to find a way to modify the indicator width so that it plays nicely with the margin.

segmented_tabs

Your Environment

Tech Version
Material-UI 1.0.0-beta.34
React 16.2.0
browser Chrome 64
oliviertassinari commented 6 years ago

I'd like the ability to transform the indicator style (namely width and left) within the Tabs component based on the default value. This would be in the form of an optional function prop that looks like this:

@jody-zeitler The indicator is rendered two different ways. One that is during for the server-sider rendering and one that is absolute for transitioning the active tab. Fixing both requirement might be challenging.

jody-zeitler commented 6 years ago

I'll look into the server-side aspect of it and see what might work.

jody-zeitler commented 6 years ago

I've realized I can accomplish what I want by adding left/right margin to the tab itself, so the bounding box is calculated appropriately. Updated sandbox:

capture d ecran 2018-02-28 a 14 46 59

import classnames from "classnames";
import MuiAppBar from "material-ui/AppBar";
import withStyles from "material-ui/styles/withStyles";
import MuiTabs from "material-ui/Tabs/Tabs";
import PropTypes from "prop-types";
import React from "react";

const SEGMENT_MARGIN = 6;

/**
 * Tabs are a composite of a static AppBar and the base Tabs component.
 */
const Tabs = props => {
  const { children, classes, light, segmented, ...otherProps } = props;

  const conditionalClasses = classnames({
    [classes.light]: light,
    [classes.segmented]: segmented
  });
  const appBarClassName = classnames(classes.appBar, conditionalClasses);
  const indicatorClassName = classnames(classes.indicator, conditionalClasses);
  const tabClassName = classnames(classes.tab, conditionalClasses);

  return (
    <MuiAppBar className={appBarClassName} position="static">
      <MuiTabs indicatorClassName={indicatorClassName} {...otherProps}>
        {React.Children.map(children, child => {
          return React.cloneElement(child, {
            "aria-label": child.props["aria-label"] || child.props.label,
            className: classnames(tabClassName, child.props.className)
          });
        })}
      </MuiTabs>
    </MuiAppBar>
  );
};

Tabs.propTypes = {
  /** set of Tab elements */
  children: PropTypes.node,
  /** class overrides */
  classes: PropTypes.shape({
    appBar: PropTypes.string,
    indicator: PropTypes.string
  }),
  /** use light theme colors */
  light: PropTypes.bool,
  /** use segmented selection style instead of underline */
  segmented: PropTypes.bool
};

Tabs.defaultProps = {};

const styles = ({ palette }) => ({
  appBar: {
    backgroundColor: palette.primary.main,
    color: palette.primary.contrastText,
    "&$light": {
      backgroundColor: palette.grey[100],
      color: palette.getContrastText(palette.grey[100]),
      "&$segmented": {
        backgroundColor: palette.grey[200],
        color: palette.getContrastText(palette.grey[200])
      }
    }
  },
  indicator: {
    height: 4,
    backgroundColor: palette.primary.dark,
    "&$light": {
      height: 2,
      backgroundColor: palette.primary.main,
      "&$segmented": {
        backgroundColor: palette.background.paper
      }
    },
    "&$segmented": {
      height: `calc(100% - ${SEGMENT_MARGIN * 2}px)`,
      bottom: SEGMENT_MARGIN,
      borderRadius: 4,
      "&$light": {
        height: `calc(100% - ${SEGMENT_MARGIN * 2}px)`
      }
    }
  },
  light: {},
  segmented: {},
  tab: {
    margin: `0 ${SEGMENT_MARGIN}px`,
    "&$segmented": {
      zIndex: 1
    }
  }
});

export default withStyles(styles)(Tabs);

https://codesandbox.io/s/9z9z4rkjny

wufeng87 commented 6 years ago

@jody-zeitler where is the document about your custom code? I want to custom the width and left value of the red underline line too. Thanks a lot.

jody-zeitler commented 6 years ago

@wufeng87 width and left are calculated based on the bounding box of the selected tab. I couldn't find a good way to alter them directly so I applied a margin to the tab to achieve the look that I was aiming for. I was able to apply bottom and height directly to the indicator as those properties are not overridden by inline styles.

wufeng87 commented 6 years ago

@jody-zeitler thanks. I moved to v1.0.0-beta, hope I could find a way to custom the indicator width.

oliviertassinari commented 6 years ago

We now demonstrate a tab customization example in the documentation: https://material-ui.com/demos/tabs/#customized-tabs.

HiranmayaGundu commented 5 years ago

@wufeng87 Were you able to find a way to access the default left value of the selected tab? I'd like to shrink the TabIndicator and centre it using the left value, but I don't see any way to access the left and width values as suggested in the issue.

oliviertassinari commented 5 years ago

@HiranmayaGundu Do you have a visual example? Maybe I can help.

HiranmayaGundu commented 5 years ago

@oliviertassinari This is what I was aiming for tabs

oliviertassinari commented 5 years ago

@HiranmayaGundu Ok, can you add this example in the documentation? 😄 https://codesandbox.io/s/xl526z333q alongside this demo? https://material-ui.com/demos/tabs/#customized-tabs

import React from "react";
import PropTypes from "prop-types";
import { withStyles } from "@material-ui/core/styles";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import Typography from "@material-ui/core/Typography";

const styles = theme => ({
  root: {
    backgroundColor: "#2e1534"
  },
  typography: {
    padding: theme.spacing.unit * 3
  }
});

const StyledTabs = withStyles(theme => ({
  indicator: {
    display: "flex",
    justifyContent: "center",
    backgroundColor: "transparent",
    "& > div": {
      maxWidth: 40,
      width: "100%",
      backgroundColor: "#635ee7"
    }
  }
}))(props => <Tabs {...props} TabIndicatorProps={{ children: <div /> }} />);

const StyledTab = withStyles(theme => ({
  root: {
    textTransform: "initial",
    color: "#9e88a2",
    fontWeight: theme.typography.fontWeightLight,
    fontSize: theme.typography.pxToRem(15),
    marginRight: theme.spacing.unit * 1,
    "&:hover": {
      color: "#fff"
    },
    "&$selected": {
      color: "#fff"
    },
    "&:focus": {
      color: "#fff"
    }
  },
  selected: {}
}))(props => <Tab disableRipple {...props} />);

class CustomizedTabs extends React.Component {
  state = {
    value: 0
  };

  handleChange = (event, value) => {
    this.setState({ value });
  };

  render() {
    const { classes } = this.props;
    const { value } = this.state;

    return (
      <div className={classes.root}>
        <StyledTabs value={value} onChange={this.handleChange}>
          <StyledTab label="Workflows" />
          <StyledTab label="Datasets" />
          <StyledTab label="Connections" />
        </StyledTabs>
        <Typography className={classes.typography} />
      </div>
    );
  }
}

CustomizedTabs.propTypes = {
  classes: PropTypes.object.isRequired
};

export default withStyles(styles)(CustomizedTabs);

feb-14-2019 09-56-08

Thanks!

HiranmayaGundu commented 5 years ago

@oliviertassinari Thanks for the prompt reply! I'll add it to the docs.

Was this intended behaviour for TabIndicatorProps? My understanding was that it was another option to style the Indicator, other than using the override.

oliviertassinari commented 5 years ago

@HiranmayaGundu I don't understand your point regarding the TabIndicatorProps property.

joshwooding commented 5 years ago

@oliviertassinari I think they were asking whether *Props properties being used to override children is intended behaviour

oliviertassinari commented 5 years ago

Yes, the indicator doesn't have any children we provide one.

trusktr commented 5 years ago

How exactly do we style the indicator? TypeScript tells me I am not allowed to pass a indicator property into the classes prop of Tab.

trusktr commented 5 years ago

Oh, I see, indicator must be supplied to the Tabs component, not to each Tab. It may be confusing at first, but makes sense that the indicator is not actually inside the tabs, just one underneath all tabs.

fahadhussain2 commented 4 years ago

image

I want to customize the tab bar something like this with curve borders. Please see the attached screenshot. Can anyone help me out please