diff --git a/docs/data/experiments/renderers/renderAvatar.js b/docs/data/experiments/renderers/renderAvatar.js new file mode 100644 index 00000000000000..ce091a483518ba --- /dev/null +++ b/docs/data/experiments/renderers/renderAvatar.js @@ -0,0 +1,16 @@ +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)} + + ); +} + +export default renderAvatar; diff --git a/docs/data/experiments/renderers/renderCountry.js b/docs/data/experiments/renderers/renderCountry.js new file mode 100644 index 00000000000000..4c6ef30fe4ebdd --- /dev/null +++ b/docs/data/experiments/renderers/renderCountry.js @@ -0,0 +1,129 @@ +import * as React from 'react'; +import { useGridApiContext } from '@mui/x-data-grid'; +// eslint-disable-next-line no-restricted-imports +import { COUNTRY_ISO_OPTIONS } from '@mui/x-data-grid-generator/services/static-data'; +import { + Autocomplete, + autocompleteClasses, + Box, + InputBase, + styled, +} from '@mui/material'; + +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 ; +} + +export default renderCountry; diff --git a/docs/data/experiments/renderers/renderEmail.js b/docs/data/experiments/renderers/renderEmail.js new file mode 100644 index 00000000000000..5c378c1d8c9fd7 --- /dev/null +++ b/docs/data/experiments/renderers/renderEmail.js @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { styled } from '@mui/material'; + +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} + + ); +} + +export default renderEmail; diff --git a/docs/data/experiments/renderers/renderIncoterm.js b/docs/data/experiments/renderers/renderIncoterm.js new file mode 100644 index 00000000000000..2508acf098c90b --- /dev/null +++ b/docs/data/experiments/renderers/renderIncoterm.js @@ -0,0 +1,99 @@ +import * as React from 'react'; +import { + Box, + ListItemIcon, + ListItemText, + MenuItem, + Select, + Tooltip, +} from '@mui/material'; +import InfoIcon from '@mui/icons-material/Info'; +import { useGridApiContext } from '@mui/x-data-grid'; +// eslint-disable-next-line no-restricted-imports +import { INCOTERM_OPTIONS } from '@mui/x-data-grid-generator/services/static-data'; + +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 ; +} + +export default renderIncoterm; diff --git a/docs/data/experiments/renderers/renderProgress.js b/docs/data/experiments/renderers/renderProgress.js new file mode 100644 index 00000000000000..63ae5a4170deba --- /dev/null +++ b/docs/data/experiments/renderers/renderProgress.js @@ -0,0 +1,184 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { useGridApiContext } from '@mui/x-data-grid'; +import { + alpha, + debounce, + Slider, + sliderClasses, + styled, + Tooltip, +} from '@mui/material'; + +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 ; +} + +export default renderProgress; diff --git a/docs/data/experiments/renderers/renderRating.js b/docs/data/experiments/renderers/renderRating.js new file mode 100644 index 00000000000000..18db1c9b565b93 --- /dev/null +++ b/docs/data/experiments/renderers/renderRating.js @@ -0,0 +1,94 @@ +import * as React from 'react'; +import { Box, Rating } from '@mui/material'; +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 ; +} + +export default renderRating; diff --git a/docs/data/experiments/renderers/renderSparkline.js b/docs/data/experiments/renderers/renderSparkline.js new file mode 100644 index 00000000000000..d8d32790ef141d --- /dev/null +++ b/docs/data/experiments/renderers/renderSparkline.js @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { SparkLineChart } from '@mui/x-charts/SparkLineChart'; + +export function renderSparkline(params) { + if (params.value == null) { + return ''; + } + + return ( + + ); +} + +export default renderSparkline; diff --git a/docs/data/experiments/renderers/renderStatus.js b/docs/data/experiments/renderers/renderStatus.js new file mode 100644 index 00000000000000..9a4bd5dc85138c --- /dev/null +++ b/docs/data/experiments/renderers/renderStatus.js @@ -0,0 +1,159 @@ +import * as React from 'react'; +import { + Chip, + ListItemIcon, + ListItemText, + MenuItem, + Select, + styled, +} from '@mui/material'; +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'; +// eslint-disable-next-line no-restricted-imports +import { STATUS_OPTIONS } from '@mui/x-data-grid-generator/services/static-data'; + +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 ; +} + +export default renderStatus; diff --git a/docs/pages/experiments/docs/DemoMultiTabs.js b/docs/pages/experiments/docs/DemoMultiTabs.js index 77f396e925cec7..be6fd9e453485b 100644 --- a/docs/pages/experiments/docs/DemoMultiTabs.js +++ b/docs/pages/experiments/docs/DemoMultiTabs.js @@ -1,37 +1,163 @@ import * as React from 'react'; -import { BarChart } from '@mui/x-charts/BarChart'; -import { axisClasses } from '@mui/x-charts'; -import dataset from './dataset.json'; +import { + generateFilledQuantity, + randomColor, + randomCountry, + randomEmail, + randomIncoterm, + randomInt, + randomName, + randomRating, + randomStatusOptions, +} from '@mui/x-data-grid-generator'; +// eslint-disable-next-line no-restricted-imports +import { + COUNTRY_ISO_OPTIONS_SORTED, + INCOTERM_OPTIONS, + STATUS_OPTIONS, +} from '@mui/x-data-grid-generator/services/static-data'; +import { DataGrid, gridStringOrNumberComparator } from '@mui/x-data-grid'; +import { renderAvatar } from '../../../data/experiments/renderers/renderAvatar'; +import { renderEmail } from '../../../data/experiments/renderers/renderEmail'; +import { renderEditRating, renderRating } from '../../../data/experiments/renderers/renderRating'; +import { + renderCountry, + renderEditCountry, +} from '../../../data/experiments/renderers/renderCountry'; +import { renderSparkline } from '../../../data/experiments/renderers/renderSparkline'; +import { + renderEditProgress, + renderProgress, +} from '../../../data/experiments/renderers/renderProgress'; +import { renderEditStatus, renderStatus } from '../../../data/experiments/renderers/renderStatus'; +import { + renderEditIncoterm, + renderIncoterm, +} from '../../../data/experiments/renderers/renderIncoterm'; -const chartSetting = { - yAxis: [ - { - label: 'rainfall (mm)', - }, - ], - width: 500, - height: 300, - sx: { - [`.${axisClasses.left} .${axisClasses.label}`]: { - transform: 'translate(-20px, 0)', +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_SORTED, + 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 valueFormatter = (value) => `${value}mm`; +const rows = Array.from({ length: 10 }, (_, index) => ({ + id: index, + name: randomName({}, {}), + avatar: randomColor(), + email: randomEmail(), + rating: randomRating(), + country: randomCountry(), + salary: randomInt(35000, 80000), + monthlyActivity: Array.from({ length: 30 }, () => randomInt(1, 25)), + budget: generateFilledQuantity({ quantity: 100 }), + status: randomStatusOptions(), + incoTerm: randomIncoterm(), +})); -export default function BarsDataset() { +export default function CustomColumnFullExample() { return ( - +
+ +
); } diff --git a/docs/pages/experiments/docs/dataset.json b/docs/pages/experiments/docs/dataset.json deleted file mode 100644 index c3c44c6bd91174..00000000000000 --- a/docs/pages/experiments/docs/dataset.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "data": [ - { - "london": 59, - "paris": 57, - "newYork": 86, - "seoul": 21, - "month": "Jan" - }, - { - "london": 50, - "paris": 52, - "newYork": 78, - "seoul": 28, - "month": "Fev" - }, - { - "london": 47, - "paris": 53, - "newYork": 106, - "seoul": 41, - "month": "Mar" - }, - { - "london": 54, - "paris": 56, - "newYork": 92, - "seoul": 73, - "month": "Apr" - }, - { - "london": 57, - "paris": 69, - "newYork": 92, - "seoul": 99, - "month": "May" - }, - { - "london": 60, - "paris": 63, - "newYork": 103, - "seoul": 144, - "month": "June" - }, - { - "london": 59, - "paris": 60, - "newYork": 105, - "seoul": 319, - "month": "July" - }, - { - "london": 65, - "paris": 60, - "newYork": 106, - "seoul": 249, - "month": "Aug" - }, - { - "london": 51, - "paris": 51, - "newYork": 95, - "seoul": 131, - "month": "Sept" - }, - { - "london": 60, - "paris": 65, - "newYork": 97, - "seoul": 55, - "month": "Oct" - }, - { - "london": 67, - "paris": 64, - "newYork": 76, - "seoul": 48, - "month": "Nov" - }, - { - "london": 61, - "paris": 70, - "newYork": 103, - "seoul": 25, - "month": "Dec" - } - ] -} diff --git a/docs/pages/experiments/docs/demos.md b/docs/pages/experiments/docs/demos.md index 43d1c4e0a3e655..541b45180b9dd7 100644 --- a/docs/pages/experiments/docs/demos.md +++ b/docs/pages/experiments/docs/demos.md @@ -39,4 +39,4 @@ So, it renders the "outlined" background variant. ## Multiple Tabs demo -{{"demo": "DemoMultiTabs.js" }} +{{"demo": "DemoMultiTabs.js", "bg": "inline" }} diff --git a/packages/mui-docs/src/HighlightedCodeWithTabs/HighlightedCodeWithTabs.tsx b/packages/mui-docs/src/HighlightedCodeWithTabs/HighlightedCodeWithTabs.tsx index 8e6f64b2bd82b2..84904bfdda33e7 100644 --- a/packages/mui-docs/src/HighlightedCodeWithTabs/HighlightedCodeWithTabs.tsx +++ b/packages/mui-docs/src/HighlightedCodeWithTabs/HighlightedCodeWithTabs.tsx @@ -14,6 +14,7 @@ export const CodeTabList = styled(TabsListBase)<{ gap: theme.spacing(0.5), borderLeft: '1px solid', borderRight: '1px solid', + overflowX: 'auto', ...theme.applyDarkStyles({ backgroundColor: alpha(theme.palette.primaryDark[800], 0.2), }), @@ -258,6 +259,7 @@ export const CodeTab = styled(TabBase)<{ ownerState: { mounted: boolean; contain }, ], ...theme.unstable_sx({ + flex: '0 0 auto', height: 26, p: '2px 8px', bgcolor: 'transparent',