I've made a table with a folder icon that is clickable link. To improve UX, I'd like to make the clickable area of the link bigger. I would like to make the entire contents of 3 of 4 of the cells in a row should be clickable as a link.
Here's what I mean
I can successfully make each cell contents (as in icons or text) clickable, but that still represents only a small area in each cell.
I've tried wrapping the entire cell contents in the link, but it leads to messy mapping, and still does not make the entire contents of the cell clickable.
Here's where I'm at so far
...
interface IProps extends Omit<unknown, 'children'> {
folders?: IGetAllRequestDTO<IFolderDTO> | null;
reload: boolean;
}
const RootFoldersTable = ({ folders, reload }: IProps): JSX.Element => {
const [selectedRow, setSelectedRow] = useState('');
const classes = useStyles();
const dispatch = useDispatch();
const getCorrectFormat = useCallback(cell => {
return cell instanceof Date
? format(cell as Date, "MM/dd/yyyy hh:mmaaaaa'm'")
: cell;
}, []);
const getAllFolders = () => {
dispatch(appActions.getAllFoldersRequest({}));
};
useEffect(() => {
getAllFolders();
}, [reload]);
const openDialogWithId = (folderId: string) => {
dispatch(appActions.setDialogFormFolderId(folderId));
dispatch(appActions.setDialogOpen());
};
const onDeleteFolder = (id: string) => {
dispatch(appActions.deleteFolderRequest(id));
};
const tableHeadElements = [
{ label: 'Name', key: 'name', sortable: false },
{ label: 'Last Modified', key: 'updateAt', sortable: false },
{ label: 'Actions', sortable: false }
];
const tableHeadElements = [
{ label: 'Name', key: 'name', sortable: false },
{ label: 'Last Modified', key: 'updateAt', sortable: false },
{ label: 'Actions', sortable: false }
];
return (
<div className={classes.tableContainer}>
<TableContainer className={classes.tableBodyContainer}>
<Table className={classes.table} size="small">
<TableHead>
<TableRow className={classes.tableHeadRow}>
{tableHeadElements.map(e => (
<TableCell key={e.key} align="center">
{e.label}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{folders?.items.map((folder: IFolderDTO, index: number) => {
const { id, name, updatedAt } = folder;
return (
<TableRow
className={classes.tableRow}
classes={{ selected: classes.selectedRow }}
selected={selectedRow === id}
onClick={() => setSelectedRow(id)}
key={index}
>
<Link to={APP_DASHBOARD_CHILD_FOLDER_CONTENTS_PATH(id)}>
<TableCell align="center">
<IconButton color="default" size={'small'}>
<FolderIcon fontSize="default" />
</IconButton>
</TableCell>
</Link>
{[name, new Date(updatedAt)].map(cell => (
<TableCell key={index} align="center">
<Link to={APP_DASHBOARD_CHILD_FOLDER_CONTENTS_PATH(id)}>
{getCorrectFormat(cell)}
</Link>
</TableCell>
))}
<FolderActionsMenu
folderId={id}
onDeleteFolder={onDeleteFolder}
openDialogWithId={openDialogWithId}
/>
</TableRow>
);
})}
</TableBody>
Thanks!
You can create a header cell data array that describes anything you need to render the TableCell:
const headerCellData = [
{
name: 'Calories',
link: '/',
},
{
name: 'Fat (g)',
link: '/?filter=fat',
align: 'right',
},
{
name: 'Carbs (g)',
link: '/?filter=carbs',
align: 'right',
},
{
name: 'Protein (g)',
align: 'right',
},
];
Then map each data item to the TableCell:
<TableHead>
<TableRow>
{headerCellData.map((c) => (
<TableCell align={c.align}>
{c.link ? <Link to={c.link}>{c.name}</Link> : c.name}
</TableCell>
))}
</TableRow>
</TableHead>
make the entire contents of the cell clickable
If you want the entire cell clickable (not just the text), you need to play around with CSS a little bit. Remove the padding of TableCell where it's unclickable and set the padding of the container that holds the link to 16px which is the padding of the TableCell we just removed:
<TableCell align={c.align} sx={{ padding: 0 }}>
<Box sx={{ padding: '16px' }}>
{c.link ? <Link to={c.link}>{c.name}</Link> : c.name}
</Box>
</TableCell>
Related
Fetching news headlines from an API and showing 10 news per page and theirs id's. which also refreshes in 10 seconds and new headlines come. TableHead mapping contents seems fine but tableBody providing this error:
Type 'string | Date' is not assignable to type 'ReactNode
//Init post
export interface InitPost {
title: string;
url: string;
created_at: Date;
author: string;
}
//state of post
const [posts, setPosts] = useState<InitPost[]>([]);
//colums
const columns: readonly Column[] = [
{ id: "title", label: "Title", minWidth: 170 },
{ id: "url", label: "URL", minWidth: 150 },
{ id: "created_at", label: "Created At", minWidth: 100 },
{ id: "author", label: "Author", minWidth: 100 },
];
Table head part: Fine
<TableHead>
<TableRow>
{columns.map((column) => (
<TableCell
key={column.id}
align={column.align}
style={{ minWidth: column.minWidth }}
>
{column.label}
</TableCell>
))}
<TableCell />
</TableRow>
</TableHead>
Error part
<TableBody>
{posts.map((row, index) => {
return (
<TableRow key={index}>
{columns.map((column) => {
const value = row[column.id];
****next line showing Error in "{value}"****
return <TableCell key={column.id}>{value}</TableCell>;
})}
<TableCell>
<Button
size="small"
variant="contained"
onClick={() => getDetails(row)}
>
Details
</Button>
</TableCell>
</TableRow>
);
})}
</TableBody>
The primary issue is that a Date type and string type are incompatible.
Objects, with the exception of React Elements of course, are not valid as children.
One of the columns you are rendering (created_at) is of type Date and cannot be used directly as a React Child.
You need to format the Date in some way or call the .toString() method between the braces to ensure that your children are not objects.
{
columns.map((column) => {
const value = row[column.id];
return <TableCell key={column.id}>{value.toString()}</TableCell>;
});
}
**This is material UI code for the sticky header and vertical scroll I want this with horizontal scroll also. I tried to use overflowX:"auto and scroll on paper but it didn't work" I also tried to use other tables to complete this work but got confused. OverflowX on paper isn't working. **
For more information or reference please check https://material-ui.com/components/tables/
const columns = [
{ id: 'name', label: 'Name', minWidth: 170 },
{ id: 'code', label: 'ISO\u00a0Code', minWidth: 100 },
{
id: 'population',
label: 'Population',
minWidth: 170,
align: 'right',
format: (value) => value.toLocaleString('en-US'),
},
{
id: 'size',
label: 'Size\u00a0(km\u00b2)',
minWidth: 170,
align: 'right',
format: (value) => value.toLocaleString('en-US'),
},
{
id: 'density',
label: 'Density',
minWidth: 170,
align: 'right',
format: (value) => value.toFixed(2),
},
];
function createData(name, code, population, size) {
const density = population / size;
return { name, code, population, size, density };
}
const rows = [
createData('India', 'IN', 1324171354, 3287263),
createData('China', 'CN', 1403500365, 9596961),
createData('Italy', 'IT', 60483973, 301340),
createData('United States', 'US', 327167434, 9833520),
createData('Canada', 'CA', 37602103, 9984670),
createData('Australia', 'AU', 25475400, 7692024),
createData('Germany', 'DE', 83019200, 357578),
createData('Ireland', 'IE', 4857000, 70273),
createData('Mexico', 'MX', 126577691, 1972550),
createData('Japan', 'JP', 126317000, 377973),
createData('France', 'FR', 67022000, 640679),
createData('United Kingdom', 'GB', 67545757, 242495),
createData('Russia', 'RU', 146793744, 17098246),
createData('Nigeria', 'NG', 200962417, 923768),
createData('Brazil', 'BR', 210147125, 8515767),
];
const useStyles = makeStyles({
root: {
width: '100%',
},
container: {
maxHeight: 440,
},
});
export default function StickyHeadTable() {
const classes = useStyles();
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(10);
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
setRowsPerPage(+event.target.value);
setPage(0);
};
return (
<Paper className={classes.root}>
<TableContainer className={classes.container}>
<Table stickyHeader aria-label="sticky table">
<TableHead>
<TableRow>
{columns.map((column) => (
<TableCell
key={column.id}
align={column.align}
style={{ minWidth: column.minWidth }}
>
{column.label}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((row) => {
return (
<TableRow hover role="checkbox" tabIndex={-1} key={row.code}>
{columns.map((column) => {
const value = row[column.id];
return (
<TableCell key={column.id} align={column.align}>
{column.format && typeof value === 'number' ? column.format(value) : value}
</TableCell>
);
})}
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 100]}
component="div"
count={rows.length}
rowsPerPage={rowsPerPage}
page={page}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
/>
</Paper>
);
}
After searching I found that putting maxWidth on the container will allow you to have a vertically and horizontally scrollable table.
Currently My table view is like following:
Mui Table The table has a column name 'Action' which has a edit icon button.
Now I want to show (visibility) edit icon to edit each row only when user will hover over table row.
I have tried to override MUITable theme of Material Table but the following code didn't work. can anybody help me?
const getMuiTheme = () => createMuiTheme({
overrides: {
MUIDataTableBodyCell: {
root: {
'&:last-child': {
visibility: 'hidden'
}
}
},
MuiTableRow: {
root: {
'&$hover:hover': {
'& .MUIDataTableBodyCell-root': {
'&:last-child': {
visibility: 'visible'
}
}
}
}
}
}
});
This is an example for that purpose: https://codesandbox.io/s/material-demo-hr3te?file=/demo.js.
Basically I do something:
Add new state to store a boolean variable to determine when to show/hide a component.
Add onMouseEnter and onMouseLeave to TableRow component, for hover effect.
Then set state according to the hover event above.
That's it!
You need to add one action column in your columns array like below:
const columns = [
{
name: 'id',
label: 'id',
options: {
sort: false,
viewColumns: false,
display: false,
filter: false
}
},
{
name: 'transportationBranch',
label: 'Transportation Branch',
options: {
sort: false,
viewColumns: false
}
},
{
name: 'charge',
label: 'Charge',
options: {
filter: false,
sort: false
}
},
{
name: 'tax',
label: 'Tax',
options: {
filter: false,
sort: false
}
},
{
name: '',
label: '',
options: {
filter: false,
sort: false,
viewColumns: false,
customBodyRender: (value, tableMeta, updateValue) => {
return (
<IconButton
id={'Edit-' + tableMeta.rowIndex}
style={{display: 'none'}}
component="button"
variant="body2"
onClick={(event) => {
console.log(event);
alert(tableMeta.rowIndex);
}}
>
<EditIcon />
</IconButton>
);
}
}
}
];
add the the following option in mui datatable options:
setRowProps: (row, dataIndex, rowIndex) => {
return {
onMouseEnter: (e) => handleRowHover(e, row, rowIndex),
onMouseLeave: (e) => handleRowHoverLeave(e, row, rowIndex)
};
},
Write these two event in your component:
function handleRowHover(event, row, rowIndex) {
let control = document.getElementById('Edit-' + rowIndex);
control.style.display = 'block';
}
function handleRowHoverLeave(event, row, rowIndex) {
let control = document.getElementById('Edit-' + rowIndex);
control.style.display = 'none';
}
Note: we used id={'Edit-' + tableMeta.rowIndex} in IconButton so
that we can use getElementById in our two events.
If you can implement this properly then you will see this type ui on your browser:
I modified my original answer according to the mods comments:
I modified ShinaBR2 answer, so that it shows only a text in the current row: https://codesandbox.io/s/material-ui-table-onhover-action-zfloy?file=/demo.js
The idea is to use the row id on mouse enter (or mouse hover) and compare it with the current row, that is hovered to display the element in the row.
export default function SimpleTable() {
const classes = useStyles();
const [showActionId, setShowActionId] = useState(-1);
return (
<TableContainer component={Paper}>
<Table className={classes.table} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Dessert (100g serving)</TableCell>
<TableCell align="right">Calories</TableCell>
<TableCell align="right">Fat (g)</TableCell>
<TableCell align="right">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<TableRow
key={row.name}
onMouseEnter={() => {
setShowActionId(row.id); // set id here
}}
onMouseLeave={() => setShowActionId(-1)}
>
<TableCell component="th" scope="row">
{row.name}
</TableCell>
<TableCell align="right">{row.calories}</TableCell>
<TableCell align="right">{row.fat}</TableCell>
<TableCell align="right">
{row.id === showActionId ? "Show me" : ""} // check the id here and display the message
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
}
One approach for this is css in js code , we make the icon hidden by default(hiddePin in makeStyles), then change the 'hidden' to 'visible' on hover using "&:hover $clearHidden".
below code was verified . my place is not good work with CodeSandBox(network issue) , so i just show the code here .
const useStyles = makeStyles({
table: {
minWidth: 350,
},
root: {
"&:hover $clearHidden": {
visibility: "visible"
}
},
clearHidden: {},
hiddePin: {
visibility: "hidden"
}
});
return (
<Table className={classes.table} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>name</TableCell>
<TableCell align="center">desc</TableCell>
<TableCell align="center">title2</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<TableRow key={row.name} hover={true}>
<TableCell component="th" scope="row">
{row.name}
</TableCell>
<TableCell align="center">{row.desc}</TableCell>
<TableCell align="center" className={classes.root} >
<IconButton size="small" className={clsx(classes.hiddePin, classes.clearHidden)} onClick={(event) => handlePinClick(event,row.stamp)}>
<SvgPinOutLine />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
To only show the actions in the row your hovering over, I added the following useState objects
const [showAction, setShowAction] = React.useState(false);
const [showId, setShowId] = React.useState(0);
and then in the row object
onMouseEnter={() => {
setShowAction(true);
setShowId(row.id);}
}
onMouseLeave={() => setShowAction(false)}
and then in the specific cell that shows the actions:
{row.id == showId && showAction ? (
<IconButton>
<DeleteIcon/>
</IconButton>
) : (
<Box></Box>)}
I hope this helps someone!
I'm creating a generic and dynamic table where I have columns, rows and an action button at the end of an row. All data is displaying as intended but the header is not displaying over the action button.
Is there a way to display the header underline over the action button?
Data:
[
{
"id": "1231",
"name": "Michael",
"phone": "11223311",
"medical": "YES"
},
{
"id": "32123",
"name": "Johnson",
"phone": "3311323",
"medical": "NO"
}
]
const headCells = [
{dataIndex: 'id', name: 'ID' },
{dataIndex: 'name', name: 'Name' },
{dataIndex: 'phone', name: 'Phone' },
{dataIndex: 'medical', name: 'Medical' }
];
const Header = ({ cols }) => {
return cols.map((col) => (
<TableCell>{col.name}</TableCell>
));
}
const Rows = ({ data, cols }) => {
if (data) {
return data.map((row) =>
<TableRow key={row.uuid}>
{cols.map(col => <TableCell>{row[col.dataIndex]}</TableCell>)}
<TableCell >
<IconButton>
<SearchIcon color="primary"/>
</IconButton>
</TableCell>
</TableRow>
);
}
else return [];
};
return(
<TableContainer component={Paper}>
<Table stickyHeader aria-label="sticky table">
<TableHead>
<TableRow>
<Header cols={headCells} />
</TableRow>
</TableHead>
<TableBody>
<Rows data={data} cols={headCells} />
</TableBody>
</Table>
</TableContainer>
)
Why don't you add it manually?:
const Header = ({ cols }) => {
return cols.map((col) => (
<TableCell >{col.name}</TableCell>
)).concat( <TableCell className='underline-header'>Actions</TableCell>);
}
CSS:
.underline-header {
text-decoration: underline;
}
This is happening because you are maping over your data columns, but you do not have a column for "actions". You could do what #Enchew says above or you could add another data column
const headCells = [
{dataIndex: 'id', name: 'ID' },
{dataIndex: 'name', name: 'Name' },
{dataIndex: 'phone', name: 'Phone' },
{dataIndex: 'medical', name: 'Medical' },
{
Component: () => (
<IconButton>
<SearchIcon color="primary"/>
</IconButton>
),
name: 'Actions'
}
];
const Header = ({ cols }) => {
return cols.map((col) => (
<TableCell>{col.name}</TableCell>
));
}
const Rows = ({ data, cols }) => {
if (data) {
return data.map((row) =>
<TableRow key={row.uuid}>
{cols.map(col => (
<TableCell>
{
col.Component ?
<Component /> :
<>row[col.dataIndex]</>
}
</TableCell>
)}
</TableRow>
);
}
else return [];
};
return(
<TableContainer component={Paper}>
<Table stickyHeader aria-label="sticky table">
<TableHead>
<TableRow>
<Header cols={headCells} />
</TableRow>
</TableHead>
<TableBody>
<Rows data={data} cols={headCells} />
</TableBody>
</Table>
</TableContainer>
)
Just a different way to approach the problem 👍🏻
I'm using Material UI to create a table in react. The data is displayed and sorted correctly when I click on each column header in the table. However I want the active attribute to become active only for that specific column when I click on it. The way its working now is all of the column headers just alternate between active and not active (true/false). Anyone have any idea to to prevent every other column becoming active when I just click on specific one? Thanks.
import React, { Component } from "react";
import PropTypes from 'prop-types';
import withStyles from '#material-ui/core/styles/withStyles';
import { withRouter } from "react-router-dom";
import Table from '#material-ui/core/Table';
import TableBody from '#material-ui/core/TableBody';
import TableCell from '#material-ui/core/TableCell';
import TableHead from '#material-ui/core/TableHead';
import TableRow from '#material-ui/core/TableRow';
import TableSortLabel from '#material-ui/core/TableSortLabel';
import Paper from '#material-ui/core/Paper';
import CssBaseline from '#material-ui/core/CssBaseline';
import axios from "axios";
const NumberFormat = require('react-number-format');
const styles = theme => ({
main: {
width: 'auto',
display: 'block',
marginLeft: theme.spacing.unit * 3,
marginRight: theme.spacing.unit * 3,
[theme.breakpoints.up(500 + theme.spacing.unit * 3 * 2)]: {
width: 1000,
marginLeft: 'auto',
marginRight: 'auto',
},
},
paper: {
marginTop: theme.spacing.unit * 16,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px`,
},
table: {
minWidth: 950,
},
tableWrapper: {
overflowX: 'auto',
},
});
const headRows = [
{ id: 'market_cap_rank', numeric: true, disablePadding: true, label: '#' },
{ id: 'name', numeric: false, disablePadding: false, label: 'Name' },
{ id: 'current_price', numeric: true, disablePadding: false, label: 'Price' },
{ id: 'price_change_percentage_24h', numeric: true, disablePadding: false, label: 'Change (24h)' },
{ id: 'market_cap', numeric: true, disablePadding: false, label: 'Market Cap' },
{ id: 'high_24h', numeric: true, disablePadding: false, label: '24H High' },
{ id: 'total_volume', numeric: true, disablePadding: false, label: 'Volume' },
];
class Test extends Component {
constructor(props) {
super(props);
this.state = {
data: [],
direction: 'asc',
arrow: true,
active: false
}
this.sortByNumericValue = this.sortByNumericValue.bind(this);
this.sortByStringValue = this.sortByStringValue.bind(this);
}
componentDidMount() {
axios.get('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=10&page=1&sparkline=false')
.then(res => {
const data = res.data;
console.log(data);
this.setState({ data: data })
})
}
sortByNumericValue = (key) => {
const { data } = this.state;
this.setState(prevState => ({
data: data.sort((a, b) => (
this.state.direction[key] === 'asc'
?
parseFloat(a[key]) - parseFloat(b[key])
: parseFloat(b[key]) - parseFloat(a[key])
)),
direction: {
[key]: this.state.direction[key] === 'asc'
? 'desc'
: 'asc'
},
arrow : !prevState.arrow,
active : !prevState.active
}));
}
sortByStringValue = (key) => {
const { data } = this.state;
data: data.sort((a, b) => {
const asc = this.state.direction[key] === 'asc';
if (a[key] > b[key]) {
return asc ? -1 : 1;
} else if (a[key] < b[key]) {
return asc ? 1 : -1;
} else {
return 0;
}
}),
this.setState(prevState => ({
data: data,
direction: {
[key]: this.state.direction[key] === 'asc'
? 'desc'
: 'asc'
},
arrow : !prevState.arrow,
active : !prevState.active
}));
}
render() {
const { classes } = this.props;
return (
<main className={classes.main}>
<CssBaseline />
<Paper className={classes.paper}>
<div className={classes.tableWrapper}>
<Table className={classes.table}>
<TableHead>
<TableRow>
{headRows.map(row => (
<TableCell
key={row.id}
padding={row.disablePadding ? 'none' : 'default'}
>
<TableSortLabel
active={this.state.active}
direction={this.state.arrow ? 'asc' : 'desc'}
onClick = {() => row.id === "name" || row.id === "market_cap_rank" ? this.sortByStringValue(row.id) : this.sortByNumericValue(row.id)}
>
{row.label}
</TableSortLabel>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{this.state.data.map((n, index) => {
return (
<TableRow
key={index}
>
<TableCell component="th" scope="row" >
{n.market_cap_rank}
</TableCell>
<TableCell >{n.name} </TableCell>
<TableCell ><NumberFormat value={n.current_price} displayType={'text'} decimalScale={2} thousandSeparator={true} prefix={'$'} /></TableCell>
<TableCell ><NumberFormat value={n.price_change_percentage_24h} displayType={'text'} decimalScale={2} /><span>%</span></TableCell>
<TableCell ><NumberFormat value={n.market_cap} displayType={'text'} decimalScale={2} thousandSeparator={true} prefix={'$'} /></TableCell>
<TableCell ><NumberFormat value={n.high_24h} displayType={'text'} decimalScale={2} thousandSeparator={true} prefix={'$'} /></TableCell>
<TableCell ><NumberFormat value={n.total_volume} displayType={'text'} decimalScale={2} thousandSeparator={true} prefix={'$'} /></TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</Paper>
</main>
);
}
}
Test.propTypes = {
classes: PropTypes.object.isRequired,
};
export default (withRouter(withStyles(styles)(Test)));
You have to add some css when you click on some column that time active css should be there and when you click other one previously should be false and current item shoud be active. Like this you can acheive the functionality.