diff --git a/docs/data/data-grid/custom-columns/CustomColumnFullExample.js b/docs/data/data-grid/custom-columns/CustomColumnFullExample.js new file mode 100644 index 000000000000..be7c002fb2b7 --- /dev/null +++ b/docs/data/data-grid/custom-columns/CustomColumnFullExample.js @@ -0,0 +1,159 @@ +import * as React from 'react'; +import { + randomColor, + randomEmail, + randomInt, + randomName, + randomArrayItem, + random, +} from '@mui/x-data-grid-generator'; +import { DataGrid, gridStringOrNumberComparator } from '@mui/x-data-grid'; +import { renderAvatar } from './cell-renderers/avatar'; +import { renderEmail } from './cell-renderers/email'; +import { renderEditRating, renderRating } from './cell-renderers/rating'; +import { + COUNTRY_ISO_OPTIONS, + renderCountry, + renderEditCountry, +} from './cell-renderers/country'; +import { renderSparkline } from './cell-renderers/sparkline'; +import { renderEditProgress, renderProgress } from './cell-renderers/progress'; +import { + renderEditStatus, + renderStatus, + STATUS_OPTIONS, +} from './cell-renderers/status'; +import { + INCOTERM_OPTIONS, + renderEditIncoterm, + renderIncoterm, +} from './cell-renderers/incoterm'; + +const columns = [ + { + field: 'name', + headerName: 'Name', + width: 120, + editable: true, + }, + { + field: 'avatar', + headerName: 'Avatar', + display: 'flex', + renderCell: renderAvatar, + valueGetter: (value, row) => + row.name == null || row.avatar == null + ? null + : { name: row.name, color: row.avatar }, + sortable: false, + filterable: false, + }, + { + field: 'email', + headerName: 'Email', + renderCell: renderEmail, + width: 150, + editable: true, + }, + { + field: 'rating', + headerName: 'Rating', + display: 'flex', + renderCell: renderRating, + renderEditCell: renderEditRating, + width: 180, + type: 'number', + editable: true, + availableAggregationFunctions: ['avg', 'min', 'max', 'size'], + }, + { + field: 'country', + headerName: 'Country', + type: 'singleSelect', + valueOptions: COUNTRY_ISO_OPTIONS, + valueFormatter: (value) => value?.label, + renderCell: renderCountry, + renderEditCell: renderEditCountry, + sortComparator: (v1, v2, param1, param2) => + gridStringOrNumberComparator(v1.label, v2.label, param1, param2), + width: 150, + editable: true, + }, + { + field: 'salary', + headerName: 'Salary', + type: 'number', + valueFormatter: (value) => { + if (!value || typeof value !== 'number') { + return value; + } + return `$${value.toLocaleString()}`; + }, + editable: true, + }, + { + field: 'monthlyActivity', + headerName: 'Monthly activity', + type: 'custom', + resizable: false, + filterable: false, + sortable: false, + editable: false, + groupable: false, + display: 'flex', + renderCell: renderSparkline, + width: 150, + valueGetter: (value, row) => row.monthlyActivity, + }, + { + field: 'budget', + headerName: 'Budget left', + renderCell: renderProgress, + renderEditCell: renderEditProgress, + availableAggregationFunctions: ['min', 'max', 'avg', 'size'], + type: 'number', + width: 120, + editable: true, + }, + { + field: 'status', + headerName: 'Status', + renderCell: renderStatus, + renderEditCell: renderEditStatus, + type: 'singleSelect', + valueOptions: STATUS_OPTIONS, + width: 150, + editable: true, + }, + { + field: 'incoTerm', + headerName: 'Incoterm', + renderCell: renderIncoterm, + renderEditCell: renderEditIncoterm, + type: 'singleSelect', + valueOptions: INCOTERM_OPTIONS, + editable: true, + }, +]; + +const rows = Array.from({ length: 10 }, (_, index) => ({ + id: index, + name: randomName({}, {}), + avatar: randomColor(), + email: randomEmail(), + rating: randomInt(1, 5), + country: randomArrayItem(COUNTRY_ISO_OPTIONS), + salary: randomInt(35000, 80000), + monthlyActivity: Array.from({ length: 30 }, () => randomInt(1, 25)), + budget: random(0, 1).toPrecision(), + status: randomArrayItem(STATUS_OPTIONS), + incoTerm: randomArrayItem(INCOTERM_OPTIONS), +})); + +export default function CustomColumnFullExample() { + return ( +
+ +
+ ); +} diff --git a/docs/data/data-grid/custom-columns/CustomColumnFullExample.tsx b/docs/data/data-grid/custom-columns/CustomColumnFullExample.tsx new file mode 100644 index 000000000000..2f0fa78b81a4 --- /dev/null +++ b/docs/data/data-grid/custom-columns/CustomColumnFullExample.tsx @@ -0,0 +1,164 @@ +import * as React from 'react'; +import { + randomColor, + randomEmail, + randomInt, + randomName, + randomArrayItem, + random, +} from '@mui/x-data-grid-generator'; +import { + DataGrid, + GridColDef, + gridStringOrNumberComparator, +} from '@mui/x-data-grid'; +import { renderAvatar } from './cell-renderers/avatar'; +import { renderEmail } from './cell-renderers/email'; +import { renderEditRating, renderRating } from './cell-renderers/rating'; +import { + COUNTRY_ISO_OPTIONS, + CountryIsoOption, + renderCountry, + renderEditCountry, +} from './cell-renderers/country'; +import { renderSparkline } from './cell-renderers/sparkline'; +import { renderEditProgress, renderProgress } from './cell-renderers/progress'; +import { + renderEditStatus, + renderStatus, + STATUS_OPTIONS, +} from './cell-renderers/status'; +import { + INCOTERM_OPTIONS, + renderEditIncoterm, + renderIncoterm, +} from './cell-renderers/incoterm'; + +const columns: GridColDef<(typeof rows)[number]>[] = [ + { + field: 'name', + headerName: 'Name', + width: 120, + editable: true, + }, + { + field: 'avatar', + headerName: 'Avatar', + display: 'flex', + renderCell: renderAvatar, + valueGetter: (value, row) => + row.name == null || row.avatar == null + ? null + : { name: row.name, color: row.avatar }, + sortable: false, + filterable: false, + } as GridColDef, + { + field: 'email', + headerName: 'Email', + renderCell: renderEmail, + width: 150, + editable: true, + }, + { + field: 'rating', + headerName: 'Rating', + display: 'flex', + renderCell: renderRating, + renderEditCell: renderEditRating, + width: 180, + type: 'number', + editable: true, + availableAggregationFunctions: ['avg', 'min', 'max', 'size'], + }, + { + field: 'country', + headerName: 'Country', + type: 'singleSelect', + valueOptions: COUNTRY_ISO_OPTIONS, + valueFormatter: (value: CountryIsoOption) => value?.label, + renderCell: renderCountry, + renderEditCell: renderEditCountry, + sortComparator: (v1, v2, param1, param2) => + gridStringOrNumberComparator(v1.label, v2.label, param1, param2), + width: 150, + editable: true, + } as GridColDef, + { + field: 'salary', + headerName: 'Salary', + type: 'number', + valueFormatter: (value?: number) => { + if (!value || typeof value !== 'number') { + return value; + } + return `$${value.toLocaleString()}`; + }, + editable: true, + }, + { + field: 'monthlyActivity', + headerName: 'Monthly activity', + type: 'custom', + resizable: false, + filterable: false, + sortable: false, + editable: false, + groupable: false, + display: 'flex', + renderCell: renderSparkline, + width: 150, + valueGetter: (value, row) => row.monthlyActivity, + }, + { + field: 'budget', + headerName: 'Budget left', + renderCell: renderProgress, + renderEditCell: renderEditProgress, + availableAggregationFunctions: ['min', 'max', 'avg', 'size'], + type: 'number', + width: 120, + editable: true, + }, + { + field: 'status', + headerName: 'Status', + renderCell: renderStatus, + renderEditCell: renderEditStatus, + type: 'singleSelect', + valueOptions: STATUS_OPTIONS, + width: 150, + editable: true, + }, + { + field: 'incoTerm', + headerName: 'Incoterm', + renderCell: renderIncoterm, + renderEditCell: renderEditIncoterm, + type: 'singleSelect', + valueOptions: INCOTERM_OPTIONS, + editable: true, + }, +]; + +const rows = Array.from({ length: 10 }, (_, index) => ({ + id: index, + name: randomName({}, {}), + avatar: randomColor(), + email: randomEmail(), + rating: randomInt(1, 5), + country: randomArrayItem(COUNTRY_ISO_OPTIONS), + salary: randomInt(35000, 80000), + monthlyActivity: Array.from({ length: 30 }, () => randomInt(1, 25)), + budget: random(0, 1).toPrecision(), + status: randomArrayItem(STATUS_OPTIONS), + incoTerm: randomArrayItem(INCOTERM_OPTIONS), +})); + +export default function CustomColumnFullExample() { + return ( +
+ +
+ ); +} diff --git a/docs/data/data-grid/custom-columns/CustomColumnFullExample.tsx.preview b/docs/data/data-grid/custom-columns/CustomColumnFullExample.tsx.preview new file mode 100644 index 000000000000..074afd47c441 --- /dev/null +++ b/docs/data/data-grid/custom-columns/CustomColumnFullExample.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/data-grid/custom-columns/cell-renderers/avatar.js b/docs/data/data-grid/custom-columns/cell-renderers/avatar.js new file mode 100644 index 000000000000..a928435e763c --- /dev/null +++ b/docs/data/data-grid/custom-columns/cell-renderers/avatar.js @@ -0,0 +1,14 @@ +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; + +export function renderAvatar(params) { + if (params.value == null) { + return ''; + } + + return ( + + {params.value.name.toUpperCase().substring(0, 1)} + + ); +} diff --git a/docs/data/data-grid/custom-columns/cell-renderers/avatar.tsx b/docs/data/data-grid/custom-columns/cell-renderers/avatar.tsx new file mode 100644 index 000000000000..e2f1f5b5b1a9 --- /dev/null +++ b/docs/data/data-grid/custom-columns/cell-renderers/avatar.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import { GridRenderCellParams } from '@mui/x-data-grid'; + +export function renderAvatar( + params: GridRenderCellParams<{ name: string; color: string }, any, any>, +) { + if (params.value == null) { + return ''; + } + + return ( + + {params.value.name.toUpperCase().substring(0, 1)} + + ); +} diff --git a/docs/data/data-grid/custom-columns/cell-renderers/country.js b/docs/data/data-grid/custom-columns/cell-renderers/country.js new file mode 100644 index 000000000000..d8dbd277fb67 --- /dev/null +++ b/docs/data/data-grid/custom-columns/cell-renderers/country.js @@ -0,0 +1,130 @@ +import * as React from 'react'; +import { useGridApiContext } from '@mui/x-data-grid'; +import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'; + +import Box from '@mui/material/Box'; +import InputBase from '@mui/material/InputBase'; +import { styled } from '@mui/material/styles'; + +export const COUNTRY_ISO_OPTIONS = [ + { value: 'DE', code: 'DE', label: 'Germany', phone: '49' }, + { value: 'ES', code: 'ES', label: 'Spain', phone: '34' }, + { value: 'FR', code: 'FR', label: 'France', phone: '33' }, + { value: 'GB', code: 'GB', label: 'United Kingdom', phone: '44' }, +]; + +const Country = React.memo(function Country(props) { + const { value } = props; + + return ( + img': { + mr: 0.5, + flexShrink: 0, + width: '20px', + }, + }} + > + + + {value.label} + + + ); +}); + +const StyledAutocomplete = styled(Autocomplete)(({ theme }) => ({ + height: '100%', + [`& .${autocompleteClasses.inputRoot}`]: { + ...theme.typography.body2, + padding: '1px 0', + height: '100%', + '& input': { + padding: '0 16px', + height: '100%', + }, + }, +})); + +function EditCountry(props) { + const { id, value, field } = props; + + const apiRef = useGridApiContext(); + + const handleChange = React.useCallback( + async (event, newValue) => { + await apiRef.current.setEditCellValue({ id, field, value: newValue }, event); + apiRef.current.stopCellEditMode({ id, field }); + }, + [apiRef, field, id], + ); + + return ( + option.label} + autoHighlight + fullWidth + open + disableClearable + renderOption={(optionProps, option) => ( + img': { + mr: 1.5, + flexShrink: 0, + }, + }} + {...optionProps} + key={option.code} + > + + {option.label} + + )} + renderInput={(params) => ( + + )} + /> + ); +} + +export function renderCountry(params) { + if (params.value == null) { + return ''; + } + + return ; +} + +export function renderEditCountry(params) { + return ; +} diff --git a/docs/data/data-grid/custom-columns/cell-renderers/country.tsx b/docs/data/data-grid/custom-columns/cell-renderers/country.tsx new file mode 100644 index 000000000000..54a3a293aac9 --- /dev/null +++ b/docs/data/data-grid/custom-columns/cell-renderers/country.tsx @@ -0,0 +1,152 @@ +import * as React from 'react'; +import { + GridRenderCellParams, + GridRenderEditCellParams, + useGridApiContext, +} from '@mui/x-data-grid'; +import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete'; +import type { AutocompleteProps } from '@mui/material/Autocomplete'; +import Box from '@mui/material/Box'; +import InputBase from '@mui/material/InputBase'; +import { styled } from '@mui/material/styles'; + +export interface CountryIsoOption { + value: string; + code: string; + label: string; + phone: string; + suggested?: boolean; +} + +export const COUNTRY_ISO_OPTIONS: CountryIsoOption[] = [ + { value: 'DE', code: 'DE', label: 'Germany', phone: '49' }, + { value: 'ES', code: 'ES', label: 'Spain', phone: '34' }, + { value: 'FR', code: 'FR', label: 'France', phone: '33' }, + { value: 'GB', code: 'GB', label: 'United Kingdom', phone: '44' }, +]; + +interface CountryProps { + value: CountryIsoOption; +} + +const Country = React.memo(function Country(props: CountryProps) { + const { value } = props; + + return ( + img': { + mr: 0.5, + flexShrink: 0, + width: '20px', + }, + }} + > + + + {value.label} + + + ); +}); + +const StyledAutocomplete = styled(Autocomplete)(({ theme }) => ({ + height: '100%', + [`& .${autocompleteClasses.inputRoot}`]: { + ...theme.typography.body2, + padding: '1px 0', + height: '100%', + '& input': { + padding: '0 16px', + height: '100%', + }, + }, +})) as typeof Autocomplete; + +function EditCountry(props: GridRenderEditCellParams) { + const { id, value, field } = props; + + const apiRef = useGridApiContext(); + + const handleChange = React.useCallback< + NonNullable['onChange']> + >( + async (event, newValue) => { + await apiRef.current.setEditCellValue({ id, field, value: newValue }, event); + apiRef.current.stopCellEditMode({ id, field }); + }, + [apiRef, field, id], + ); + + return ( + + value={value} + onChange={handleChange} + options={COUNTRY_ISO_OPTIONS} + getOptionLabel={(option: any) => option.label} + autoHighlight + fullWidth + open + disableClearable + renderOption={(optionProps, option: any) => ( + img': { + mr: 1.5, + flexShrink: 0, + }, + }} + {...optionProps} + key={option.code} + > + + {option.label} + + )} + renderInput={(params) => ( + + )} + /> + ); +} + +export function renderCountry( + params: GridRenderCellParams, +) { + if (params.value == null) { + return ''; + } + + return ; +} + +export function renderEditCountry( + params: GridRenderEditCellParams, +) { + return ; +} diff --git a/docs/data/data-grid/custom-columns/cell-renderers/email.js b/docs/data/data-grid/custom-columns/cell-renderers/email.js new file mode 100644 index 000000000000..a80f6c387fba --- /dev/null +++ b/docs/data/data-grid/custom-columns/cell-renderers/email.js @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { styled } from '@mui/material/styles'; + +const Link = styled('a')({ + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + overflow: 'hidden', + color: 'inherit', +}); + +const DemoLink = React.memo(function DemoLink(props) { + const handleClick = (event) => { + event.preventDefault(); + event.stopPropagation(); + }; + + return ( + + {props.children} + + ); +}); + +export function renderEmail(params) { + const email = params.value ?? ''; + + return ( + + {email} + + ); +} diff --git a/docs/data/data-grid/custom-columns/cell-renderers/email.tsx b/docs/data/data-grid/custom-columns/cell-renderers/email.tsx new file mode 100644 index 000000000000..e58fc5d78261 --- /dev/null +++ b/docs/data/data-grid/custom-columns/cell-renderers/email.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { styled } from '@mui/material/styles'; +import { GridRenderCellParams } from '@mui/x-data-grid'; + +interface DemoLinkProps { + href: string; + children: string; + tabIndex: number; +} + +const Link = styled('a')({ + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + overflow: 'hidden', + color: 'inherit', +}); + +const DemoLink = React.memo(function DemoLink(props: DemoLinkProps) { + const handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + }; + + return ( + + {props.children} + + ); +}); + +export function renderEmail(params: GridRenderCellParams) { + const email = params.value ?? ''; + + return ( + + {email} + + ); +} diff --git a/docs/data/data-grid/custom-columns/cell-renderers/incoterm.js b/docs/data/data-grid/custom-columns/cell-renderers/incoterm.js new file mode 100644 index 000000000000..524293d92754 --- /dev/null +++ b/docs/data/data-grid/custom-columns/cell-renderers/incoterm.js @@ -0,0 +1,104 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import MenuItem from '@mui/material/MenuItem'; + +import Select from '@mui/material/Select'; +import Tooltip from '@mui/material/Tooltip'; +import InfoIcon from '@mui/icons-material/Info'; +import { useGridApiContext } from '@mui/x-data-grid'; + +export const INCOTERM_OPTIONS = [ + 'EXW (Ex Works)', + 'FAS (Free Alongside Ship)', + 'FCA (Free Carrier)', + 'CPT (Carriage Paid To)', + 'DAP (Delivered at Place)', + 'DPU (Delivered at Place Unloaded)', + 'DDP (Delivered Duty Paid)', +]; + +const Incoterm = React.memo(function Incoterm(props) { + const { value } = props; + + if (!value) { + return null; + } + + const valueStr = value.toString(); + const tooltip = valueStr.slice(valueStr.indexOf('(') + 1, valueStr.indexOf(')')); + const code = valueStr.slice(0, valueStr.indexOf('(')).trim(); + + return ( + + {code} + + + + + ); +}); + +function EditIncoterm(props) { + const { id, value, field } = props; + + const apiRef = useGridApiContext(); + + const handleChange = async (event) => { + await apiRef.current.setEditCellValue( + { id, field, value: event.target.value }, + event, + ); + apiRef.current.stopCellEditMode({ id, field }); + }; + + const handleClose = (event, reason) => { + if (reason === 'backdropClick') { + apiRef.current.stopCellEditMode({ id, field }); + } + }; + + return ( + + ); +} + +export function renderIncoterm(params) { + return ; +} + +export function renderEditIncoterm(params) { + return ; +} diff --git a/docs/data/data-grid/custom-columns/cell-renderers/incoterm.tsx b/docs/data/data-grid/custom-columns/cell-renderers/incoterm.tsx new file mode 100644 index 000000000000..de90756039bf --- /dev/null +++ b/docs/data/data-grid/custom-columns/cell-renderers/incoterm.tsx @@ -0,0 +1,116 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import MenuItem from '@mui/material/MenuItem'; +import type { MenuProps } from '@mui/material/Menu'; +import Select, { SelectProps } from '@mui/material/Select'; +import Tooltip from '@mui/material/Tooltip'; +import InfoIcon from '@mui/icons-material/Info'; +import { + GridRenderCellParams, + GridRenderEditCellParams, + useGridApiContext, +} from '@mui/x-data-grid'; + +export const INCOTERM_OPTIONS = [ + 'EXW (Ex Works)', + 'FAS (Free Alongside Ship)', + 'FCA (Free Carrier)', + 'CPT (Carriage Paid To)', + 'DAP (Delivered at Place)', + 'DPU (Delivered at Place Unloaded)', + 'DDP (Delivered Duty Paid)', +]; + +interface IncotermProps { + value: string | null | undefined; +} + +const Incoterm = React.memo(function Incoterm(props: IncotermProps) { + const { value } = props; + + if (!value) { + return null; + } + + const valueStr = value.toString(); + const tooltip = valueStr.slice(valueStr.indexOf('(') + 1, valueStr.indexOf(')')); + const code = valueStr.slice(0, valueStr.indexOf('(')).trim(); + + return ( + + {code} + + + + + ); +}); + +function EditIncoterm(props: GridRenderEditCellParams) { + const { id, value, field } = props; + + const apiRef = useGridApiContext(); + + const handleChange: SelectProps['onChange'] = async (event) => { + await apiRef.current.setEditCellValue( + { id, field, value: event.target.value as any }, + event, + ); + apiRef.current.stopCellEditMode({ id, field }); + }; + + const handleClose: MenuProps['onClose'] = (event, reason) => { + if (reason === 'backdropClick') { + apiRef.current.stopCellEditMode({ id, field }); + } + }; + + return ( + + ); +} + +export function renderIncoterm( + params: GridRenderCellParams, +) { + return ; +} + +export function renderEditIncoterm( + params: GridRenderEditCellParams, +) { + return ; +} diff --git a/docs/data/data-grid/custom-columns/cell-renderers/progress.js b/docs/data/data-grid/custom-columns/cell-renderers/progress.js new file mode 100644 index 000000000000..d604aaca719a --- /dev/null +++ b/docs/data/data-grid/custom-columns/cell-renderers/progress.js @@ -0,0 +1,178 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { useGridApiContext } from '@mui/x-data-grid'; +import { alpha, styled } from '@mui/material/styles'; +import Slider, { sliderClasses } from '@mui/material/Slider'; +import Tooltip from '@mui/material/Tooltip'; +import { debounce } from '@mui/material/utils'; + +const Center = styled('div')({ + height: '100%', + display: 'flex', + alignItems: 'center', +}); + +const Element = styled('div')(({ theme }) => ({ + border: `1px solid ${(theme.vars || theme).palette.divider}`, + position: 'relative', + overflow: 'hidden', + width: '100%', + height: 26, + borderRadius: 2, +})); + +const Value = styled('div')({ + position: 'absolute', + lineHeight: '24px', + width: '100%', + display: 'flex', + justifyContent: 'center', +}); + +const Bar = styled('div')({ + height: '100%', + '&.low': { + backgroundColor: '#f44336', + }, + '&.medium': { + backgroundColor: '#efbb5aa3', + }, + '&.high': { + backgroundColor: '#088208a3', + }, +}); + +const ProgressBar = React.memo(function ProgressBar(props) { + const { value } = props; + const valueInPercent = value * 100; + + return ( + + {`${valueInPercent.toLocaleString()} %`} + = 30 && valueInPercent <= 70, + high: valueInPercent > 70, + })} + style={{ maxWidth: `${valueInPercent}%` }} + /> + + ); +}); + +const StyledSlider = styled(Slider)(({ theme }) => ({ + display: 'flex', + height: '100%', + width: '100%', + alignItems: 'center', + justifyContent: 'center', + padding: 0, + borderRadius: 0, + [`& .${sliderClasses.rail}`]: { + height: '100%', + backgroundColor: 'transparent', + }, + [`& .${sliderClasses.track}`]: { + height: '100%', + transition: theme.transitions.create('background-color', { + duration: theme.transitions.duration.shorter, + }), + '&.low': { + backgroundColor: '#f44336', + }, + '&.medium': { + backgroundColor: '#efbb5aa3', + }, + '&.high': { + backgroundColor: '#088208a3', + }, + }, + [`& .${sliderClasses.thumb}`]: { + height: '100%', + width: 5, + borderRadius: 0, + marginTop: 0, + backgroundColor: alpha('#000000', 0.2), + }, +})); + +const ValueLabelComponent = React.memo(function ValueLabelComponent(props) { + const { children, open, value } = props; + + return ( + + {children} + + ); +}); + +function EditProgress(props) { + const { id, value, field } = props; + const [valueState, setValueState] = React.useState(Number(value)); + + const apiRef = useGridApiContext(); + + const updateCellEditProps = React.useCallback( + (newValue) => { + apiRef.current.setEditCellValue({ id, field, value: newValue }); + }, + [apiRef, field, id], + ); + + const debouncedUpdateCellEditProps = React.useMemo( + () => debounce(updateCellEditProps, 60), + [updateCellEditProps], + ); + + const handleChange = (event, newValue) => { + setValueState(newValue); + debouncedUpdateCellEditProps(newValue); + }; + + React.useEffect(() => { + setValueState(Number(value)); + }, [value]); + + const handleRef = (element) => { + if (element) { + element.querySelector('[type="range"]').focus(); + } + }; + + return ( + = 0.3 && valueState <= 0.7, + high: valueState > 0.7, + }), + }} + value={valueState} + max={1} + step={0.00001} + onChange={handleChange} + components={{ ValueLabel: ValueLabelComponent }} + valueLabelDisplay="auto" + valueLabelFormat={(newValue) => `${(newValue * 100).toLocaleString()} %`} + /> + ); +} + +export function renderProgress(params) { + if (params.value == null) { + return ''; + } + + return ( +
+ +
+ ); +} + +export function renderEditProgress(params) { + return ; +} diff --git a/docs/data/data-grid/custom-columns/cell-renderers/progress.tsx b/docs/data/data-grid/custom-columns/cell-renderers/progress.tsx new file mode 100644 index 000000000000..a831d003ff58 --- /dev/null +++ b/docs/data/data-grid/custom-columns/cell-renderers/progress.tsx @@ -0,0 +1,186 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { + GridRenderCellParams, + GridRenderEditCellParams, + useGridApiContext, +} from '@mui/x-data-grid'; +import { alpha, styled } from '@mui/material/styles'; +import Slider, { sliderClasses, SliderProps } from '@mui/material/Slider'; +import Tooltip from '@mui/material/Tooltip'; +import { debounce } from '@mui/material/utils'; + +interface ProgressBarProps { + value: number; +} + +const Center = styled('div')({ + height: '100%', + display: 'flex', + alignItems: 'center', +}); + +const Element = styled('div')(({ theme }) => ({ + border: `1px solid ${(theme.vars || theme).palette.divider}`, + position: 'relative', + overflow: 'hidden', + width: '100%', + height: 26, + borderRadius: 2, +})); + +const Value = styled('div')({ + position: 'absolute', + lineHeight: '24px', + width: '100%', + display: 'flex', + justifyContent: 'center', +}); + +const Bar = styled('div')({ + height: '100%', + '&.low': { + backgroundColor: '#f44336', + }, + '&.medium': { + backgroundColor: '#efbb5aa3', + }, + '&.high': { + backgroundColor: '#088208a3', + }, +}); + +const ProgressBar = React.memo(function ProgressBar(props: ProgressBarProps) { + const { value } = props; + const valueInPercent = value * 100; + + return ( + + {`${valueInPercent.toLocaleString()} %`} + = 30 && valueInPercent <= 70, + high: valueInPercent > 70, + })} + style={{ maxWidth: `${valueInPercent}%` }} + /> + + ); +}); + +const StyledSlider = styled(Slider)(({ theme }) => ({ + display: 'flex', + height: '100%', + width: '100%', + alignItems: 'center', + justifyContent: 'center', + padding: 0, + borderRadius: 0, + [`& .${sliderClasses.rail}`]: { + height: '100%', + backgroundColor: 'transparent', + }, + [`& .${sliderClasses.track}`]: { + height: '100%', + transition: theme.transitions.create('background-color', { + duration: theme.transitions.duration.shorter, + }), + '&.low': { + backgroundColor: '#f44336', + }, + '&.medium': { + backgroundColor: '#efbb5aa3', + }, + '&.high': { + backgroundColor: '#088208a3', + }, + }, + [`& .${sliderClasses.thumb}`]: { + height: '100%', + width: 5, + borderRadius: 0, + marginTop: 0, + backgroundColor: alpha('#000000', 0.2), + }, +})); + +const ValueLabelComponent = React.memo(function ValueLabelComponent(props: any) { + const { children, open, value } = props; + + return ( + + {children} + + ); +}); + +function EditProgress(props: GridRenderEditCellParams) { + const { id, value, field } = props; + const [valueState, setValueState] = React.useState(Number(value)); + + const apiRef = useGridApiContext(); + + const updateCellEditProps = React.useCallback( + (newValue: number) => { + apiRef.current.setEditCellValue({ id, field, value: newValue }); + }, + [apiRef, field, id], + ); + + const debouncedUpdateCellEditProps = React.useMemo( + () => debounce(updateCellEditProps, 60), + [updateCellEditProps], + ); + + const handleChange = (event: Event, newValue: number | number[]) => { + setValueState(newValue as number); + debouncedUpdateCellEditProps(newValue as number); + }; + + React.useEffect(() => { + setValueState(Number(value)); + }, [value]); + + const handleRef: SliderProps['ref'] = (element) => { + if (element) { + element.querySelector('[type="range"]')!.focus(); + } + }; + + return ( + = 0.3 && valueState <= 0.7, + high: valueState > 0.7, + }), + }} + value={valueState} + max={1} + step={0.00001} + onChange={handleChange} + components={{ ValueLabel: ValueLabelComponent }} + valueLabelDisplay="auto" + valueLabelFormat={(newValue) => `${(newValue * 100).toLocaleString()} %`} + /> + ); +} + +export function renderProgress(params: GridRenderCellParams) { + if (params.value == null) { + return ''; + } + + return ( +
+ +
+ ); +} + +export function renderEditProgress(params: GridRenderEditCellParams) { + return ; +} diff --git a/docs/data/data-grid/custom-columns/cell-renderers/rating.js b/docs/data/data-grid/custom-columns/cell-renderers/rating.js new file mode 100644 index 000000000000..46d1d09ea751 --- /dev/null +++ b/docs/data/data-grid/custom-columns/cell-renderers/rating.js @@ -0,0 +1,93 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Rating from '@mui/material/Rating'; +import { useGridApiContext } from '@mui/x-data-grid'; + +const RatingValue = React.memo(function RatingValue(props) { + const { value } = props; + return ( + + {' '} + {Math.round(Number(value) * 10) / 10} + + ); +}); + +function EditRating(props) { + const { id, value, field } = props; + + const apiRef = useGridApiContext(); + + const changedThroughKeyboard = React.useRef(false); + + const handleChange = async (event) => { + await apiRef.current.setEditCellValue( + { id, field, value: Number(event.target.value) }, + event, + ); + if (!changedThroughKeyboard.current) { + apiRef.current.stopCellEditMode({ id, field }); + } + changedThroughKeyboard.current = false; + }; + + const handleRef = (element) => { + if (element) { + if (value !== 0) { + element.querySelector(`input[value="${value}"]`).focus(); + } else { + element.querySelector('input[value=""]').focus(); + } + } + }; + + const handleKeyDown = (event) => { + if (event.key.startsWith('Arrow')) { + changedThroughKeyboard.current = true; + } else { + changedThroughKeyboard.current = false; + } + }; + + return ( + + + {Number(value)} + + ); +} + +export function renderRating(params) { + if (params.value == null) { + return ''; + } + + return ; +} + +export function renderEditRating(params) { + return ; +} diff --git a/docs/data/data-grid/custom-columns/cell-renderers/rating.tsx b/docs/data/data-grid/custom-columns/cell-renderers/rating.tsx new file mode 100644 index 000000000000..9eb9c68ce767 --- /dev/null +++ b/docs/data/data-grid/custom-columns/cell-renderers/rating.tsx @@ -0,0 +1,101 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Rating from '@mui/material/Rating'; +import { + GridRenderCellParams, + GridRenderEditCellParams, + useGridApiContext, +} from '@mui/x-data-grid'; + +interface RatingValueProps { + value: number; +} + +const RatingValue = React.memo(function RatingValue(props: RatingValueProps) { + const { value } = props; + return ( + + {' '} + {Math.round(Number(value) * 10) / 10} + + ); +}); + +function EditRating(props: GridRenderEditCellParams) { + const { id, value, field } = props; + + const apiRef = useGridApiContext(); + + const changedThroughKeyboard = React.useRef(false); + + const handleChange = async (event: any) => { + await apiRef.current.setEditCellValue( + { id, field, value: Number(event.target.value) }, + event, + ); + if (!changedThroughKeyboard.current) { + apiRef.current.stopCellEditMode({ id, field }); + } + changedThroughKeyboard.current = false; + }; + + const handleRef = (element: HTMLElement | undefined) => { + if (element) { + if (value !== 0) { + element.querySelector(`input[value="${value}"]`)!.focus(); + } else { + element.querySelector('input[value=""]')!.focus(); + } + } + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key.startsWith('Arrow')) { + changedThroughKeyboard.current = true; + } else { + changedThroughKeyboard.current = false; + } + }; + + return ( + + + {Number(value)} + + ); +} + +export function renderRating(params: GridRenderCellParams) { + if (params.value == null) { + return ''; + } + + return ; +} + +export function renderEditRating(params: GridRenderEditCellParams) { + return ; +} diff --git a/docs/data/data-grid/custom-columns/cell-renderers/sparkline.js b/docs/data/data-grid/custom-columns/cell-renderers/sparkline.js new file mode 100644 index 000000000000..b6b083cf7b54 --- /dev/null +++ b/docs/data/data-grid/custom-columns/cell-renderers/sparkline.js @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { SparkLineChart } from '@mui/x-charts/SparkLineChart'; + +export function renderSparkline(params) { + if (params.value == null) { + return ''; + } + + return ( + + ); +} diff --git a/docs/data/data-grid/custom-columns/cell-renderers/sparkline.tsx b/docs/data/data-grid/custom-columns/cell-renderers/sparkline.tsx new file mode 100644 index 000000000000..c5e44fa6c885 --- /dev/null +++ b/docs/data/data-grid/custom-columns/cell-renderers/sparkline.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { SparkLineChart } from '@mui/x-charts/SparkLineChart'; +import { GridRenderCellParams } from '@mui/x-data-grid'; + +export function renderSparkline(params: GridRenderCellParams) { + if (params.value == null) { + return ''; + } + + return ( + + ); +} diff --git a/docs/data/data-grid/custom-columns/cell-renderers/status.js b/docs/data/data-grid/custom-columns/cell-renderers/status.js new file mode 100644 index 000000000000..f67ec6746c92 --- /dev/null +++ b/docs/data/data-grid/custom-columns/cell-renderers/status.js @@ -0,0 +1,156 @@ +import * as React from 'react'; +import Chip from '@mui/material/Chip'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import MenuItem from '@mui/material/MenuItem'; + +import Select from '@mui/material/Select'; +import { styled } from '@mui/material/styles'; +import ReportProblemIcon from '@mui/icons-material/ReportProblem'; +import InfoIcon from '@mui/icons-material/Info'; +import AutorenewIcon from '@mui/icons-material/Autorenew'; +import DoneIcon from '@mui/icons-material/Done'; +import { + GridEditModes, + useGridApiContext, + useGridRootProps, +} from '@mui/x-data-grid'; + +export const STATUS_OPTIONS = ['Open', 'PartiallyFilled', 'Filled', 'Rejected']; + +const StyledChip = styled(Chip)(({ theme }) => ({ + justifyContent: 'left', + '& .icon': { + color: 'inherit', + }, + '&.Open': { + color: (theme.vars || theme).palette.info.dark, + border: `1px solid ${(theme.vars || theme).palette.info.main}`, + }, + '&.Filled': { + color: (theme.vars || theme).palette.success.dark, + border: `1px solid ${(theme.vars || theme).palette.success.main}`, + }, + '&.PartiallyFilled': { + color: (theme.vars || theme).palette.warning.dark, + border: `1px solid ${(theme.vars || theme).palette.warning.main}`, + }, + '&.Rejected': { + color: (theme.vars || theme).palette.error.dark, + border: `1px solid ${(theme.vars || theme).palette.error.main}`, + }, +})); + +const Status = React.memo((props) => { + const { status } = props; + + let icon = null; + if (status === 'Rejected') { + icon = ; + } else if (status === 'Open') { + icon = ; + } else if (status === 'PartiallyFilled') { + icon = ; + } else if (status === 'Filled') { + icon = ; + } + + let label = status; + if (status === 'PartiallyFilled') { + label = 'Partially Filled'; + } + + return ( + + ); +}); + +function EditStatus(props) { + const { id, value, field } = props; + const rootProps = useGridRootProps(); + const apiRef = useGridApiContext(); + + const handleChange = async (event) => { + const isValid = await apiRef.current.setEditCellValue({ + id, + field, + value: event.target.value, + }); + + if (isValid && rootProps.editMode === GridEditModes.Cell) { + apiRef.current.stopCellEditMode({ id, field, cellToFocusAfter: 'below' }); + } + }; + + const handleClose = (event, reason) => { + if (reason === 'backdropClick') { + apiRef.current.stopCellEditMode({ id, field, ignoreModifications: true }); + } + }; + + return ( + + ); +} + +export function renderStatus(params) { + if (params.value == null) { + return ''; + } + + return ; +} + +export function renderEditStatus(params) { + return ; +} diff --git a/docs/data/data-grid/custom-columns/cell-renderers/status.tsx b/docs/data/data-grid/custom-columns/cell-renderers/status.tsx new file mode 100644 index 000000000000..85bb29b22699 --- /dev/null +++ b/docs/data/data-grid/custom-columns/cell-renderers/status.tsx @@ -0,0 +1,162 @@ +import * as React from 'react'; +import Chip from '@mui/material/Chip'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import MenuItem from '@mui/material/MenuItem'; +import type { MenuProps } from '@mui/material/Menu'; +import Select, { SelectProps } from '@mui/material/Select'; +import { styled } from '@mui/material/styles'; +import ReportProblemIcon from '@mui/icons-material/ReportProblem'; +import InfoIcon from '@mui/icons-material/Info'; +import AutorenewIcon from '@mui/icons-material/Autorenew'; +import DoneIcon from '@mui/icons-material/Done'; +import { + GridEditModes, + GridRenderCellParams, + GridRenderEditCellParams, + useGridApiContext, + useGridRootProps, +} from '@mui/x-data-grid'; + +export const STATUS_OPTIONS = ['Open', 'PartiallyFilled', 'Filled', 'Rejected']; + +interface StatusProps { + status: string; +} + +const StyledChip = styled(Chip)(({ theme }) => ({ + justifyContent: 'left', + '& .icon': { + color: 'inherit', + }, + '&.Open': { + color: (theme.vars || theme).palette.info.dark, + border: `1px solid ${(theme.vars || theme).palette.info.main}`, + }, + '&.Filled': { + color: (theme.vars || theme).palette.success.dark, + border: `1px solid ${(theme.vars || theme).palette.success.main}`, + }, + '&.PartiallyFilled': { + color: (theme.vars || theme).palette.warning.dark, + border: `1px solid ${(theme.vars || theme).palette.warning.main}`, + }, + '&.Rejected': { + color: (theme.vars || theme).palette.error.dark, + border: `1px solid ${(theme.vars || theme).palette.error.main}`, + }, +})); + +const Status = React.memo((props: StatusProps) => { + const { status } = props; + + let icon: any = null; + if (status === 'Rejected') { + icon = ; + } else if (status === 'Open') { + icon = ; + } else if (status === 'PartiallyFilled') { + icon = ; + } else if (status === 'Filled') { + icon = ; + } + + let label: string = status; + if (status === 'PartiallyFilled') { + label = 'Partially Filled'; + } + + return ( + + ); +}); + +function EditStatus(props: GridRenderEditCellParams) { + const { id, value, field } = props; + const rootProps = useGridRootProps(); + const apiRef = useGridApiContext(); + + const handleChange: SelectProps['onChange'] = async (event) => { + const isValid = await apiRef.current.setEditCellValue({ + id, + field, + value: event.target.value, + }); + + if (isValid && rootProps.editMode === GridEditModes.Cell) { + apiRef.current.stopCellEditMode({ id, field, cellToFocusAfter: 'below' }); + } + }; + + const handleClose: MenuProps['onClose'] = (event, reason) => { + if (reason === 'backdropClick') { + apiRef.current.stopCellEditMode({ id, field, ignoreModifications: true }); + } + }; + + return ( + + ); +} + +export function renderStatus(params: GridRenderCellParams) { + if (params.value == null) { + return ''; + } + + return ; +} + +export function renderEditStatus(params: GridRenderEditCellParams) { + return ; +} diff --git a/docs/data/data-grid/custom-columns/custom-columns.md b/docs/data/data-grid/custom-columns/custom-columns.md index 118a6905b663..ebabc45e057f 100644 --- a/docs/data/data-grid/custom-columns/custom-columns.md +++ b/docs/data/data-grid/custom-columns/custom-columns.md @@ -44,6 +44,18 @@ You can change the date format by importing different locale (`en-US` locale is See [Localization](/x/react-date-pickers/localization/) for more information. ::: +## Full example + +The demo below shows the most common custom column renderers used across our demos. + +:::success +You can copy the column definitions and custom cell renderers from the demo source code. + +All column definitions are located in the main component file, while each cell renderer is in a separate file. +::: + +{{"demo": "CustomColumnFullExample.js", "bg": "inline"}} + ## API - [DataGrid](/x/api/data-grid/data-grid/) diff --git a/docs/data/data-grid/faq/faq.md b/docs/data/data-grid/faq/faq.md index 7c3d17e7c94b..2aec0da7e174 100644 --- a/docs/data/data-grid/faq/faq.md +++ b/docs/data/data-grid/faq/faq.md @@ -124,7 +124,14 @@ It only impacts the rendering part and does not impact the internal calculations ## What is the purpose of useDemoData hook used in the documentation examples? The `useDemoData` hook is a utility hook from the `@mui/x-data-grid-generator` package. -It generates random data for the Data Grid. It is often used in documentation examples to provide realistic data without polluting the code with data generation logic. +It contains columns definitions and generates random data for the Data Grid. +It is often used in our demos to provide realistic data without polluting the code with data generation logic. + +:::success +Looking for the column definitions and custom cell renderers from the `useDemoData` hook? + +Check out the [Custom columns demo](/x/react-data-grid/custom-columns/#full-example) where you can copy them from the demo source code. +::: Here's how it's used: diff --git a/packages/x-data-grid-generator/src/columns/commodities.columns.tsx b/packages/x-data-grid-generator/src/columns/commodities.columns.tsx index 60c9f269b87f..56f6f1e9528a 100644 --- a/packages/x-data-grid-generator/src/columns/commodities.columns.tsx +++ b/packages/x-data-grid-generator/src/columns/commodities.columns.tsx @@ -174,6 +174,7 @@ export const getCommodityColumns = (editable = false): GridColDefGenerator[] => }, { field: 'incoTerm', + headerName: 'Incoterm', generateData: randomIncoterm, renderCell: renderIncoterm, renderEditCell: renderEditIncoterm, diff --git a/packages/x-data-grid-generator/src/renderer/renderEditCountry.tsx b/packages/x-data-grid-generator/src/renderer/renderEditCountry.tsx index f963d73b6563..52157b6a69ea 100644 --- a/packages/x-data-grid-generator/src/renderer/renderEditCountry.tsx +++ b/packages/x-data-grid-generator/src/renderer/renderEditCountry.tsx @@ -54,6 +54,7 @@ function EditCountry(props: GridRenderEditCellParams) { }, }} {...optionProps} + key={option.code} >