Categories
Material UI

Material UI — Virtualized Lists and Tables

Material UI is a Material Design library made for React.

It’s a set of React components that have Material Design styles.

In this article, we’ll look at how to add lists with pinned headings, virtualized lists, and tables with Material UI.

Pinned Subheader List

We can use CSS to make the subheader pinned to the top of the screen until it’s pushed off by the next subheader.

For example, we can write:

import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText";
import ListSubheader from "@material-ui/core/ListSubheader";

const useStyles = makeStyles(theme => ({
  root: {
    width: "100%",
    maxWidth: 360,
    backgroundColor: theme.palette.background.paper,
    position: "relative",
    overflow: "auto",
    maxHeight: 300
  },
  listSection: {
    backgroundColor: "inherit"
  },
  ul: {
    backgroundColor: "inherit",
    padding: 0
  }
}));

export default function App() {
  const classes = useStyles();

  return (
    <List className={classes.root} subheader={<li />}>
      {Array(10)
        .fill()
        .map((s, sectionId) => (
          <li key={`section-${sectionId}`} className={classes.listSection}>
            <ul className={classes.ul}>
              <ListSubheader>{`I'm sticky ${sectionId}`}</ListSubheader>
              {[0, 1, 2].map(item => (
                <ListItem key={`item-${sectionId}-${item}`}>
                  <ListItemText primary={`Item ${item}`} />
                </ListItem>
              ))}
            </ul>
          </li>
        ))}
    </List>
  );
}

We have the makeStyles function to set the position of the root to relative .

Also, we set a maxHeight so that it will scroll when the content overflows the list.

This way, we set the subheader to stay pinned.

Inset List

We can make list items that have no icon with text that aligns with the ones with icons.

For example, we can write:

import React from "react";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import StarIcon from "@material-ui/icons/Star";

export default function App() {
  return (
    <List aria-label="contacts">
      <ListItem button>
        <ListItemIcon>
          <StarIcon />
        </ListItemIcon>
        <ListItemText primary="james smith" />
      </ListItem>
      <ListItem button>
        <ListItemText inset primary="mary jane" />
      </ListItem>
    </List>
  );
}

We added the inset prop to make the text in the 2nd list item aligns with the text on the first.

Virtualized List

If we have a list with lots of items, then we’ve to make a virtualized list.

To do that, we can write:

import React from "react";
import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText";
import { FixedSizeList } from "react-window";

function renderRow(props) {
  const { index, style } = props;

  return (
    <ListItem button style={style} key={index}>
      <ListItemText primary={`Item ${index + 1}`} />
    </ListItem>
  );
}

export default function App() {
  return (
    <FixedSizeList height={300} width={300} itemSize={40} itemCount={300}>
      {renderRow}
    </FixedSizeList>
  );
}

to create a virtualized list with react-window.

We have the renderRow to render a table row.

And we used the FixedSizedList instead of a regular List .

itemCount sets the item count.

itemSize has the size of an item.

The height and width are the height and width of the list.

Table

Material UI comes with a table component to let us display data in a table.

To create a simple one, we can write the following:

import React from "react";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Paper from "@material-ui/core/Paper";

function createData(firstName, lastName, age) {
  return { firstName, lastName, age };
}

const rows = [
  createData("james", "smith", 20),
  createData("mary", "jones", 30),
  createData("may", "wong", 30)
];

export default function App() {
  return (
    <TableContainer component={Paper}>
      <Table>
        <TableHead>
          <TableRow>
            <TableCell>first name</TableCell>
            <TableCell>last name</TableCell>
            <TableCell align="right">age</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {rows.map(row => (
            <TableRow key={row.name}>
              <TableCell component="th" scope="row">
                {row.firstName}
              </TableCell>
              <TableCell>{row.lastName}</TableCell>
              <TableCell align="right">{row.age}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
}

We created a table with the TableContainer , Table , TableHead , TableRow and TableCell components.

TableContainer has the container.

Tabke has the table itself.

TableHead holds the headings.

TableRow has the rows.

TableCell has the cells.

We can set the align prop of a cell to right to make the content align to the right.

Also, we can set the component prop to display as th if we wish.

The scope is set to the row to indicate that the cell is for a row and we get different cell formatting than headings.

Conclusion

We can make virtualized lists if our list has lots of items.

List headings can be pinned.

Table components are part of the core Material UI package.

Categories
Material UI

Material UI — Utility Components

Material UI is a Material Design library made for React.

It’s a set of React components that have Material Design styles.

In this article, we’ll look at how to add a click away listener, CSS resets, and portals with Material UI.

Click Away Listener

We can add a click away listener to listen to when we click outside an element.

This lets us add things like menus easily.

For example, we can write:

import React from "react";
import ClickAwayListener from "@material-ui/core/ClickAwayListener";

export default function App() {
  const [open, setOpen] = React.useState(false);

const handleClick = () => {
    setOpen(prev => !prev);
  };

const handleClickAway = () => {
    setOpen(false);
  };

return (
    <ClickAwayListener onClickAway={handleClickAway}>
      <div>
        <button type="button" onClick={handleClick}>
          menu
        </button>
        {open ? <div>some menu</div> : null}
      </div>
    </ClickAwayListener>
  );
}

to add a menu to our app.

We have the ClickAwayListener to our App .

Then we put the stuff that we want to do something with when the user click away from inside.

We have a div that will be removed when we click outside of it.

The onClickAway prop takes a function that runs when we click away from something.

The handleClick prop toggles the open prop to toggle the menu.

handleClickAway sets open to false to close the menu.

When we click the button the menu will open.

When we click outside it, it’ll close.

Portal

The Portal component is like the React portal component.

It lets us render the dropdown into a new location outside of the DOM hierarchy.

To use it, we can write:

import React from "react";
import ClickAwayListener from "@material-ui/core/ClickAwayListener";
import Portal from "@material-ui/core/Portal";

export default function App() {
  const [open, setOpen] = React.useState(false);

  const handleClick = () => {
    setOpen(prev => !prev);
  };

  const handleClickAway = () => {
    setOpen(false);
  };

  return (
    <ClickAwayListener onClickAway={handleClickAway}>
      <div>
        <button type="button" onClick={handleClick}>
          menu
        </button>
        {open ? (
          <Portal>
            <div>some menu.</div>
          </Portal>
        ) : null}
      </div>
    </ClickAwayListener>
  );
}

We added the Portal component, which will render the div inside in the body instead of inside the div inside the ClickAwayListener .

Leading Edge

We can configure the ClickAwayListener to listen to leading events like mouse down and touch start.

For example, we can write:

import React from "react";
import ClickAwayListener from "@material-ui/core/ClickAwayListener";
import Portal from "@material-ui/core/Portal";

export default function App() {
  const [open, setOpen] = React.useState(false);

  const handleClick = () => {
    setOpen(prev => !prev);
  };

  const handleClickAway = () => {
    setOpen(false);
  };

  return (
    <ClickAwayListener
      mouseEvent="onMouseDown"
      touchEvent="onTouchStart"
      onClickAway={handleClickAway}
    >
      <div>
        <button type="button" onClick={handleClick}>
          menu
        </button>
        {open ? <div>some menu.</div> : null}
      </div>
    </ClickAwayListener>
  );
}

We have a mouseEvent and touchEvent props to set the events that we listen to.

Therefore, we listen to the mouse down and touch start events.

CSS Baseline

The CssBaseline component provides CSS resets.

It provides style normalization provided by normalize.css.

It resets the CSS defaults to the same ones across every browser.

For instance, we can use it by writing:

import React from "react";
import CssBaseline from "@material-ui/core/CssBaseline";

export default function App() {
  return (
    <>
      <CssBaseline />
      reset
    </>
  );
}

The CssBaseline component provides a global CSS reset.

If we want to scope the CSS reset to some components, we can use the ScopedCssBaseline component.

For example, we can write:

import React from "react";
import ScopedCssBaseline from "@material-ui/core/ScopedCssBaseline";

export default function App() {
  return (
    <>
      <ScopedCssBaseline>reset</ScopedCssBaseline>
    </>
  );
}

We wrap the ScopedCssBaseline component around the elements that we want to reset the defaults for.

Conclusion

We can use the click away listener to listen for users clicking away from elements.

Material UI provides CSS reset components we can use to reset the defaults to make them consistent everywhere.

Portals let us render items anywhere in the DOM hierarchy.

Categories
Material UI

Material UI — Tooltips

Material UI is a Material Design library made for React.

It’s a set of React components that have Material Design styles.

In this article, we’ll look at how to add tooltips with Material UI.

Simple Tooltips

We can add simple tooltips that display when we hover over another component.

For example, we can add a floating action button with the Fab component.

Then we surround that with a Tooltip to display a tooltip.

For example, we can write:

import React from "react";
import AddIcon from "@material-ui/icons/Add";
import Fab from "@material-ui/core/Fab";
import DeleteIcon from "@material-ui/icons/Delete";
import IconButton from "@material-ui/core/IconButton";
import Tooltip from "@material-ui/core/Tooltip";

export default function App() {
  return (
    <div>
      <Tooltip title="Delete File">
        <IconButton>
          <DeleteIcon />
        </IconButton>
      </Tooltip>
      <Tooltip title="Add Task">
        <Fab color="primary">
          <AddIcon />
        </Fab>
      </Tooltip>
    </div>
  );
}

to add tooltips with the Tooltip component.

We can surround it around an IconButton or a Fab .

The title has th content that’ll be displayed in the tooltip.

Positioned Tooltips

We can put the tooltip in the position we want.

For example, we can write:

import React from "react";
import AddIcon from "@material-ui/icons/Add";
import Fab from "@material-ui/core/Fab";
import DeleteIcon from "@material-ui/icons/Delete";
import IconButton from "@material-ui/core/IconButton";
import Tooltip from "@material-ui/core/Tooltip";

export default function App() {
  return (
    <div>
      <Tooltip title="Delete File" placement="left-end">
        <IconButton>
          <DeleteIcon />
        </IconButton>
      </Tooltip>
      <Tooltip title="Add Task" placement="left-start">
        <Fab color="primary">
          <AddIcon />
        </Fab>
      </Tooltip>
    </div>
  );
}

to add a placement prop to each tooltip to change the placement.

Customized Tooltips

We can style our tooltips with the withStyles function.

For example, we can write:

import React from "react";
import { withStyles } from "@material-ui/core/styles";
import DeleteIcon from "@material-ui/icons/Delete";
import IconButton from "@material-ui/core/IconButton";
import Tooltip from "@material-ui/core/Tooltip";

const LightTooltip = withStyles(theme => ({
  tooltip: {
    backgroundColor: theme.palette.common.white,
    color: "green",
    boxShadow: theme.shadows[1],
    fontSize: 11
  }
}))(Tooltip);

export default function App() {
  return (
    <div>
      <LightTooltip title="Delete File" placement="left-end">
        <IconButton>
          <DeleteIcon />
        </IconButton>
      </LightTooltip>
    </div>
  );
}

We made the backgroundColor white and the content color green.

Also, we change the fontSize to 11.

We can also use the makeStyles function to create classes that we can pass into the tooltip.

For example, we can write:

import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import DeleteIcon from "@material-ui/icons/Delete";
import IconButton from "@material-ui/core/IconButton";
import Tooltip from "@material-ui/core/Tooltip";

const useStyles = makeStyles(theme => ({
  arrow: {
    color: theme.palette.common.black
  },
  tooltip: {
    backgroundColor: theme.palette.common.black
  }
}));

function BlackTooltip(props) {
  const classes = useStyles();

  return <Tooltip arrow classes={classes} {...props} />;
}

export default function App() {
  return (
    <div>
      <BlackTooltip title="Delete File">
        <IconButton>
          <DeleteIcon />
        </IconButton>
      </BlackTooltip>
    </div>
  );
}

We created a BlackTooltip component that adds some classes to the Tooltip .

Now we have a black tooltip with a black arrow color as indicated in the arrow and tooltip properties.

Arrow Tooltips

To add an arrow to a tooltip, we can add the arrow prop.

For example, we can write;

import React from "react";
import DeleteIcon from "@material-ui/icons/Delete";
import IconButton from "@material-ui/core/IconButton";
import Tooltip from "@material-ui/core/Tooltip";

export default function App() {
  return (
    <div>
      <Tooltip title="Delete File" arrow>
        <IconButton>
          <DeleteIcon />
        </IconButton>
      </Tooltip>
    </div>
  );
}

We use the arrow prop on the Tooltip to add it.

Custom Child Element

To add a custom child element, we’ve to use the forwardRef and return our component inside the callback.

This way, the DOM event listeners for the element are applied.

For example, we can write:

import React from "react";
import DeleteIcon from "@material-ui/icons/Delete";
import IconButton from "@material-ui/core/IconButton";
import Tooltip from "@material-ui/core/Tooltip";

const Content = React.forwardRef((props, ref) => {
  return (
    <div {...props} ref={ref}>
      <IconButton>
        <DeleteIcon />
      </IconButton>
    </div>
  );
});

export default function App() {
  return (
    <div>
      <Tooltip title="Delete File" arrow>
        <Content />
      </Tooltip>
    </div>
  );
}

to create a Content component that we can put between the Tooltip tags.

We put our content inside the forwardRef callback so the tooltip will show properly.

Conclusion

We can create tooltips with various styles and positions.

The content can also be changed to show what we want.

The styling can be changed in various ways.

Categories
Material UI

Material UI — Tables

Material UI is a Material Design library made for React.

It’s a set of React components that have Material Design styles.

In this article, we’ll look at how to add tables with Material UI.

Dense Table

We can change the size prop of the table to small to make the table denser.

For example, we can write:

import React from "react";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Paper from "@material-ui/core/Paper";

function createData(firstName, lastName, age) {
  return { firstName, lastName, age };
}

const rows = [
  createData("james", "smith", 20),
  createData("mary", "jones", 30),
  createData("may", "wong", 30)
];

export default function App() {
  return (
    <TableContainer component={Paper}>
      <Table size="small">
        <TableHead>
          <TableRow>
            <TableCell>first name</TableCell>
            <TableCell>last name</TableCell>
            <TableCell align="right">age</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {rows.map(row => (
            <TableRow key={row.firstName}>
              <TableCell component="th" scope="row">
                {row.firstName}
              </TableCell>
              <TableCell>{row.lastName}</TableCell>
              <TableCell align="right">{row.age}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
}

then we display the table rows in a denser way than the default.

Sorting and Selecting

We can add sorting with the TableSortLabel component.

For example, we can write:

import React from "react";
import PropTypes from "prop-types";
import clsx from "clsx";
import { lighten, makeStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TablePagination from "@material-ui/core/TablePagination";
import TableRow from "@material-ui/core/TableRow";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import Paper from "@material-ui/core/Paper";
import Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import Tooltip from "@material-ui/core/Tooltip";

import DeleteIcon from "@material-ui/icons/Delete";
import FilterListIcon from "@material-ui/icons/FilterList";

function createData(name, age) {
  return { name, age };
}

const rows = [
  createData("james", 30),
  createData("mary", 45),
  createData("alex", 26)
];

function descendingComparator(a, b, orderBy) {
  if (b[orderBy] < a[orderBy]) {
    return -1;
  }
  if (b[orderBy] > a[orderBy]) {
    return 1;
  }
  return 0;
}

function getComparator(order, orderBy) {
  return order === "desc"
    ? (a, b) => descendingComparator(a, b, orderBy)
    : (a, b) => -descendingComparator(a, b, orderBy);
}

function stableSort(array, comparator) {
  const stabilizedThis = array.map((el, index) => [el, index]);
  stabilizedThis.sort((a, b) => {
    const order = comparator(a[0], b[0]);
    if (order !== 0) return order;
    return a[1] - b[1];
  });
  return stabilizedThis.map(el => el[0]);
}

const headCells = [
  {
    id: "name",
    label: "name"
  },
  { id: "age", numeric: true, label: "age" }
];

function EnhancedTableHead(props) {
  const {
    classes,
    onSelectAllClick,
    order,
    orderBy,
    numSelected,
    rowCount,
    onRequestSort
  } = props;
  const createSortHandler = property => event => {
    onRequestSort(event, property);
  };

  return (
    <TableHead>
      <TableRow>
        <TableCell padding="checkbox">
          <Checkbox
            indeterminate={numSelected > 0 && numSelected < rowCount}
            checked={rowCount > 0 && numSelected === rowCount}
            onChange={onSelectAllClick}
            inputProps={{ "aria-label": "select all desserts" }}
          />
        </TableCell>
        {headCells.map(headCell => (
          <TableCell
            key={headCell.id}
            align={headCell.numeric ? "right" : "left"}
            padding={headCell.disablePadding ? "none" : "default"}
            sortDirection={orderBy === headCell.id ? order : false}
          >
            <TableSortLabel
              active={orderBy === headCell.id}
              direction={orderBy === headCell.id ? order : "asc"}
              onClick={createSortHandler(headCell.id)}
            >
              {headCell.label}
              {orderBy === headCell.id ? (
                <span className={classes.visuallyHidden}>
                  {order === "desc" ? "sorted descending" : "sorted ascending"}
                </span>
              ) : null}
            </TableSortLabel>
          </TableCell>
        ))}
      </TableRow>
    </TableHead>
  );
}

EnhancedTableHead.propTypes = {
  classes: PropTypes.object.isRequired,
  numSelected: PropTypes.number.isRequired,
  onRequestSort: PropTypes.func.isRequired,
  onSelectAllClick: PropTypes.func.isRequired,
  order: PropTypes.oneOf(["asc", "desc"]).isRequired,
  orderBy: PropTypes.string.isRequired,
  rowCount: PropTypes.number.isRequired
};

const useToolbarStyles = makeStyles(theme => ({
  root: {
    paddingLeft: theme.spacing(2),
    paddingRight: theme.spacing(1)
  },
  highlight:
    theme.palette.type === "light"
      ? {
          color: theme.palette.secondary.main,
          backgroundColor: lighten(theme.palette.secondary.light, 0.85)
        }
      : {
          color: theme.palette.text.primary,
          backgroundColor: theme.palette.secondary.dark
        },
  title: {
    flex: "1 1 100%"
  }
}));

const EnhancedTableToolbar = props => {
  const classes = useToolbarStyles();
  const { numSelected } = props;

return (
    <Toolbar
      className={clsx(classes.root, {
        [classes.highlight]: numSelected > 0
      })}
    >
      {numSelected > 0 ? (
        <Typography
          className={classes.title}
          color="inherit"
          variant="subtitle1"
          component="div"
        >
          {numSelected} selected
        </Typography>
      ) : (
        <Typography
          className={classes.title}
          variant="h6"
          id="tableTitle"
          component="div"
        >
          Names
        </Typography>
      )}

{numSelected > 0 ? (
        <Tooltip title="Delete">
          <IconButton aria-label="delete">
            <DeleteIcon />
          </IconButton>
        </Tooltip>
      ) : (
        <Tooltip title="Filter list">
          <IconButton aria-label="filter list">
            <FilterListIcon />
          </IconButton>
        </Tooltip>
      )}
    </Toolbar>
  );
};

EnhancedTableToolbar.propTypes = {
  numSelected: PropTypes.number.isRequired
};

const useStyles = makeStyles(theme => ({
  root: {
    width: "100%"
  },
  paper: {
    width: "100%",
    marginBottom: theme.spacing(2)
  },
  table: {
    minWidth: 750
  },
  visuallyHidden: {
    border: 0,
    clip: "rect(0 0 0 0)",
    height: 1,
    margin: -1,
    overflow: "hidden",
    padding: 0,
    position: "absolute",
    top: 20,
    width: 1
  }
}));

export default function App() {
  const classes = useStyles();
  const [order, setOrder] = React.useState("asc");
  const [orderBy, setOrderBy] = React.useState("calories");
  const [selected, setSelected] = React.useState([]);
  const [page, setPage] = React.useState(0);
  const [rowsPerPage, setRowsPerPage] = React.useState(5);

  const handleRequestSort = (event, property) => {
    const isAsc = orderBy === property && order === "asc";
    setOrder(isAsc ? "desc" : "asc");
    setOrderBy(property);
  };

  const handleSelectAllClick = event => {
    if (event.target.checked) {
      const newSelecteds = rows.map(n => n.name);
      setSelected(newSelecteds);
      return;
    }
    setSelected([]);
  };

  const handleClick = (event, name) => {
    const selectedIndex = selected.indexOf(name);
    let newSelected = [];

    if (selectedIndex === -1) {
      newSelected = newSelected.concat(selected, name);
    } else if (selectedIndex === 0) {
      newSelected = newSelected.concat(selected.slice(1));
    } else if (selectedIndex === selected.length - 1) {
      newSelected = newSelected.concat(selected.slice(0, -1));
    } else if (selectedIndex > 0) {
      newSelected = newSelected.concat(
        selected.slice(0, selectedIndex),
        selected.slice(selectedIndex + 1)
      );
    }

    setSelected(newSelected);
  };

  const handleChangePage = (event, newPage) => {
    setPage(newPage);
  };

  const handleChangeRowsPerPage = event => {
    setRowsPerPage(parseInt(event.target.value, 10));
    setPage(0);
  };

  const isSelected = name => selected.indexOf(name) !== -1;

  const emptyRows =
    rowsPerPage - Math.min(rowsPerPage, rows.length - page * rowsPerPage);

  return (
    <div className={classes.root}>
      <Paper className={classes.paper}>
        <EnhancedTableToolbar numSelected={selected.length} />
        <TableContainer>
          <Table className={classes.table}>
            <EnhancedTableHead
              classes={classes}
              numSelected={selected.length}
              order={order}
              orderBy={orderBy}
              onSelectAllClick={handleSelectAllClick}
              onRequestSort={handleRequestSort}
              rowCount={rows.length}
            />
            <TableBody>
              {stableSort(rows, getComparator(order, orderBy))
                .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
                .map((row, index) => {
                  const isItemSelected = isSelected(row.name);

return (
                    <TableRow
                      hover
                      onClick={event => handleClick(event, row.name)}
                      role="checkbox"
                      aria-checked={isItemSelected}
                      tabIndex={-1}
                      key={row.name}
                      selected={isItemSelected}
                    >
                      <TableCell padding="checkbox">
                        <Checkbox checked={isItemSelected} />
                      </TableCell>
                      <TableCell component="th" scope="row" padding="none">
                        {row.name}
                      </TableCell>
                      <TableCell align="right">{row.age}</TableCell>
                    </TableRow>
                  );
                })}
              {emptyRows > 0 && (
                <TableRow>
                  <TableCell />
                </TableRow>
              )}
            </TableBody>
          </Table>
        </TableContainer>
        <TablePagination
          rowsPerPageOptions={[5, 10, 25]}
          component="div"
          count={rows.length}
          rowsPerPage={rowsPerPage}
          page={page}
          onChangePage={handleChangePage}
          onChangeRowsPerPage={handleChangeRowsPerPage}
        />
      </Paper>
    </div>
  );
}

to add a table with sorting and selection.

We started with the example from https://material-ui.com/components/tables/#sorting-amp-selecting.

We have the stableSort function to do the sorting.

And we have the checkbox on the row to show let us select items.

We also added a toolbar to the bottom to let us select the number of rows to display with the TablePagination component.

The EnhancedTableHead has the table heading with sorting.

We used the TableSortLabel to let us do the sorting.

Conclusion

We can add dense tables and tables with sorting with the built-in components.

Categories
Material UI

Material UI — Popovers

Material UI is a Material Design library made for React.

It’s a set of React components that have Material Design styles.

In this article, we’ll look at how to add popovers with Material UI.

Simple Popover

Popovers let us display some content over other things.

To add it, we can use the Popover component.

For example, we can write:

import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import Popover from "@material-ui/core/Popover";
import Typography from "@material-ui/core/Typography";
import Button from "@material-ui/core/Button";

const useStyles = makeStyles(theme => ({
  typography: {
    padding: theme.spacing(3)
  }
}));

export default function App() {
  const classes = useStyles();
  const [anchorEl, setAnchorEl] = React.useState(null);

  const handleClick = event => {
    setAnchorEl(event.currentTarget);
  };

  const handleClose = () => {
    setAnchorEl(null);
  };

  const open = Boolean(anchorEl);

  return (
    <div>
      <Button variant="contained" color="primary" onClick={handleClick}>
        Open Popover
      </Button>
      <Popover
        open={open}
        anchorEl={anchorEl}
        onClose={handleClose}
        anchorOrigin={{
          vertical: "bottom",
          horizontal: "center"
        }}
        transformOrigin={{
          vertical: "top",
          horizontal: "center"
        }}
      >
        <Typography className={classes.typography}>Popover content.</Typography>
      </Popover>
    </div>
  );
}

to add a button to open the popover and the popover itself.

The anchorEl prop has the anchor element of the popover, which is the button itself.

anchorOrigin specifies where relative to the button do we display the popover.

transformOrigin lets us change the position of the popover itself.

open sets whether the popover ios open or not.

If the anchorEl is set, then it’s open.

Otherwise, it’s not open.

onClose is a function that runs when the popover closes.

Mouse Over Interaction

We can open a popover when we move our mouse over an element.

For example, we can write:

import React from "react";
import Popover from "@material-ui/core/Popover";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/core/styles";

const useStyles = makeStyles(theme => ({
  popover: {
    pointerEvents: "none"
  },
  paper: {
    padding: theme.spacing(3)
  }
}));

export default function App() {
  const classes = useStyles();
  const [anchorEl, setAnchorEl] = React.useState(null);

  const handlePopoverOpen = event => {
    setAnchorEl(event.currentTarget);
  };

  const handlePopoverClose = () => {
    setAnchorEl(null);
  };

  const open = Boolean(anchorEl);

  return (
    <div>
      <Typography
        onMouseEnter={handlePopoverOpen}
        onMouseLeave={handlePopoverClose}
      >
        Hover me
      </Typography>
      <Popover
        className={classes.popover}
        classes={{
          paper: classes.paper
        }}
        open={open}
        anchorEl={anchorEl}
        anchorOrigin={{
          vertical: "bottom",
          horizontal: "left"
        }}
        transformOrigin={{
          vertical: "top",
          horizontal: "left"
        }}
        onClose={handlePopoverClose}
        disableRestoreFocus
      >
        <Typography>some popover.</Typography>
      </Popover>
    </div>
  );
}

We pass in functions to the onMouseEnter prop to let us open the popover when we hover over the text.

Similarly, we pass in a function to the onMouseLeave prop to close the modal when our mouse leaves the element respectively.

The other parts are the same as the previous example.

PopupState Helper

To make managing the popover state easier, we can use the material-ui-popup-state library to manage the state.

To install it, we run:

npm install --save material-ui-popup-state

Then we can write:

import React from "react";
import Typography from "@material-ui/core/Typography";
import Box from "@material-ui/core/Box";
import Button from "@material-ui/core/Button";
import Popover from "@material-ui/core/Popover";
import PopupState, { bindTrigger, bindPopover } from "material-ui-popup-state";

export default function App() {
  return (
    <PopupState variant="popover">
      {popupState => (
        <div>
          <Button
            variant="contained"
            color="primary"
            {...bindTrigger(popupState)}
          >
            Open Popover
          </Button>
          <Popover
            {...bindPopover(popupState)}
            anchorOrigin={{
              vertical: "bottom",
              horizontal: "center"
            }}
            transformOrigin={{
              vertical: "top",
              horizontal: "center"
            }}
          >
            <Box p={3}>
              <Typography>some popover.</Typography>
            </Box>
          </Popover>
        </div>
      )}
    </PopupState>
  );
}

We have the PopupState coponent wirh the variant set to popover to display a popover.

Then inside it, we have a function that takes the popupState parameter, which has the open state of the popover.

We use the bindTrigger with it to return an object to toggle the popup in the button.

In the popover, we call bindPopover to bind the open prop to the popupState .

The rest of the code is the same as previous examples.

Conclusion

We can add popovers with the Popover component.

The material-ui-popup-state library can make managing the popover state easier.