diff --git a/docs/data/charts/components/HtmlLegend.tsx b/docs/data/charts/components/HtmlLegend.tsx
index a58701b379a8f..b716c8e433055 100644
--- a/docs/data/charts/components/HtmlLegend.tsx
+++ b/docs/data/charts/components/HtmlLegend.tsx
@@ -1,6 +1,6 @@
import * as React from 'react';
import Box from '@mui/material/Box';
-import { unstable_useBarSeries } from '@mui/x-charts/hooks';
+import { useLegend } from '@mui/x-charts/hooks';
import { ChartDataProvider } from '@mui/x-charts/context';
import { ChartsSurface } from '@mui/x-charts/ChartsSurface';
import { BarPlot } from '@mui/x-charts/BarChart';
@@ -8,7 +8,7 @@ import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis';
import { ChartsYAxis } from '@mui/x-charts/ChartsYAxis';
function MyCustomLegend() {
- const s = unstable_useBarSeries();
+ const { items } = useLegend();
return (
- {Object.values(s?.series ?? []).map((v) => {
+ {items.map((v) => {
return (
diff --git a/docs/data/charts/components/ScaleDemo.tsx b/docs/data/charts/components/ScaleDemo.tsx
index 14837a6eb2e5d..094fce069f0a1 100644
--- a/docs/data/charts/components/ScaleDemo.tsx
+++ b/docs/data/charts/components/ScaleDemo.tsx
@@ -21,7 +21,7 @@ const StyledText = styled('text')(({ theme }) => ({
shapeRendering: 'crispEdges',
}));
-function ValueHighlight(props: { svgRef: React.RefObject }) {
+function ValueHighlight(props: { svgRef: React.RefObject }) {
const { svgRef } = props;
// Get the drawing area bounding box
diff --git a/docs/data/charts/components/components.md b/docs/data/charts/components/components.md
index 791c338ced4fe..c89a0ef699d0e 100644
--- a/docs/data/charts/components/components.md
+++ b/docs/data/charts/components/components.md
@@ -83,7 +83,7 @@ With the introduction of the `ChartDataProvider` in v8, the chart data can be ac
This allows you to create HTML components that interact with the charts data.
In the next example, notice that `MyCustomLegend` component displays the series names and colors.
-This creates an html `table` element, which handles long series names better than the default legend.
+This creates an html `table` element, which can be customized in any way.
{{"demo": "HtmlLegend.js"}}
@@ -91,7 +91,7 @@ This creates an html `table` element, which handles long series names better tha
Note that the HTML components are not part of the SVG hierarchy.
Hence, they should be:
-- Outside the ` ` component to avoid mixing HTAM and SVG.
+- Outside the ` ` component to avoid mixing HTML and SVG.
- Inside the ` ` component to get access to the data.
:::
diff --git a/docs/data/charts/getting-started/getting-started.md b/docs/data/charts/getting-started/getting-started.md
index 7efdf925eebfa..64eddde9f28f1 100644
--- a/docs/data/charts/getting-started/getting-started.md
+++ b/docs/data/charts/getting-started/getting-started.md
@@ -92,6 +92,15 @@ The outer margin space is where information like axes, titles, and legends are d
See the [Styling documentation](/x/react-charts/styling/#placement) for complete details.
+## Server-side rendering
+
+The chart support server-side rendering under two conditions:
+
+1. The `width` and `height` props needs to be provided.
+2. The animation should be disabled with the `skipAnimation` prop.
+
+The reason is that it is not possible to compute the SVG dimensions on the server, and the `skipAnimation` ensures that the animation is not in an "empty" state when first rendering.
+
## Axis management
MUI X Charts take a flexible approach to axis management, with support for multiple axes and any combination of scales and ranges.
diff --git a/docs/data/charts/heatmap/ColorConfig.js b/docs/data/charts/heatmap/ColorConfig.js
index b20e75a7a1181..16b4f95bd98a4 100644
--- a/docs/data/charts/heatmap/ColorConfig.js
+++ b/docs/data/charts/heatmap/ColorConfig.js
@@ -107,6 +107,7 @@ export default function ColorConfig() {
xAxis={[{ data: xData }]}
yAxis={[{ data: yData }]}
series={[{ data }]}
+ margin={{ left: 70 }}
zAxis={[
{
min: 20,
diff --git a/docs/data/charts/heatmap/ColorConfig.tsx b/docs/data/charts/heatmap/ColorConfig.tsx
index ee09304de139d..d71f65a9df85e 100644
--- a/docs/data/charts/heatmap/ColorConfig.tsx
+++ b/docs/data/charts/heatmap/ColorConfig.tsx
@@ -110,6 +110,7 @@ export default function ColorConfig() {
xAxis={[{ data: xData }]}
yAxis={[{ data: yData }]}
series={[{ data }]}
+ margin={{ left: 70 }}
zAxis={[
{
min: 20,
diff --git a/docs/data/charts/label/PieLabel.js b/docs/data/charts/label/PieLabel.js
index f363fbac7e81d..78f4259cd41de 100644
--- a/docs/data/charts/label/PieLabel.js
+++ b/docs/data/charts/label/PieLabel.js
@@ -21,6 +21,6 @@ export default function PieLabel() {
}
const props = {
- width: 500,
+ width: 200,
height: 200,
};
diff --git a/docs/data/charts/label/PieLabel.tsx b/docs/data/charts/label/PieLabel.tsx
index f363fbac7e81d..78f4259cd41de 100644
--- a/docs/data/charts/label/PieLabel.tsx
+++ b/docs/data/charts/label/PieLabel.tsx
@@ -21,6 +21,6 @@ export default function PieLabel() {
}
const props = {
- width: 500,
+ width: 200,
height: 200,
};
diff --git a/docs/data/charts/legend/BasicColorLegend.js b/docs/data/charts/legend/BasicColorLegend.js
index cb3af11c0885b..481f6e61f62e2 100644
--- a/docs/data/charts/legend/BasicColorLegend.js
+++ b/docs/data/charts/legend/BasicColorLegend.js
@@ -3,11 +3,12 @@ import Typography from '@mui/material/Typography';
import { LineChart } from '@mui/x-charts/LineChart';
import { ChartsReferenceLine } from '@mui/x-charts/ChartsReferenceLine';
import { PiecewiseColorLegend } from '@mui/x-charts/ChartsLegend';
+import Stack from '@mui/material/Stack';
import { dataset } from './tempAnomaly';
export default function BasicColorLegend() {
return (
-
+
Global temperature anomaly relative to 1961-1990 average
@@ -43,16 +44,19 @@ export default function BasicColorLegend() {
]}
grid={{ horizontal: true }}
height={300}
- margin={{ top: 30, right: 150 }}
- hideLegend
+ margin={{ top: 20, right: 20 }}
+ slotProps={{
+ legend: {
+ axisDirection: 'x',
+ direction: 'vertical',
+ },
+ }}
+ slots={{
+ legend: PiecewiseColorLegend,
+ }}
>
-
-
+
);
}
diff --git a/docs/data/charts/legend/BasicColorLegend.tsx b/docs/data/charts/legend/BasicColorLegend.tsx
index cb3af11c0885b..481f6e61f62e2 100644
--- a/docs/data/charts/legend/BasicColorLegend.tsx
+++ b/docs/data/charts/legend/BasicColorLegend.tsx
@@ -3,11 +3,12 @@ import Typography from '@mui/material/Typography';
import { LineChart } from '@mui/x-charts/LineChart';
import { ChartsReferenceLine } from '@mui/x-charts/ChartsReferenceLine';
import { PiecewiseColorLegend } from '@mui/x-charts/ChartsLegend';
+import Stack from '@mui/material/Stack';
import { dataset } from './tempAnomaly';
export default function BasicColorLegend() {
return (
-
+
Global temperature anomaly relative to 1961-1990 average
@@ -43,16 +44,19 @@ export default function BasicColorLegend() {
]}
grid={{ horizontal: true }}
height={300}
- margin={{ top: 30, right: 150 }}
- hideLegend
+ margin={{ top: 20, right: 20 }}
+ slotProps={{
+ legend: {
+ axisDirection: 'x',
+ direction: 'vertical',
+ },
+ }}
+ slots={{
+ legend: PiecewiseColorLegend,
+ }}
>
-
-
+
);
}
diff --git a/docs/data/charts/legend/ContinuousInteractiveDemoNoSnap.js b/docs/data/charts/legend/ContinuousInteractiveDemoNoSnap.js
index 09feb0234a322..ad79ebd83f4c5 100644
--- a/docs/data/charts/legend/ContinuousInteractiveDemoNoSnap.js
+++ b/docs/data/charts/legend/ContinuousInteractiveDemoNoSnap.js
@@ -1,3 +1,4 @@
+// @ts-check
import * as React from 'react';
import ChartsUsageDemo from 'docsx/src/modules/components/ChartsUsageDemo';
import { interpolateRdYlBu } from 'd3-scale-chromatic';
@@ -14,121 +15,113 @@ export default function ContinuousInteractiveDemoNoSnap() {
{
propName: 'direction',
knob: 'select',
- defaultValue: 'row',
- options: ['row', 'column'],
+ defaultValue: 'horizontal',
+ options: ['horizontal', 'vertical'],
+ },
+ {
+ propName: 'labelPosition',
+ knob: 'select',
+ defaultValue: 'end',
+ options: ['start', 'end', 'extremes'],
},
{
propName: 'length',
knob: 'number',
defaultValue: 50,
min: 10,
- max: 80,
},
{
propName: 'thickness',
knob: 'number',
- defaultValue: 5,
+ defaultValue: 12,
min: 1,
max: 20,
},
{
- propName: 'align',
- knob: 'select',
- defaultValue: 'middle',
- options: ['start', 'middle', 'end'],
- },
- {
- propName: 'fontSize',
- knob: 'number',
- defaultValue: 10,
- min: 8,
- max: 20,
+ propName: 'reverse',
+ knob: 'switch',
+ defaultValue: false,
},
]}
- renderDemo={(props) => (
-
- `${value?.toFixed(2)}°`,
- },
- ]}
- xAxis={[
- {
- scaleType: 'time',
- dataKey: 'year',
- disableLine: true,
- valueFormatter: (value) => value.getFullYear().toString(),
+ renderDemo={(
+ /** @type {{ direction: "horizontal" | "vertical"; length: number; thickness: number; labelPosition: 'start' | 'end' | 'extremes'; reverse: boolean; }} */
+ props,
+ ) => (
+ `${value?.toFixed(2)}°`,
+ },
+ ]}
+ xAxis={[
+ {
+ scaleType: 'time',
+ dataKey: 'year',
+ disableLine: true,
+ valueFormatter: (value) => value.getFullYear().toString(),
+ },
+ ]}
+ yAxis={[
+ {
+ disableLine: true,
+ disableTicks: true,
+ valueFormatter: (value) => `${value}°`,
+ colorMap: {
+ type: 'continuous',
+ min: -0.5,
+ max: 1.5,
+ color: (t) => interpolateRdYlBu(1 - t),
},
- ]}
- yAxis={[
- {
- disableLine: true,
- disableTicks: true,
- valueFormatter: (value) => `${value}°`,
- colorMap: {
- type: 'continuous',
- min: -0.5,
- max: 1.5,
- color: (t) => interpolateRdYlBu(1 - t),
- },
+ },
+ ]}
+ grid={{ horizontal: true }}
+ height={300}
+ margin={{ top: 20, right: 20 }}
+ slots={{ legend: ContinuousColorLegend }}
+ slotProps={{
+ legend: {
+ axisDirection: 'y',
+ direction: props.direction,
+ thickness: props.thickness,
+ labelPosition: props.labelPosition,
+ reverse: props.reverse,
+ sx: {
+ [props.direction === 'horizontal' ? 'width' : 'height']:
+ `${props.length}${props.direction === 'horizontal' ? '%' : 'px'}`,
},
- ]}
- grid={{ horizontal: true }}
- height={300}
- margin={{
- top: props.direction === 'row' ? 50 : 20,
- right: props.direction === 'row' ? 20 : 50,
- }}
- hideLegend
- >
-
-
-
-
+ },
+ }}
+ >
+
+
)}
- getCode={({ props }) => {
- return [
- `import { LineChart } from '@mui/x-charts/LineChart';`,
- `import { ContinuousColorLegend } from '@mui/x-charts/ChartsLegend';`,
- '',
- `',
- ` `,
- ' ',
- ].join('\n');
+ getCode={(
+ /** @type {{props: { direction: "horizontal" | "vertical"; length: number; thickness: number; labelPosition: 'start' | 'end' | 'extremes'; reverse: boolean; }}} */
+ { props },
+ ) => {
+ return `
+import { ContinuousColorLegend } from '@mui/x-charts/ChartsLegend';
+
+
+`;
}}
/>
);
diff --git a/docs/data/charts/legend/CustomLegend.js b/docs/data/charts/legend/CustomLegend.js
new file mode 100644
index 0000000000000..62c74bb546743
--- /dev/null
+++ b/docs/data/charts/legend/CustomLegend.js
@@ -0,0 +1,111 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import Stack from '@mui/material/Stack';
+import Typography from '@mui/material/Typography';
+import { useTheme } from '@mui/material/styles';
+import { useLegend } from '@mui/x-charts/hooks';
+import { LineChart } from '@mui/x-charts/LineChart';
+
+function LineWithMark({ color, size }) {
+ return (
+
+
+
+
+ );
+}
+
+function DashedLine({ color, size }) {
+ return (
+
+
+
+ );
+}
+
+function MyCustomLegend() {
+ const { items } = useLegend();
+ return (
+
+ {items.map((item) => {
+ const { label, id, color, seriesId } = item;
+ return (
+
+ {seriesId === 'avg' ? (
+
+ ) : (
+
+ )}
+ {`${label}`}
+
+ );
+ })}
+
+ );
+}
+
+const dataset = [
+ { month: 'Jan', '1991_2020_avg': 4.1, 2023: 3.9 },
+ { month: 'Fev', '1991_2020_avg': 4.7, 2023: 8.9 },
+ { month: 'Mar', '1991_2020_avg': 7.5, 2023: 9.5 },
+ { month: 'Apr', '1991_2020_avg': 10.6, 2023: 11.5 },
+ { month: 'May', '1991_2020_avg': 13.8, 2023: 15.5 },
+ { month: 'Jun', '1991_2020_avg': 16.7, 2023: 16.4 },
+ { month: 'Jul', '1991_2020_avg': 18.9, 2023: 19.5 },
+ { month: 'Aug', '1991_2020_avg': 18.8, 2023: 20.5 },
+ { month: 'Sep', '1991_2020_avg': 15.8, 2023: 16.4 },
+ { month: 'Oct', '1991_2020_avg': 11.9, 2023: 13.2 },
+ { month: 'Nov', '1991_2020_avg': 7.6, 2023: 8.1 },
+ { month: 'Dec', '1991_2020_avg': 4.7, 2023: 6.1 },
+];
+
+export default function CustomLegend() {
+ const theme = useTheme();
+
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/charts/legend/CustomLegend.tsx b/docs/data/charts/legend/CustomLegend.tsx
new file mode 100644
index 0000000000000..9d84a0b9ee10e
--- /dev/null
+++ b/docs/data/charts/legend/CustomLegend.tsx
@@ -0,0 +1,116 @@
+import * as React from 'react';
+import Box from '@mui/material/Box';
+import Stack from '@mui/material/Stack';
+import Typography from '@mui/material/Typography';
+import { useTheme } from '@mui/material/styles';
+import { useLegend } from '@mui/x-charts/hooks';
+import { LineChart } from '@mui/x-charts/LineChart';
+
+type IconProps = {
+ color: string;
+ size: number;
+};
+
+function LineWithMark({ color, size }: IconProps) {
+ return (
+
+
+
+
+ );
+}
+
+function DashedLine({ color, size }: IconProps) {
+ return (
+
+
+
+ );
+}
+
+function MyCustomLegend() {
+ const { items } = useLegend();
+ return (
+
+ {items.map((item) => {
+ const { label, id, color, seriesId } = item;
+ return (
+
+ {seriesId === 'avg' ? (
+
+ ) : (
+
+ )}
+ {`${label}`}
+
+ );
+ })}
+
+ );
+}
+
+const dataset = [
+ { month: 'Jan', '1991_2020_avg': 4.1, 2023: 3.9 },
+ { month: 'Fev', '1991_2020_avg': 4.7, 2023: 8.9 },
+ { month: 'Mar', '1991_2020_avg': 7.5, 2023: 9.5 },
+ { month: 'Apr', '1991_2020_avg': 10.6, 2023: 11.5 },
+ { month: 'May', '1991_2020_avg': 13.8, 2023: 15.5 },
+ { month: 'Jun', '1991_2020_avg': 16.7, 2023: 16.4 },
+ { month: 'Jul', '1991_2020_avg': 18.9, 2023: 19.5 },
+ { month: 'Aug', '1991_2020_avg': 18.8, 2023: 20.5 },
+ { month: 'Sep', '1991_2020_avg': 15.8, 2023: 16.4 },
+ { month: 'Oct', '1991_2020_avg': 11.9, 2023: 13.2 },
+ { month: 'Nov', '1991_2020_avg': 7.6, 2023: 8.1 },
+ { month: 'Dec', '1991_2020_avg': 4.7, 2023: 6.1 },
+];
+
+export default function CustomLegend() {
+ const theme = useTheme();
+
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/charts/legend/HiddenLegend.js b/docs/data/charts/legend/HiddenLegend.js
index 80879da5f9bf5..d33b0ffa42de2 100644
--- a/docs/data/charts/legend/HiddenLegend.js
+++ b/docs/data/charts/legend/HiddenLegend.js
@@ -17,22 +17,20 @@ const series = [
export default function HiddenLegend() {
const [isHidden, setIsHidden] = React.useState(false);
+ const Toggle = (
+ setIsHidden(event.target.checked)} />}
+ label="hide the legend"
+ labelPlacement="end"
+ sx={{ margin: 'auto' }}
+ />
+ );
+
return (
- setIsHidden(event.target.checked)} />
- }
- label="hide the legend"
- labelPlacement="end"
- />
-
+ {Toggle}
+
);
}
diff --git a/docs/data/charts/legend/HiddenLegend.tsx b/docs/data/charts/legend/HiddenLegend.tsx
index 80879da5f9bf5..d33b0ffa42de2 100644
--- a/docs/data/charts/legend/HiddenLegend.tsx
+++ b/docs/data/charts/legend/HiddenLegend.tsx
@@ -17,22 +17,20 @@ const series = [
export default function HiddenLegend() {
const [isHidden, setIsHidden] = React.useState(false);
+ const Toggle = (
+ setIsHidden(event.target.checked)} />}
+ label="hide the legend"
+ labelPlacement="end"
+ sx={{ margin: 'auto' }}
+ />
+ );
+
return (
- setIsHidden(event.target.checked)} />
- }
- label="hide the legend"
- labelPlacement="end"
- />
-
+ {Toggle}
+
);
}
diff --git a/docs/data/charts/legend/HiddenLegend.tsx.preview b/docs/data/charts/legend/HiddenLegend.tsx.preview
index 52019cf368ffa..c20f9eec4e8c8 100644
--- a/docs/data/charts/legend/HiddenLegend.tsx.preview
+++ b/docs/data/charts/legend/HiddenLegend.tsx.preview
@@ -1,14 +1,2 @@
- setIsHidden(event.target.checked)} />
- }
- label="hide the legend"
- labelPlacement="end"
-/>
-
\ No newline at end of file
+{Toggle}
+
\ No newline at end of file
diff --git a/docs/data/charts/legend/LegendClickNoSnap.js b/docs/data/charts/legend/LegendClickNoSnap.js
index 968e601b061a7..f9ca1551987df 100644
--- a/docs/data/charts/legend/LegendClickNoSnap.js
+++ b/docs/data/charts/legend/LegendClickNoSnap.js
@@ -1,3 +1,5 @@
+// @ts-check
+
import * as React from 'react';
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
@@ -8,13 +10,13 @@ import UndoOutlinedIcon from '@mui/icons-material/UndoOutlined';
import { ChartsLegend, PiecewiseColorLegend } from '@mui/x-charts/ChartsLegend';
import { HighlightedCode } from '@mui/docs/HighlightedCode';
-import { ChartContainer } from '@mui/x-charts/ChartContainer';
+import { ChartDataProvider } from '@mui/x-charts/context';
+/** @type {import('@mui/x-charts/PieChart').PieChartProps['series']} */
const pieSeries = [
{
type: 'pie',
id: 'series-1',
- label: 'Series 1',
data: [
{ label: 'Pie A', id: 'P1-A', value: 400 },
{ label: 'Pie B', id: 'P2-B', value: 300 },
@@ -22,6 +24,7 @@ const pieSeries = [
},
];
+/** @type {import('@mui/x-charts/BarChart').BarChartProps['series']} */
const barSeries = [
{
type: 'bar',
@@ -37,6 +40,7 @@ const barSeries = [
},
];
+/** @type {import('@mui/x-charts/LineChart').LineChartProps['series']} */
const lineSeries = [
{
type: 'line',
@@ -61,31 +65,44 @@ export default function LegendClickNoSnap() {
spacing={{ xs: 0, md: 4 }}
sx={{ width: '100%' }}
>
-
+
Chart Legend
-
+
setItemData([context, index])}
/>
-
+
Pie Chart Legend
-
+
setItemData([context, index])}
/>
-
- Pie Chart Legend
-
+ Piecewise Color Legend
+
setItemData([context, index])}
/>
-
+
@@ -127,6 +141,7 @@ export default function LegendClickNoSnap() {
aria-label="reset"
size="small"
onClick={() => {
+ // @ts-ignore
setItemData(null);
}}
>
diff --git a/docs/data/charts/legend/LegendDimensionNoSnap.js b/docs/data/charts/legend/LegendDimensionNoSnap.js
index 2581092596b58..f9dd92f87bb2c 100644
--- a/docs/data/charts/legend/LegendDimensionNoSnap.js
+++ b/docs/data/charts/legend/LegendDimensionNoSnap.js
@@ -1,6 +1,9 @@
+// @ts-check
+
import * as React from 'react';
import ChartsUsageDemo from 'docsx/src/modules/components/ChartsUsageDemo';
import { PieChart } from '@mui/x-charts/PieChart';
+import { legendClasses } from '@mui/x-charts/ChartsLegend';
const data = [
{ id: 0, value: 10, label: 'Series A' },
@@ -13,7 +16,7 @@ const data = [
{ id: 7, value: 15, label: 'Series H' },
];
-const itemsNumber = 15;
+const itemsNumber = 8;
export default function LegendDimensionNoSnap() {
return (
@@ -23,15 +26,17 @@ export default function LegendDimensionNoSnap() {
{
propName: 'direction',
knob: 'select',
- defaultValue: 'column',
- options: ['row', 'column'],
+ defaultValue: 'horizontal',
+ options: ['horizontal', 'vertical'],
},
- { propName: 'itemMarkWidth', knob: 'number', defaultValue: 20 },
- { propName: 'itemMarkHeight', knob: 'number', defaultValue: 2 },
- { propName: 'markGap', knob: 'number', defaultValue: 5 },
- { propName: 'itemGap', knob: 'number', defaultValue: 10 },
+ { propName: 'markSize', knob: 'number', defaultValue: 15, min: 0 },
+ { propName: 'markGap', knob: 'number', defaultValue: 8, min: 0 },
+ { propName: 'itemGap', knob: 'number', defaultValue: 16, min: 0 },
]}
- renderDemo={(props) => (
+ renderDemo={(
+ /** @type {{ direction: "horizontal" | "vertical"; markSize: number; markGap: number; itemGap: number; scrollable: boolean;}} */
+ props,
+ ) => (
)}
- getCode={({ props }) => {
- return [
- `import { PieChart } from '@mui/x-charts/PieChart';`,
- '',
- ` ',
- ].join('\n');
+ getCode={(
+ /** @type {{ props: { direction: "horizontal" | "vertical"; markSize: number; markGap: number; itemGap: number; scrollable: boolean;}}} */
+ { props },
+ ) => {
+ return `
+import { PieChart } from '@mui/x-charts/PieChart';
+import { legendClasses } from '@mui/x-charts/ChartsLegend';
+
+
+`;
}}
/>
);
diff --git a/docs/data/charts/legend/LegendLabelPositions.js b/docs/data/charts/legend/LegendLabelPositions.js
new file mode 100644
index 0000000000000..4b1817806d249
--- /dev/null
+++ b/docs/data/charts/legend/LegendLabelPositions.js
@@ -0,0 +1,203 @@
+import * as React from 'react';
+import { interpolateRdYlBu } from 'd3-scale-chromatic';
+import {
+ ContinuousColorLegend,
+ piecewiseColorDefaultLabelFormatter,
+ PiecewiseColorLegend,
+} from '@mui/x-charts/ChartsLegend';
+import { ChartDataProvider } from '@mui/x-charts/context';
+import { ChartsAxesGradients } from '@mui/x-charts/internals';
+import Stack from '@mui/material/Stack';
+import Typography from '@mui/material/Typography';
+import Divider from '@mui/material/Divider';
+
+export default function LegendLabelPositions() {
+ const piecewiseFormatter = (params) =>
+ params.index === 0 || params.index === params.length
+ ? piecewiseColorDefaultLabelFormatter(params)
+ : '';
+
+ return (
+ `${value}°`,
+ colorMap: {
+ type: 'continuous',
+ min: -0.5,
+ max: 1.5,
+ color: (t) => interpolateRdYlBu(1 - t),
+ },
+ },
+ ]}
+ xAxis={[
+ {
+ valueFormatter: (value) => `${value}°`,
+ colorMap: {
+ type: 'piecewise',
+ thresholds: [0, 1.5],
+ colors: [
+ interpolateRdYlBu(0.9),
+ interpolateRdYlBu(0.5),
+ interpolateRdYlBu(0.1),
+ ],
+ },
+ },
+ ]}
+ >
+
+
+ Continuous
+ Horizontal
+ div': { flex: 1 } }}>
+
+ start
+
+
+
+ end
+
+
+
+ extremes
+
+
+
+
+ Vertical
+ div': {
+ flex: 1,
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ '& .MuiContinuousColorLegend-root': { flex: 1 },
+ }}
+ >
+
+ start
+
+
+
+ end
+
+
+
+ extremes
+
+
+
+
+
+ Piecewise
+ Horizontal
+ div': { flex: 1 } }}>
+
+
+
+
+
+ Vertical
+ div': {
+ flex: 1,
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/data/charts/legend/LegendLabelPositions.tsx b/docs/data/charts/legend/LegendLabelPositions.tsx
new file mode 100644
index 0000000000000..9494cff502e70
--- /dev/null
+++ b/docs/data/charts/legend/LegendLabelPositions.tsx
@@ -0,0 +1,204 @@
+import * as React from 'react';
+import { interpolateRdYlBu } from 'd3-scale-chromatic';
+import {
+ ContinuousColorLegend,
+ piecewiseColorDefaultLabelFormatter,
+ PiecewiseColorLegend,
+ PiecewiseLabelFormatterParams,
+} from '@mui/x-charts/ChartsLegend';
+import { ChartDataProvider } from '@mui/x-charts/context';
+import { ChartsAxesGradients } from '@mui/x-charts/internals';
+import Stack from '@mui/material/Stack';
+import Typography from '@mui/material/Typography';
+import Divider from '@mui/material/Divider';
+
+export default function LegendLabelPositions() {
+ const piecewiseFormatter = (params: PiecewiseLabelFormatterParams) =>
+ params.index === 0 || params.index === params.length
+ ? piecewiseColorDefaultLabelFormatter(params)
+ : '';
+
+ return (
+ `${value}°`,
+ colorMap: {
+ type: 'continuous',
+ min: -0.5,
+ max: 1.5,
+ color: (t) => interpolateRdYlBu(1 - t),
+ },
+ },
+ ]}
+ xAxis={[
+ {
+ valueFormatter: (value) => `${value}°`,
+ colorMap: {
+ type: 'piecewise',
+ thresholds: [0, 1.5],
+ colors: [
+ interpolateRdYlBu(0.9),
+ interpolateRdYlBu(0.5),
+ interpolateRdYlBu(0.1),
+ ],
+ },
+ },
+ ]}
+ >
+
+
+ Continuous
+ Horizontal
+ div': { flex: 1 } }}>
+
+ start
+
+
+
+ end
+
+
+
+ extremes
+
+
+
+
+ Vertical
+ div': {
+ flex: 1,
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ '& .MuiContinuousColorLegend-root': { flex: 1 },
+ }}
+ >
+
+ start
+
+
+
+ end
+
+
+
+ extremes
+
+
+
+
+
+ Piecewise
+ Horizontal
+ div': { flex: 1 } }}>
+
+
+
+
+
+ Vertical
+ div': {
+ flex: 1,
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ },
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/docs/data/charts/legend/LegendMarkTypeNoSnap.js b/docs/data/charts/legend/LegendMarkTypeNoSnap.js
new file mode 100644
index 0000000000000..a73ca40fd4adf
--- /dev/null
+++ b/docs/data/charts/legend/LegendMarkTypeNoSnap.js
@@ -0,0 +1,64 @@
+// @ts-check
+
+import * as React from 'react';
+import ChartsUsageDemo from 'docsx/src/modules/components/ChartsUsageDemo';
+import { BarChart } from '@mui/x-charts/BarChart';
+
+const seriesConfig = [
+ { id: 0, data: [10], label: 'Series A' },
+ { id: 1, data: [15], label: 'Series B' },
+ { id: 2, data: [20], label: 'Series C' },
+ { id: 3, data: [10], label: 'Series D' },
+];
+
+export default function LegendMarkTypeNoSnap() {
+ return (
+ (
+ ({
+ ...seriesItem,
+ labelMarkType: props.markType,
+ }))}
+ xAxis={[
+ {
+ scaleType: 'band',
+ data: ['A'],
+ },
+ ]}
+ height={200}
+ />
+ )}
+ getCode={(
+ /** @type {{props: { markType: "square" | "circle" | "line" }}} */
+ { props },
+ ) => {
+ return `
+import { BarChart } from '@mui/x-charts/BarChart';
+
+ ({
+ ...seriesItem,
+ labelMarkType: '${props.markType}',
+ }))
+ }
+/>
+`;
+ }}
+ />
+ );
+}
diff --git a/docs/data/charts/legend/LegendPositionNoSnap.js b/docs/data/charts/legend/LegendPositionNoSnap.js
index ac23970b9c43e..1a96d35c09952 100644
--- a/docs/data/charts/legend/LegendPositionNoSnap.js
+++ b/docs/data/charts/legend/LegendPositionNoSnap.js
@@ -1,3 +1,5 @@
+// @ts-check
+
import * as React from 'react';
import ChartsUsageDemo from 'docsx/src/modules/components/ChartsUsageDemo';
import { PieChart } from '@mui/x-charts/PieChart';
@@ -28,13 +30,13 @@ export default function LegendPositionNoSnap() {
{
propName: 'direction',
knob: 'select',
- defaultValue: 'row',
- options: ['row', 'column'],
+ defaultValue: 'vertical',
+ options: ['horizontal', 'vertical'],
},
{
propName: 'vertical',
knob: 'select',
- defaultValue: 'top',
+ defaultValue: 'middle',
options: ['top', 'middle', 'bottom'],
},
{
@@ -43,59 +45,54 @@ export default function LegendPositionNoSnap() {
defaultValue: 'middle',
options: ['left', 'middle', 'right'],
},
- {
- propName: 'padding',
- knob: 'number',
- defaultValue: 0,
- },
{
propName: 'itemsNumber',
knob: 'number',
- defaultValue: 5,
+ defaultValue: 3,
min: 1,
max: data.length,
},
]}
- renderDemo={(props) => (
+ renderDemo={(
+ /** @type {{ itemsNumber: number | undefined; direction: "horizontal" | "vertical"; vertical: "top" | "middle" | "bottom"; horizontal: "left" | "middle" | "right"; }} */
+ props,
+ ) => (
)}
- getCode={({ props }) => {
- return [
- `import { PieChart } from '@mui/x-charts/PieChart';`,
- '',
- ` ',
- ].join('\n');
+ getCode={(
+ /** @type {{props:{ itemsNumber: number | undefined; direction: "horizontal" | "vertical"; vertical: "top" | "middle" | "bottom"; horizontal: "left" | "middle" | "right";}}} */
+ { props },
+ ) => {
+ return `
+import { PieChart } from '@mui/x-charts/PieChart';
+
+
+`;
}}
/>
);
diff --git a/docs/data/charts/legend/LegendRoundedSymbol.js b/docs/data/charts/legend/LegendRoundedSymbol.js
deleted file mode 100644
index d5f9b2b864431..0000000000000
--- a/docs/data/charts/legend/LegendRoundedSymbol.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import * as React from 'react';
-import { PieChart } from '@mui/x-charts/PieChart';
-import { legendClasses } from '@mui/x-charts/ChartsLegend';
-
-const series = [
- {
- data: [
- { id: 0, value: 10, label: 'series A' },
- { id: 1, value: 15, label: 'series B' },
- { id: 2, value: 20, label: 'series C' },
- { id: 3, value: 30, label: 'series D' },
- ],
- },
-];
-
-export default function LegendRoundedSymbol() {
- return (
-
- );
-}
diff --git a/docs/data/charts/legend/LegendRoundedSymbol.tsx b/docs/data/charts/legend/LegendRoundedSymbol.tsx
deleted file mode 100644
index d5f9b2b864431..0000000000000
--- a/docs/data/charts/legend/LegendRoundedSymbol.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import * as React from 'react';
-import { PieChart } from '@mui/x-charts/PieChart';
-import { legendClasses } from '@mui/x-charts/ChartsLegend';
-
-const series = [
- {
- data: [
- { id: 0, value: 10, label: 'series A' },
- { id: 1, value: 15, label: 'series B' },
- { id: 2, value: 20, label: 'series C' },
- { id: 3, value: 30, label: 'series D' },
- ],
- },
-];
-
-export default function LegendRoundedSymbol() {
- return (
-
- );
-}
diff --git a/docs/data/charts/legend/LegendRoundedSymbol.tsx.preview b/docs/data/charts/legend/LegendRoundedSymbol.tsx.preview
deleted file mode 100644
index 7ed5a28c5642d..0000000000000
--- a/docs/data/charts/legend/LegendRoundedSymbol.tsx.preview
+++ /dev/null
@@ -1,10 +0,0 @@
-
\ No newline at end of file
diff --git a/docs/data/charts/legend/LegendTextStylingNoSnap.js b/docs/data/charts/legend/LegendTextStylingNoSnap.js
index 9af402d6c16be..1ffc7be8bd4a2 100644
--- a/docs/data/charts/legend/LegendTextStylingNoSnap.js
+++ b/docs/data/charts/legend/LegendTextStylingNoSnap.js
@@ -1,20 +1,17 @@
+// @ts-check
+
import * as React from 'react';
import ChartsUsageDemo from 'docsx/src/modules/components/ChartsUsageDemo';
import { PieChart } from '@mui/x-charts/PieChart';
+import { labelMarkClasses } from '@mui/x-charts/ChartsLabel';
const data = [
{ id: 0, value: 10, label: 'Series A' },
{ id: 1, value: 15, label: 'Series B' },
{ id: 2, value: 20, label: 'Series C' },
{ id: 3, value: 10, label: 'Series D' },
- { id: 4, value: 15, label: 'Series E' },
- { id: 5, value: 20, label: 'Series F' },
- { id: 6, value: 10, label: 'Series G' },
- { id: 7, value: 15, label: 'Series H' },
];
-const itemsNumber = 15;
-
export default function LegendTextStylingNoSnap() {
return (
(
+ renderDemo={(
+ /** @type {{ fontSize: number; color: string; markColor: string; }} */
+ props,
+ ) => (
({
- ...item,
- label: item.label.replace(' ', props.breakLine ? '\n' : ' '),
- })),
+ data,
},
]}
+ height={200}
+ width={200}
slotProps={{
legend: {
- labelStyle: {
+ sx: {
fontSize: props.fontSize,
- fill: props.fill,
+ color: props.color,
+ [`.${labelMarkClasses.fill}`]: {
+ fill: props.markColor,
+ },
},
},
}}
- margin={{
- top: 10,
- bottom: 10,
- left: 10,
- right: 200,
- }}
- width={400}
- height={400}
/>
)}
- getCode={({ props }) => {
- return [
- `import { PieChart } from '@mui/x-charts/PieChart';`,
- '',
- ` ',
- ].join('\n');
+ getCode={(
+ /** @type {{props: { fontSize: number; color: string; markColor: string; }}} */
+ { props },
+ ) => {
+ return `
+import { PieChart } from '@mui/x-charts/PieChart';
+import { labelMarkClasses } from '@mui/x-charts/ChartsLabel';
+
+
+`;
}}
/>
);
diff --git a/docs/data/charts/legend/PiecewiseInteractiveDemoNoSnap.js b/docs/data/charts/legend/PiecewiseInteractiveDemoNoSnap.js
index 03ea6ccec047b..2dd1a9b8ae118 100644
--- a/docs/data/charts/legend/PiecewiseInteractiveDemoNoSnap.js
+++ b/docs/data/charts/legend/PiecewiseInteractiveDemoNoSnap.js
@@ -1,7 +1,11 @@
+// @ts-check
import * as React from 'react';
import ChartsUsageDemo from 'docsx/src/modules/components/ChartsUsageDemo';
import { LineChart } from '@mui/x-charts/LineChart';
-import { PiecewiseColorLegend } from '@mui/x-charts/ChartsLegend';
+import {
+ piecewiseColorDefaultLabelFormatter,
+ PiecewiseColorLegend,
+} from '@mui/x-charts/ChartsLegend';
import { ChartsReferenceLine } from '@mui/x-charts/ChartsReferenceLine';
import { dataset } from './tempAnomaly';
@@ -11,109 +15,120 @@ export default function PiecewiseInteractiveDemoNoSnap() {
componentName="Legend"
data={[
{
- propName: 'hideFirst',
- knob: 'switch',
+ propName: 'direction',
+ knob: 'select',
+ defaultValue: 'horizontal',
+ options: ['horizontal', 'vertical'],
},
{
- propName: 'direction',
+ propName: 'labelPosition',
knob: 'select',
- defaultValue: 'row',
- options: ['row', 'column'],
+ defaultValue: 'extremes',
+ options: ['start', 'end', 'extremes'],
},
{
- propName: 'padding',
- knob: 'number',
- defaultValue: 0,
+ propName: 'markType',
+ knob: 'select',
+ defaultValue: 'square',
+ options: ['square', 'circle', 'line'],
},
{
- propName: 'fontSize',
+ propName: 'onlyShowExtremes',
+ knob: 'switch',
+ defaultValue: false,
+ },
+ {
+ propName: 'padding',
knob: 'number',
- defaultValue: 10,
- min: 8,
- max: 20,
+ defaultValue: 0,
},
]}
- renderDemo={(props) => (
-
-
`${value?.toFixed(2)}°`,
- },
- ]}
- xAxis={[
- {
- scaleType: 'time',
- dataKey: 'year',
- disableLine: true,
- valueFormatter: (value) => value.getFullYear().toString(),
- colorMap: {
- type: 'piecewise',
- thresholds: [new Date(1961, 0, 1), new Date(1990, 0, 1)],
- colors: ['blue', 'gray', 'red'],
- },
+ renderDemo={(
+ /** @type {{ direction: "vertical" | "horizontal"; markType: 'square' | 'circle' | 'line'; labelPosition: 'start' | 'end' | 'extremes'; padding: number; onlyShowExtremes: boolean; }} */
+ props,
+ ) => (
+ `${value?.toFixed(2)}°`,
+ },
+ ]}
+ xAxis={[
+ {
+ scaleType: 'time',
+ dataKey: 'year',
+ disableLine: true,
+ valueFormatter: (value) => value.getFullYear().toString(),
+ colorMap: {
+ type: 'piecewise',
+ thresholds: [new Date(1961, 0, 1), new Date(1990, 0, 1)],
+ colors: ['blue', 'gray', 'red'],
},
- ]}
- yAxis={[
- {
- disableLine: true,
- disableTicks: true,
- valueFormatter: (value) => `${value}°`,
+ },
+ ]}
+ yAxis={[
+ {
+ disableLine: true,
+ disableTicks: true,
+ valueFormatter: (value) => `${value}°`,
+ },
+ ]}
+ grid={{ horizontal: true }}
+ height={300}
+ margin={{ top: 20, right: 20 }}
+ slots={{
+ legend: PiecewiseColorLegend,
+ }}
+ slotProps={{
+ legend: {
+ axisDirection: 'x',
+ direction: props.direction,
+ markType: props.markType,
+ labelPosition: props.labelPosition,
+ labelFormatter: props.onlyShowExtremes
+ ? (params) =>
+ params.index === 0 || params.index === params.length
+ ? piecewiseColorDefaultLabelFormatter(params)
+ : ''
+ : undefined,
+ sx: {
+ padding: props.padding,
},
- ]}
- grid={{ horizontal: true }}
- height={300}
- margin={{
- top: props.direction === 'row' ? 50 : 20,
- right: props.direction === 'row' ? 20 : 150,
- }}
- hideLegend
- >
-
-
-
-
+ },
+ }}
+ >
+
+
)}
- getCode={({ props }) => {
- return [
- `import { LineChart } from '@mui/x-charts/LineChart';`,
- `import { PiecewiseColorLegend } from '@mui/x-charts/ChartsLegend';`,
- '',
- `',
- ` `,
- ' ',
- ].join('\n');
+ getCode={(
+ /** @type {{props:{ direction: "vertical" | "horizontal"; markType: 'square' | 'circle' | 'line'; labelPosition: 'start' | 'end' | 'extremes'; padding: number; onlyShowExtremes: boolean; }}} */
+ { props },
+ ) => {
+ return `
+import {
+ PiecewiseColorLegend,
+ piecewiseColorDefaultLabelFormatter,
+} from '@mui/x-charts/ChartsLegend';
+
+\n params.index === 0 || params.index === params.length\n ? piecewiseColorDefaultLabelFormatter(params) \n : ''" : ''}
+ },
+ }}
+/>
+`;
}}
/>
);
diff --git a/docs/data/charts/legend/ScrollableLegend.js b/docs/data/charts/legend/ScrollableLegend.js
new file mode 100644
index 0000000000000..954cf672ee553
--- /dev/null
+++ b/docs/data/charts/legend/ScrollableLegend.js
@@ -0,0 +1,47 @@
+import * as React from 'react';
+import Stack from '@mui/material/Stack';
+import { PieChart } from '@mui/x-charts/PieChart';
+
+const series = [
+ {
+ data: [
+ { id: 0, value: 10, label: 'Series A' },
+ { id: 1, value: 15, label: 'Series B' },
+ { id: 2, value: 20, label: 'Series C' },
+ { id: 3, value: 10, label: 'Series D' },
+ { id: 4, value: 15, label: 'Series E' },
+ { id: 5, value: 20, label: 'Series F' },
+ { id: 6, value: 10, label: 'Series G' },
+ { id: 7, value: 15, label: 'Series H' },
+ { id: 8, value: 20, label: 'Series I' },
+ { id: 9, value: 10, label: 'Series J' },
+ { id: 10, value: 15, label: 'Series K' },
+ { id: 11, value: 20, label: 'Series L' },
+ { id: 12, value: 10, label: 'Series M' },
+ { id: 13, value: 15, label: 'Series N' },
+ { id: 14, value: 20, label: 'Series O' },
+ ],
+ },
+];
+
+export default function ScrollableLegend() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/charts/legend/ScrollableLegend.tsx b/docs/data/charts/legend/ScrollableLegend.tsx
new file mode 100644
index 0000000000000..954cf672ee553
--- /dev/null
+++ b/docs/data/charts/legend/ScrollableLegend.tsx
@@ -0,0 +1,47 @@
+import * as React from 'react';
+import Stack from '@mui/material/Stack';
+import { PieChart } from '@mui/x-charts/PieChart';
+
+const series = [
+ {
+ data: [
+ { id: 0, value: 10, label: 'Series A' },
+ { id: 1, value: 15, label: 'Series B' },
+ { id: 2, value: 20, label: 'Series C' },
+ { id: 3, value: 10, label: 'Series D' },
+ { id: 4, value: 15, label: 'Series E' },
+ { id: 5, value: 20, label: 'Series F' },
+ { id: 6, value: 10, label: 'Series G' },
+ { id: 7, value: 15, label: 'Series H' },
+ { id: 8, value: 20, label: 'Series I' },
+ { id: 9, value: 10, label: 'Series J' },
+ { id: 10, value: 15, label: 'Series K' },
+ { id: 11, value: 20, label: 'Series L' },
+ { id: 12, value: 10, label: 'Series M' },
+ { id: 13, value: 15, label: 'Series N' },
+ { id: 14, value: 20, label: 'Series O' },
+ ],
+ },
+];
+
+export default function ScrollableLegend() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/charts/legend/ScrollableLegend.tsx.preview b/docs/data/charts/legend/ScrollableLegend.tsx.preview
new file mode 100644
index 0000000000000..6f7c38b28639a
--- /dev/null
+++ b/docs/data/charts/legend/ScrollableLegend.tsx.preview
@@ -0,0 +1,15 @@
+
\ No newline at end of file
diff --git a/docs/data/charts/legend/VeryBasicColorLegend.js b/docs/data/charts/legend/VeryBasicColorLegend.js
new file mode 100644
index 0000000000000..766603bb4b903
--- /dev/null
+++ b/docs/data/charts/legend/VeryBasicColorLegend.js
@@ -0,0 +1,62 @@
+import * as React from 'react';
+import { LineChart } from '@mui/x-charts/LineChart';
+import { PiecewiseColorLegend } from '@mui/x-charts/ChartsLegend';
+import Stack from '@mui/material/Stack';
+import { dataset } from './tempAnomaly';
+
+const data = {
+ dataset,
+ series: [
+ {
+ label: 'Global temperature anomaly relative to 1961-1990',
+ dataKey: 'anomaly',
+ showMark: false,
+ valueFormatter: (value) => `${value?.toFixed(2)}°`,
+ },
+ ],
+ xAxis: [
+ {
+ scaleType: 'time',
+ dataKey: 'year',
+ disableLine: true,
+ valueFormatter: (value) => value.getFullYear().toString(),
+ colorMap: {
+ type: 'piecewise',
+ thresholds: [new Date(1961, 0, 1), new Date(1990, 0, 1)],
+ colors: ['blue', 'gray', 'red'],
+ },
+ },
+ ],
+ yAxis: [
+ {
+ disableLine: true,
+ disableTicks: true,
+ valueFormatter: (value) => `${value}°`,
+ },
+ ],
+ grid: { horizontal: true },
+ height: 300,
+ margin: { top: 20, right: 20 },
+};
+
+export default function VeryBasicColorLegend() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/charts/legend/VeryBasicColorLegend.tsx b/docs/data/charts/legend/VeryBasicColorLegend.tsx
new file mode 100644
index 0000000000000..8468e565f6f6f
--- /dev/null
+++ b/docs/data/charts/legend/VeryBasicColorLegend.tsx
@@ -0,0 +1,62 @@
+import * as React from 'react';
+import { LineChart, LineChartProps } from '@mui/x-charts/LineChart';
+import { PiecewiseColorLegend } from '@mui/x-charts/ChartsLegend';
+import Stack from '@mui/material/Stack';
+import { dataset } from './tempAnomaly';
+
+const data: LineChartProps = {
+ dataset,
+ series: [
+ {
+ label: 'Global temperature anomaly relative to 1961-1990',
+ dataKey: 'anomaly',
+ showMark: false,
+ valueFormatter: (value) => `${value?.toFixed(2)}°`,
+ },
+ ],
+ xAxis: [
+ {
+ scaleType: 'time',
+ dataKey: 'year',
+ disableLine: true,
+ valueFormatter: (value) => value.getFullYear().toString(),
+ colorMap: {
+ type: 'piecewise',
+ thresholds: [new Date(1961, 0, 1), new Date(1990, 0, 1)],
+ colors: ['blue', 'gray', 'red'],
+ },
+ },
+ ],
+ yAxis: [
+ {
+ disableLine: true,
+ disableTicks: true,
+ valueFormatter: (value) => `${value}°`,
+ },
+ ],
+ grid: { horizontal: true },
+ height: 300,
+ margin: { top: 20, right: 20 },
+};
+
+export default function VeryBasicColorLegend() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/charts/legend/VeryBasicColorLegend.tsx.preview b/docs/data/charts/legend/VeryBasicColorLegend.tsx.preview
new file mode 100644
index 0000000000000..183ce44d8eed9
--- /dev/null
+++ b/docs/data/charts/legend/VeryBasicColorLegend.tsx.preview
@@ -0,0 +1,15 @@
+
\ No newline at end of file
diff --git a/docs/data/charts/legend/legend.md b/docs/data/charts/legend/legend.md
index 77c554482acaf..2d0da25629787 100644
--- a/docs/data/charts/legend/legend.md
+++ b/docs/data/charts/legend/legend.md
@@ -1,7 +1,7 @@
---
title: Charts - Legend
productId: x-charts
-components: ChartsLegend, DefaultChartsLegend, ChartsText, ContinuousColorLegend, PiecewiseColorLegend
+components: ChartsLegend, ChartsText, ContinuousColorLegend, PiecewiseColorLegend
---
# Charts - Legend
@@ -16,56 +16,78 @@ In chart components, the legend links series with `label` properties and their c
## Customization
+This section explains how to customize the legend using classes and properties.
+
+### Dimensions
+
+Much of the customization can be done using CSS properties.
+There is a main class for the legend container, `.MuiChartsLegend-root`.
+Alternatively the `legendClasses` variable can be used if using CSS-in-JS to target the elements.
+
+Each legend item is composed of two main elements: the `mark` and the `label`.
+
+The example below explains how it is possible to customize some parts the legend.
+And shows how to use both the `legendClasses` variable and the CSS class directly.
+
+{{"demo": "LegendDimensionNoSnap.js", "hideToolbar": true, "bg": "playground"}}
+
### Position
-The legend can either be displayed in a `'column'` or `'row'` layout controlled with the `direction` property.
+The legend can either be displayed in a `'vertical'` or `'horizontal'` layout controlled with the `direction` property.
-It can also be moved with the `position: { vertical, horizontal }` property which defines how the legend aligns within the SVG.
+It can also be moved with the `position: { vertical, horizontal }` property which defines how the legend aligns itself in the parent container.
- `vertical` can be `'top'`, `'middle'`, or `'bottom'`.
- `horizontal` can be `'left'`, `'middle'`, or `'right'`.
-You can add spacing to a given `position` with the `padding` property, which can be either a number or an object `{ top, bottom, left, right }`.
-This `padding` will add space between the SVG borders and the legend.
-
By default, the legend is placed above the charts.
+:::warning
+The `position` property is only available in the `slotProps`, but not in the ` `.
+In the second case, you are free to place the legend where you want.
+:::
+
{{"demo": "LegendPositionNoSnap.js", "hideToolbar": true, "bg": "playground"}}
### Hiding
-You can hide the legend with the property `slotProps.legend.hidden`.
+You can hide the legend with the `hideLegend` property of the Chart.
{{"demo": "HiddenLegend.js"}}
-### Dimensions
+### Label styling
-Inside the legend, you can customize the pixel value of the width and height of the mark with the `itemMarkWidth` and `itemMarkHeight` props.
+Changing the `label` style can be done by targeting the root component's font properties.
-You can also access the `markGap` prop to change the gap between the mark and its label, or the `itemGap` to change the gap between two legend items.
-Both props impact the values defined in pixels.
+To change the `mark` color or shape, the `fill` class is used instead.
+Keep in mind that the `mark` is an SVG element, so the `fill` property is used to change its color.
-{{"demo": "LegendDimensionNoSnap.js", "hideToolbar": true, "bg": "playground"}}
+{{"demo": "LegendTextStylingNoSnap.js", "hideToolbar": true, "bg": "playground"}}
-### Label styling
+### Change mark shape
-To break lines in legend labels, use the special `\n` character. To customize the label style, you should not use CSS.
-Instead, pass a styling object to the `labelStyle` property.
+To change the mark shape, you can use the `labelMarkType` property of the series item.
+For the `pie` series, the `labelMarkType` property is available for each of the pie slices too.
-{{"demo": "LegendTextStylingNoSnap.js", "hideToolbar": true, "bg": "playground"}}
+{{"demo": "LegendMarkTypeNoSnap.js", "hideToolbar": true, "bg": "playground"}}
-:::info
-The `labelStyle` property is needed to measure text size, and then place legend items at the correct position.
-Style applied by other means will not be taken into account.
-Which can lead to label overflow.
-:::
+### Scrollable legend
+
+The legend can be made scrollable by setting the `overflowY` for vertical legends or `overflowX` for horizontal legends.
+Make sure that the legend container has a fixed height or width to enable scrolling.
-### Rounded symbol
+{{"demo": "ScrollableLegend.js"}}
-To create a rounded symbol, use the `legendClasses.mark` to apply CSS on marks.
-Notice that SVG `rect` uses `ry` property to control the symbol's corner radius instead of `border-radius`.
+### Custom component
-{{"demo": "LegendRoundedSymbol.js"}}
+For advanced customization, you can create your own legend with `useLegend`.
+This hook returns the items that the default legend would plot.
+Allowing you to focus on the rendering.
+
+This demo show how to use it with slots.
+Another demo in [HTML components docs](/x/react-charts/components/#html-components) shows how to use it with composition.
+
+{{"demo": "CustomLegend.js"}}
## Color legend
@@ -74,6 +96,10 @@ To display legend associated to a [colorMap](https://mui.com/x/react-charts/styl
- ` ` if you're using `colorMap.type='continuous'`
- ` ` if you're using `colorMap.type='piecewise'`.
+Then it is possible to override the `legend` slot to display the wanted legend, or use the [composition API](https://mui.com/x/react-charts/composition/) to add as many legends as needed.
+
+{{"demo": "VeryBasicColorLegend.js"}}
+
### Select data
To select the color mapping to represent, use the following props:
@@ -87,16 +113,27 @@ To select the color mapping to represent, use the following props:
This component position is done exactly the same way as the [legend for series](#position).
+### Label position
+
+The labels can be positioned in relation to the marks or gradient with the `labelPosition` prop.
+The values accepted are `'start'`, `'end'` or `'extremes'`.
+
+- With `direction='horizontal'`, using `'start'` places the labels above the visual marker, while `end` places them below.
+- When `direction='vertical'`, is `'start'` or `'end'` the labels are positioned `left` and `right` of the visual markers, respectively.
+- With the `'extremes'` value, the labels are positioned at both the beginning and end of the visual marker.
+
+{{"demo": "LegendLabelPositions.js"}}
+
### Continuous color mapping
To modify the shape of the gradient, use the `length` and `thickness` props.
-The `length` can either be a number (in px) or a percentage string. The `"100%"` corresponds to the SVG dimension.
+The `length` can either be a number (in px) or a percentage string. The `"100%"` corresponds to the parent dimension.
To format labels use the `minLabel`/`maxLabel`.
They accept either a string to display.
Or a function `({value, formattedValue}) => string`.
-The labels and gradient bar alignment can be modified by the `align` prop.
+It is also possible to reverse the gradient with the `reverse` prop.
{{"demo": "ContinuousInteractiveDemoNoSnap.js", "hideToolbar": true, "bg": "playground"}}
@@ -105,18 +142,21 @@ The labels and gradient bar alignment can be modified by the `align` prop.
The piecewise Legend is quite similar to the series legend.
It accepts the same props for [customization](#dimensions).
-The props `hideFirst` and `hideLast` allows to hide the two extreme pieces: values lower than the min threshold, and value higher than the max threshold.
-
To override labels generated by default, provide a `labelFormatter` prop.
It takes the min/max of the piece and returns the label.
Values can be `null` for the first and last pieces.
And returning `null` removes the piece from the legend.
+Returning an empty string removes any label, but still display the `mark`.
```ts
-labelFormatter = ({ min, max, formattedMin, formattedMax }) => string | null;
+labelFormatter = ({ index, length, min, max, formattedMin, formattedMax }) =>
+ string | null;
```
+The `markType` can be changed with the `markType` prop.
+Since the color values are based on the axis, and not the series, the `markType` has to be set directly on the legend.
+
{{"demo": "PiecewiseInteractiveDemoNoSnap.js", "hideToolbar": true, "bg": "playground"}}
## Click event
diff --git a/docs/data/charts/pie-demo/PieChartWithCenterLabel.js b/docs/data/charts/pie-demo/PieChartWithCenterLabel.js
index 41f892141c412..9e094cd1abca9 100644
--- a/docs/data/charts/pie-demo/PieChartWithCenterLabel.js
+++ b/docs/data/charts/pie-demo/PieChartWithCenterLabel.js
@@ -11,7 +11,7 @@ const data = [
];
const size = {
- width: 400,
+ width: 200,
height: 200,
};
diff --git a/docs/data/charts/pie-demo/PieChartWithCenterLabel.tsx b/docs/data/charts/pie-demo/PieChartWithCenterLabel.tsx
index 3177a8e13c233..8d619a7e5b988 100644
--- a/docs/data/charts/pie-demo/PieChartWithCenterLabel.tsx
+++ b/docs/data/charts/pie-demo/PieChartWithCenterLabel.tsx
@@ -11,7 +11,7 @@ const data = [
];
const size = {
- width: 400,
+ width: 200,
height: 200,
};
diff --git a/docs/data/charts/pie-demo/StraightAnglePieChart.js b/docs/data/charts/pie-demo/StraightAnglePieChart.js
index 444b0cac8fb88..9efd2ef122719 100644
--- a/docs/data/charts/pie-demo/StraightAnglePieChart.js
+++ b/docs/data/charts/pie-demo/StraightAnglePieChart.js
@@ -21,6 +21,7 @@ export default function StraightAnglePieChart() {
},
]}
height={300}
+ width={300}
/>
);
}
diff --git a/docs/data/charts/pie-demo/StraightAnglePieChart.tsx b/docs/data/charts/pie-demo/StraightAnglePieChart.tsx
index 444b0cac8fb88..9efd2ef122719 100644
--- a/docs/data/charts/pie-demo/StraightAnglePieChart.tsx
+++ b/docs/data/charts/pie-demo/StraightAnglePieChart.tsx
@@ -21,6 +21,7 @@ export default function StraightAnglePieChart() {
},
]}
height={300}
+ width={300}
/>
);
}
diff --git a/docs/data/charts/pie-demo/StraightAnglePieChart.tsx.preview b/docs/data/charts/pie-demo/StraightAnglePieChart.tsx.preview
index e75631909bfe8..7892941a06c16 100644
--- a/docs/data/charts/pie-demo/StraightAnglePieChart.tsx.preview
+++ b/docs/data/charts/pie-demo/StraightAnglePieChart.tsx.preview
@@ -7,4 +7,5 @@
},
]}
height={300}
+ width={300}
/>
\ No newline at end of file
diff --git a/docs/data/charts/pie/BasicPie.js b/docs/data/charts/pie/BasicPie.js
index 2899266e0316b..97d722784cda2 100644
--- a/docs/data/charts/pie/BasicPie.js
+++ b/docs/data/charts/pie/BasicPie.js
@@ -13,7 +13,7 @@ export default function BasicPie() {
],
},
]}
- width={400}
+ width={200}
height={200}
/>
);
diff --git a/docs/data/charts/pie/BasicPie.tsx b/docs/data/charts/pie/BasicPie.tsx
index 2899266e0316b..97d722784cda2 100644
--- a/docs/data/charts/pie/BasicPie.tsx
+++ b/docs/data/charts/pie/BasicPie.tsx
@@ -13,7 +13,7 @@ export default function BasicPie() {
],
},
]}
- width={400}
+ width={200}
height={200}
/>
);
diff --git a/docs/data/charts/pie/BasicPie.tsx.preview b/docs/data/charts/pie/BasicPie.tsx.preview
index 61ac8ef626f20..28431a59a1325 100644
--- a/docs/data/charts/pie/BasicPie.tsx.preview
+++ b/docs/data/charts/pie/BasicPie.tsx.preview
@@ -8,6 +8,6 @@
],
},
]}
- width={400}
+ width={200}
height={200}
/>
\ No newline at end of file
diff --git a/docs/data/charts/pie/PieActiveArc.js b/docs/data/charts/pie/PieActiveArc.js
index 4e151e983c04f..8e6af13f69d65 100644
--- a/docs/data/charts/pie/PieActiveArc.js
+++ b/docs/data/charts/pie/PieActiveArc.js
@@ -14,6 +14,7 @@ export default function PieActiveArc() {
},
]}
height={200}
+ width={200}
/>
);
}
diff --git a/docs/data/charts/pie/PieActiveArc.tsx b/docs/data/charts/pie/PieActiveArc.tsx
index 4e151e983c04f..8e6af13f69d65 100644
--- a/docs/data/charts/pie/PieActiveArc.tsx
+++ b/docs/data/charts/pie/PieActiveArc.tsx
@@ -14,6 +14,7 @@ export default function PieActiveArc() {
},
]}
height={200}
+ width={200}
/>
);
}
diff --git a/docs/data/charts/pie/PieActiveArc.tsx.preview b/docs/data/charts/pie/PieActiveArc.tsx.preview
index 6e94368e1bbb3..3e5229579966b 100644
--- a/docs/data/charts/pie/PieActiveArc.tsx.preview
+++ b/docs/data/charts/pie/PieActiveArc.tsx.preview
@@ -8,4 +8,5 @@
},
]}
height={200}
+ width={200}
/>
\ No newline at end of file
diff --git a/docs/data/charts/pie/PieAnimation.js b/docs/data/charts/pie/PieAnimation.js
index 51584d139a45e..f8b488baa6a24 100644
--- a/docs/data/charts/pie/PieAnimation.js
+++ b/docs/data/charts/pie/PieAnimation.js
@@ -29,6 +29,7 @@ export default function PieAnimation() {
setItemData(d)}
diff --git a/docs/data/charts/scatter/ScatterDataset.js b/docs/data/charts/scatter/ScatterDataset.js
index 5a50fdc796762..b767d2169ac9e 100644
--- a/docs/data/charts/scatter/ScatterDataset.js
+++ b/docs/data/charts/scatter/ScatterDataset.js
@@ -88,6 +88,7 @@ const chartSetting = {
},
width: 500,
height: 300,
+ margin: { left: 60 },
};
export default function ScatterDataset() {
diff --git a/docs/data/charts/scatter/ScatterDataset.tsx b/docs/data/charts/scatter/ScatterDataset.tsx
index 5a50fdc796762..b767d2169ac9e 100644
--- a/docs/data/charts/scatter/ScatterDataset.tsx
+++ b/docs/data/charts/scatter/ScatterDataset.tsx
@@ -88,6 +88,7 @@ const chartSetting = {
},
width: 500,
height: 300,
+ margin: { left: 60 },
};
export default function ScatterDataset() {
diff --git a/docs/data/charts/styling/BasicColor.js b/docs/data/charts/styling/BasicColor.js
index 49bb1619c9a45..a1325422ac1e8 100644
--- a/docs/data/charts/styling/BasicColor.js
+++ b/docs/data/charts/styling/BasicColor.js
@@ -29,7 +29,7 @@ export default function BasicColor() {
};
return (
-
+
+
= {
slotProps: {
legend: {
position: {
vertical: 'middle',
horizontal: 'right',
},
- direction: 'column',
- itemGap: 2,
+ direction: 'vertical',
},
},
margin: {
top: 20,
- right: 150,
- left: 20,
+ right: 20,
+ left: 30,
},
-} as const;
+};
const series = [
{ label: 'Series 1', data: getGaussianSeriesData([-5, 0]) },
{ label: 'Series 2', data: getGaussianSeriesData([-4, 0]) },
diff --git a/docs/data/charts/styling/GradientTooltip.js b/docs/data/charts/styling/GradientTooltip.js
index 91d3f04fdbacd..b18e442dbaa09 100644
--- a/docs/data/charts/styling/GradientTooltip.js
+++ b/docs/data/charts/styling/GradientTooltip.js
@@ -4,34 +4,40 @@ import { BarChart } from '@mui/x-charts/BarChart';
export default function GradientTooltip() {
return (
-
+
+
+
+
+
);
}
diff --git a/docs/data/charts/styling/GradientTooltip.tsx b/docs/data/charts/styling/GradientTooltip.tsx
index 91d3f04fdbacd..b18e442dbaa09 100644
--- a/docs/data/charts/styling/GradientTooltip.tsx
+++ b/docs/data/charts/styling/GradientTooltip.tsx
@@ -4,34 +4,40 @@ import { BarChart } from '@mui/x-charts/BarChart';
export default function GradientTooltip() {
return (
-
+
+
+
+
+
);
}
diff --git a/docs/data/charts/styling/MuiColorTemplate.js b/docs/data/charts/styling/MuiColorTemplate.js
index 0e64f20d0bcac..8d3283df006c9 100644
--- a/docs/data/charts/styling/MuiColorTemplate.js
+++ b/docs/data/charts/styling/MuiColorTemplate.js
@@ -38,17 +38,12 @@ function getGaussianSeriesData(mean, stdev = [0.3, 0.4], N = 50) {
const legendPlacement = {
slotProps: {
legend: {
- position: {
- vertical: 'middle',
- horizontal: 'right',
- },
- direction: 'column',
- itemGap: 2,
+ direction: 'vertical',
},
},
margin: {
top: 20,
- right: 100,
+ right: 20,
},
};
diff --git a/docs/data/charts/styling/MuiColorTemplate.tsx b/docs/data/charts/styling/MuiColorTemplate.tsx
index aca47cdc1f298..89dc8dedec6bb 100644
--- a/docs/data/charts/styling/MuiColorTemplate.tsx
+++ b/docs/data/charts/styling/MuiColorTemplate.tsx
@@ -9,7 +9,7 @@ import Button from '@mui/material/Button';
import Brightness4Icon from '@mui/icons-material/Brightness4';
import Brightness7Icon from '@mui/icons-material/Brightness7';
import { Chance } from 'chance';
-import { ScatterChart } from '@mui/x-charts/ScatterChart';
+import { ScatterChart, ScatterChartProps } from '@mui/x-charts/ScatterChart';
import { ScatterValueType } from '@mui/x-charts/models';
import {
blueberryTwilightPalette,
@@ -39,22 +39,17 @@ function getGaussianSeriesData(
});
}
-const legendPlacement = {
+const legendPlacement: Partial = {
slotProps: {
legend: {
- position: {
- vertical: 'middle',
- horizontal: 'right',
- },
- direction: 'column',
- itemGap: 2,
+ direction: 'vertical',
},
},
margin: {
top: 20,
- right: 100,
+ right: 20,
},
-} as const;
+};
const series = [
{ label: 'Series 1', data: getGaussianSeriesData([-5, 0]) },
diff --git a/docs/data/charts/styling/PatternPie.js b/docs/data/charts/styling/PatternPie.js
deleted file mode 100644
index 02f53eebf0161..0000000000000
--- a/docs/data/charts/styling/PatternPie.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import * as React from 'react';
-import { PieChart } from '@mui/x-charts/PieChart';
-
-export default function PatternPie() {
- return (
-
-
-
-
-
-
- );
-}
diff --git a/docs/data/charts/styling/PatternPie.tsx b/docs/data/charts/styling/PatternPie.tsx
deleted file mode 100644
index 02f53eebf0161..0000000000000
--- a/docs/data/charts/styling/PatternPie.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import * as React from 'react';
-import { PieChart } from '@mui/x-charts/PieChart';
-
-export default function PatternPie() {
- return (
-
-
-
-
-
-
- );
-}
diff --git a/docs/data/charts/styling/styling.md b/docs/data/charts/styling/styling.md
index fc64c9cb923d9..6ec9ede685dcb 100644
--- a/docs/data/charts/styling/styling.md
+++ b/docs/data/charts/styling/styling.md
@@ -171,15 +171,6 @@ It is possible to use gradients and patterns to fill the charts.
This can be done by passing your gradient or pattern definition as children of the chart component.
Note that the gradient or pattern defined that way is only usable for SVG.
-So a direct definition like `color: "url(#Pattern)'` would cause undefined colors in HTML elements such as the tooltip.
-The demo solves this issue by using a CSS variable `'--my-custom-pattern': 'url(#Pattern)'` to specify fallback color with `color: 'var(--my-custom-pattern, #123456)'`.
-
-{{"demo": "PatternPie.js"}}
-
-#### Using gradients on tooltips
-
-Gradients defined as SVG elements are not directly supported in HTML.
-However you can use the [gradient functions](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#gradient_functions) to define a gradient in CSS.
-This gradient can be used in the tooltip by setting the `sx` prop on the tooltip component, instead of the fallback color used in the previous examples.
+So a direct definition like `color: "url(#Pattern)'` would cause undefined colors in HTML elements.
{{"demo": "GradientTooltip.js"}}
diff --git a/docs/data/charts/tooltip/AxisFormatter.js b/docs/data/charts/tooltip/AxisFormatter.js
index ec92663310f40..ef3ff43fb3fa1 100644
--- a/docs/data/charts/tooltip/AxisFormatter.js
+++ b/docs/data/charts/tooltip/AxisFormatter.js
@@ -48,6 +48,7 @@ const chartParams = {
dataset,
width: 600,
height: 400,
+ margin: { left: 60 },
sx: {
[`.${axisClasses.left} .${axisClasses.label}`]: {
transform: 'translate(-20px, 0)',
diff --git a/docs/data/charts/tooltip/AxisFormatter.tsx b/docs/data/charts/tooltip/AxisFormatter.tsx
index 0a687484e96b9..d0781688dbdc4 100644
--- a/docs/data/charts/tooltip/AxisFormatter.tsx
+++ b/docs/data/charts/tooltip/AxisFormatter.tsx
@@ -48,6 +48,7 @@ const chartParams: BarChartProps = {
dataset,
width: 600,
height: 400,
+ margin: { left: 60 },
sx: {
[`.${axisClasses.left} .${axisClasses.label}`]: {
transform: 'translate(-20px, 0)',
diff --git a/docs/data/charts/tooltip/CustomAxisTooltip.js b/docs/data/charts/tooltip/CustomAxisTooltip.js
index 9689757aa66a9..34e877e571616 100644
--- a/docs/data/charts/tooltip/CustomAxisTooltip.js
+++ b/docs/data/charts/tooltip/CustomAxisTooltip.js
@@ -28,11 +28,11 @@ export function CustomAxisTooltip() {
},
},
tbody: {
- 'tr:first-child': { td: { paddingTop: 1.5 } },
- 'tr:last-child': { td: { paddingBottom: 1.5 } },
+ 'tr:first-of-type': { td: { paddingTop: 1.5 } },
+ 'tr:last-of-type': { td: { paddingBottom: 1.5 } },
tr: {
- 'td:first-child': { paddingLeft: 1.5 },
- 'td:last-child': { paddingRight: 1.5 },
+ 'td:first-of-type': { paddingLeft: 1.5 },
+ 'td:last-of-type': { paddingRight: 1.5 },
td: {
paddingRight: '7px',
paddingBottom: '10px',
diff --git a/docs/data/charts/tooltip/CustomAxisTooltip.tsx b/docs/data/charts/tooltip/CustomAxisTooltip.tsx
index 9689757aa66a9..34e877e571616 100644
--- a/docs/data/charts/tooltip/CustomAxisTooltip.tsx
+++ b/docs/data/charts/tooltip/CustomAxisTooltip.tsx
@@ -28,11 +28,11 @@ export function CustomAxisTooltip() {
},
},
tbody: {
- 'tr:first-child': { td: { paddingTop: 1.5 } },
- 'tr:last-child': { td: { paddingBottom: 1.5 } },
+ 'tr:first-of-type': { td: { paddingTop: 1.5 } },
+ 'tr:last-of-type': { td: { paddingBottom: 1.5 } },
tr: {
- 'td:first-child': { paddingLeft: 1.5 },
- 'td:last-child': { paddingRight: 1.5 },
+ 'td:first-of-type': { paddingLeft: 1.5 },
+ 'td:last-of-type': { paddingRight: 1.5 },
td: {
paddingRight: '7px',
paddingBottom: '10px',
diff --git a/docs/data/charts/tooltip/ItemTooltipFixedY.js b/docs/data/charts/tooltip/ItemTooltipFixedY.js
index 93f94d31c0e80..42d45c9576292 100644
--- a/docs/data/charts/tooltip/ItemTooltipFixedY.js
+++ b/docs/data/charts/tooltip/ItemTooltipFixedY.js
@@ -67,7 +67,7 @@ export function ItemTooltipFixedY({ children }) {
const handleMove = (event) => {
positionRef.current = {
x: event.clientX,
- y: event.clientY,
+ y: (svgRef.current?.getBoundingClientRect().top ?? 0) + drawingArea.top,
};
popperRef.current?.update();
};
@@ -99,8 +99,8 @@ export function ItemTooltipFixedY({ children }) {
y: positionRef.current.y,
top: positionRef.current.y,
left: positionRef.current.x,
- right: positionRef.current.x,
- bottom: positionRef.current.y,
+ right: 0,
+ bottom: 0,
width: 0,
height: 0,
toJSON: () => '',
diff --git a/docs/data/charts/tooltip/ItemTooltipFixedY.tsx b/docs/data/charts/tooltip/ItemTooltipFixedY.tsx
index 70c7bf29ec7db..84dcb6f7e7740 100644
--- a/docs/data/charts/tooltip/ItemTooltipFixedY.tsx
+++ b/docs/data/charts/tooltip/ItemTooltipFixedY.tsx
@@ -73,7 +73,7 @@ export function ItemTooltipFixedY({ children }: React.PropsWithChildren) {
const handleMove = (event: PointerEvent) => {
positionRef.current = {
x: event.clientX,
- y: event.clientY,
+ y: (svgRef.current?.getBoundingClientRect().top ?? 0) + drawingArea.top,
};
popperRef.current?.update();
};
@@ -105,8 +105,8 @@ export function ItemTooltipFixedY({ children }: React.PropsWithChildren) {
y: positionRef.current.y,
top: positionRef.current.y,
left: positionRef.current.x,
- right: positionRef.current.x,
- bottom: positionRef.current.y,
+ right: 0,
+ bottom: 0,
width: 0,
height: 0,
toJSON: () => '',
diff --git a/docs/data/charts/tooltip/SeriesFormatter.js b/docs/data/charts/tooltip/SeriesFormatter.js
index d876c031babbe..d1a841ec1c134 100644
--- a/docs/data/charts/tooltip/SeriesFormatter.js
+++ b/docs/data/charts/tooltip/SeriesFormatter.js
@@ -3,7 +3,7 @@ import { PieChart } from '@mui/x-charts/PieChart';
import { legendClasses } from '@mui/x-charts/ChartsLegend';
const otherProps = {
- width: 400,
+ width: 200,
height: 200,
sx: {
[`.${legendClasses.root}`]: {
diff --git a/docs/data/charts/tooltip/SeriesFormatter.tsx b/docs/data/charts/tooltip/SeriesFormatter.tsx
index 030064774ba97..b60633f83fe75 100644
--- a/docs/data/charts/tooltip/SeriesFormatter.tsx
+++ b/docs/data/charts/tooltip/SeriesFormatter.tsx
@@ -3,7 +3,7 @@ import { PieChart, PieChartProps } from '@mui/x-charts/PieChart';
import { legendClasses } from '@mui/x-charts/ChartsLegend';
const otherProps: Partial = {
- width: 400,
+ width: 200,
height: 200,
sx: {
[`.${legendClasses.root}`]: {
diff --git a/docs/data/charts/zoom-and-pan/ZoomControlled.js b/docs/data/charts/zoom-and-pan/ZoomControlled.js
index e7232b9c5bd7a..9eaab87027c0b 100644
--- a/docs/data/charts/zoom-and-pan/ZoomControlled.js
+++ b/docs/data/charts/zoom-and-pan/ZoomControlled.js
@@ -1,24 +1,28 @@
import * as React from 'react';
import { LineChartPro } from '@mui/x-charts-pro/LineChartPro';
-
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
+const initialZoomData = [
+ {
+ axisId: 'my-x-axis',
+ start: 20,
+ end: 40,
+ },
+];
+
export default function ZoomControlled() {
- const [zoom, setZoom] = React.useState([
- {
- axisId: 'my-x-axis',
- start: 20,
- end: 40,
- },
- ]);
+ const apiRef = React.useRef(undefined);
+
+ const [zoomData, setZoomData] = React.useState(initialZoomData);
return (
setZoomData(newZoomData)}
xAxis={[
{
zoom: true,
@@ -28,9 +32,12 @@ export default function ZoomControlled() {
},
]}
/>
+ {JSON.stringify(zoomData, null, 2)}
setZoom([{ axisId: 'my-x-axis', start: 0, end: 100 }])}
+ onClick={() =>
+ apiRef.current.setZoomData([{ axisId: 'my-x-axis', start: 0, end: 100 }])
+ }
>
Reset zoom
diff --git a/docs/data/charts/zoom-and-pan/ZoomControlled.tsx b/docs/data/charts/zoom-and-pan/ZoomControlled.tsx
index e76583385576d..859930b220901 100644
--- a/docs/data/charts/zoom-and-pan/ZoomControlled.tsx
+++ b/docs/data/charts/zoom-and-pan/ZoomControlled.tsx
@@ -1,24 +1,29 @@
import * as React from 'react';
import { LineChartPro } from '@mui/x-charts-pro/LineChartPro';
-import { ZoomData } from '@mui/x-charts-pro/context';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
+import { ChartPublicAPI, ZoomData } from '@mui/x-charts/internals';
+const initialZoomData: ZoomData[] = [
+ {
+ axisId: 'my-x-axis',
+ start: 20,
+ end: 40,
+ },
+];
export default function ZoomControlled() {
- const [zoom, setZoom] = React.useState([
- {
- axisId: 'my-x-axis',
- start: 20,
- end: 40,
- },
- ]);
+ const apiRef = React.useRef(undefined) as React.MutableRefObject<
+ ChartPublicAPI<[any]> | undefined
+ >;
+ const [zoomData, setZoomData] = React.useState(initialZoomData);
return (
setZoomData(newZoomData)}
xAxis={[
{
zoom: true,
@@ -28,9 +33,12 @@ export default function ZoomControlled() {
},
]}
/>
+ {JSON.stringify(zoomData, null, 2)}
setZoom([{ axisId: 'my-x-axis', start: 0, end: 100 }])}
+ onClick={() =>
+ apiRef.current.setZoomData([{ axisId: 'my-x-axis', start: 0, end: 100 }])
+ }
>
Reset zoom
diff --git a/docs/data/charts/zoom-and-pan/zoom-and-pan.md b/docs/data/charts/zoom-and-pan/zoom-and-pan.md
index ef41a64022500..7d5b397f336fc 100644
--- a/docs/data/charts/zoom-and-pan/zoom-and-pan.md
+++ b/docs/data/charts/zoom-and-pan/zoom-and-pan.md
@@ -41,21 +41,6 @@ The following options are available:
{{"demo": "ZoomOptionsNoSnap.js", "hideToolbar": true, "bg": "playground"}}
-## Controlled zoom
-
-You can control the zoom state by setting the `zoom` and `onZoomChange` props.
-This way, you can control the zoom state from outside the chart.
-
-The `onZoomChange` prop is a function that receives the new zoom state.
-
-While the `zoom` prop is an array of objects that define the zoom state for each axis with zoom enabled.
-
-- **axisId**: The id of the axis to control.
-- **start**: The starting percentage of the axis range.
-- **end**: The ending percentage of the zoom range.
-
-{{"demo": "ZoomControlled.js"}}
-
## Zoom filtering
You can make the zoom of an axis affect one or more axes extremums by setting the `zoom.filterMode` prop on the axis config.
@@ -66,3 +51,20 @@ You can make the zoom of an axis affect one or more axes extremums by setting th
See how the secondary axis adapts to the visible part of the primary axis in the following example.
{{"demo": "ZoomFilterMode.js"}}
+
+## External zoom management
+
+You can manage the zoom state by two means:
+
+- By defining an initial state with the `initialZoom` prop.
+- By imperatively set a zoom value with the `setZoomData` method of the public api.
+
+In addition, the `onZoomChange` prop is a function that receives the new zoom state.
+
+The `zoom` state is an array of objects that define the zoom state for each axis with zoom enabled.
+
+- **axisId**: The id of the axis to control.
+- **start**: The starting percentage of the axis range.
+- **end**: The ending percentage of the zoom range.
+
+{{"demo": "ZoomControlled.js"}}
diff --git a/docs/data/chartsApiPages.ts b/docs/data/chartsApiPages.ts
index c4ec485b59c66..dbc2961b5e3d8 100644
--- a/docs/data/chartsApiPages.ts
+++ b/docs/data/chartsApiPages.ts
@@ -119,10 +119,6 @@ const chartsApiPages: MuiPage[] = [
pathname: '/x/api/charts/continuous-color-legend',
title: 'ContinuousColorLegend',
},
- {
- pathname: '/x/api/charts/default-charts-legend',
- title: 'DefaultChartsLegend',
- },
{
pathname: '/x/api/charts/gauge',
title: 'Gauge',
diff --git a/docs/data/data-grid/accessibility/accessibility.md b/docs/data/data-grid/accessibility/accessibility.md
index f1f0773763d95..ae24f6969d30d 100644
--- a/docs/data/data-grid/accessibility/accessibility.md
+++ b/docs/data/data-grid/accessibility/accessibility.md
@@ -10,7 +10,7 @@ Common conformance guidelines for accessibility include:
- US:
- [ADA](https://www.ada.gov/) - US Department of Justice
- [Section 508](https://www.section508.gov/) - US federal agencies
-- Europe: [EAA](https://ec.europa.eu/social/main.jsp?catId=1202) (European Accessibility Act)
+- Europe: [EAA](https://employment-social-affairs.ec.europa.eu/policies-and-activities/social-protection-social-inclusion/persons-disabilities/union-equality-strategy-rights-persons-disabilities-2021-2030/european-accessibility-act_en) (European Accessibility Act)
WCAG 2.1 has three levels of conformance: A, AA, and AAA.
Level AA exceeds the basic criteria for accessibility and is a common target for most organizations, so this is what we aim to support.
diff --git a/docs/data/data-grid/aggregation/aggregation.md b/docs/data/data-grid/aggregation/aggregation.md
index 0dad0c32852c2..0f9b5de14250b 100644
--- a/docs/data/data-grid/aggregation/aggregation.md
+++ b/docs/data/data-grid/aggregation/aggregation.md
@@ -12,6 +12,10 @@ The aggregated values are rendered in a footer row at the bottom of the Data Gri
{{"demo": "AggregationInitialState.js", "bg": "inline", "defaultCodeOpen": false}}
+:::info
+If you're looking for aggregation on the server side, see [Server-side data—Aggregation](/x/react-data-grid/server-side-data/aggregation/).
+:::
+
## Pass aggregation to the Data Grid
### Structure of the model
diff --git a/docs/data/data-grid/api-object/UseGridApiRef.js b/docs/data/data-grid/api-object/UseGridApiRef.js
index e7535dc38eb1d..17b7e1aac96d9 100644
--- a/docs/data/data-grid/api-object/UseGridApiRef.js
+++ b/docs/data/data-grid/api-object/UseGridApiRef.js
@@ -32,6 +32,7 @@ export default function UseGridApiRef() {
pagination
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
+ pageSizeOptions={[10, 25, 50, 100]}
/>
diff --git a/docs/data/data-grid/api-object/UseGridApiRef.tsx b/docs/data/data-grid/api-object/UseGridApiRef.tsx
index e7535dc38eb1d..17b7e1aac96d9 100644
--- a/docs/data/data-grid/api-object/UseGridApiRef.tsx
+++ b/docs/data/data-grid/api-object/UseGridApiRef.tsx
@@ -32,6 +32,7 @@ export default function UseGridApiRef() {
pagination
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
+ pageSizeOptions={[10, 25, 50, 100]}
/>
diff --git a/docs/data/data-grid/api-object/UseGridApiRef.tsx.preview b/docs/data/data-grid/api-object/UseGridApiRef.tsx.preview
index c28263ca7dd39..3025c4557fe49 100644
--- a/docs/data/data-grid/api-object/UseGridApiRef.tsx.preview
+++ b/docs/data/data-grid/api-object/UseGridApiRef.tsx.preview
@@ -7,5 +7,6 @@
pagination
paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel}
+ pageSizeOptions={[10, 25, 50, 100]}
/>
\ No newline at end of file
diff --git a/docs/data/data-grid/column-recipes/ColumnSizingPersistWidthOrder.tsx b/docs/data/data-grid/column-recipes/ColumnSizingPersistWidthOrder.tsx
index a1880f3e70857..ab02705eecc86 100644
--- a/docs/data/data-grid/column-recipes/ColumnSizingPersistWidthOrder.tsx
+++ b/docs/data/data-grid/column-recipes/ColumnSizingPersistWidthOrder.tsx
@@ -50,7 +50,7 @@ export default function ColumnSizingPersistWidthOrder() {
}
const useColumnsState = (
- apiRef: React.MutableRefObject,
+ apiRef: React.RefObject,
columns: GridColDef[],
) => {
const [widths, setWidths] = React.useState>(
diff --git a/docs/data/data-grid/components/CustomColumnsPanel.tsx b/docs/data/data-grid/components/CustomColumnsPanel.tsx
index 1516ba3e1cf7b..0d42eb3be4509 100644
--- a/docs/data/data-grid/components/CustomColumnsPanel.tsx
+++ b/docs/data/data-grid/components/CustomColumnsPanel.tsx
@@ -43,7 +43,7 @@ function ColumnGroup({
}: {
group: GridColumnGroup;
columnLookup: GridColumnLookup;
- apiRef: React.MutableRefObject;
+ apiRef: React.RefObject;
columnVisibilityModel: GridColumnVisibilityModel;
}) {
const leaves = React.useMemo(() => {
diff --git a/docs/data/data-grid/custom-columns/EditingWithDatePickers.js b/docs/data/data-grid/custom-columns/EditingWithDatePickers.js
index 3418d179a1c70..14da0d2c568ef 100644
--- a/docs/data/data-grid/custom-columns/EditingWithDatePickers.js
+++ b/docs/data/data-grid/custom-columns/EditingWithDatePickers.js
@@ -15,12 +15,9 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
-import InputBase from '@mui/material/InputBase';
import { enUS as locale } from 'date-fns/locale';
-import { styled } from '@mui/material/styles';
-
-const dateAdapter = new AdapterDateFns({ locale });
-
+import { format } from 'date-fns/format';
+import { unstable_useEnhancedEffect as useEnhancedEffect } from '@mui/utils';
/**
* `date` column
*/
@@ -38,37 +35,47 @@ const dateColumnType = {
})),
valueFormatter: (value) => {
if (value) {
- return dateAdapter.format(value, 'keyboardDate');
+ return format(value, 'MM/dd/yyyy', { locale });
}
return '';
},
};
-const GridEditDateInput = styled(InputBase)({
- fontSize: 'inherit',
- padding: '0 9px',
-});
-
-function WrappedGridEditDateInput(props) {
- const { InputProps, focused, ...other } = props;
- return ;
-}
-
-function GridEditDateCell({ id, field, value, colDef }) {
+function GridEditDateCell({ id, field, value, colDef, hasFocus }) {
const apiRef = useGridApiContext();
-
+ const inputRef = React.useRef(null);
const Component = colDef.type === 'dateTime' ? DateTimePicker : DatePicker;
const handleChange = (newValue) => {
apiRef.current.setEditCellValue({ id, field, value: newValue });
};
+ useEnhancedEffect(() => {
+ if (hasFocus) {
+ inputRef.current.focus();
+ }
+ }, [hasFocus]);
+
return (
);
}
@@ -87,18 +94,7 @@ function GridFilterDateInput(props) {
value={item.value ? new Date(item.value) : null}
autoFocus
label={apiRef.current.getLocaleText('filterPanelInputLabel')}
- slotProps={{
- textField: {
- variant: 'standard',
- },
- inputAdornment: {
- sx: {
- '& .MuiButtonBase-root': {
- marginRight: -1,
- },
- },
- },
- }}
+ slotProps={{ textField: { size: 'small' } }}
onChange={handleFilterChange}
/>
);
@@ -121,7 +117,7 @@ const dateTimeColumnType = {
})),
valueFormatter: (value) => {
if (value) {
- return dateAdapter.format(value, 'keyboardDateTime');
+ return format(value, 'MM/dd/yyyy hh:mm a', { locale });
}
return '';
},
diff --git a/docs/data/data-grid/custom-columns/EditingWithDatePickers.tsx b/docs/data/data-grid/custom-columns/EditingWithDatePickers.tsx
index a381b48b922bd..12505f43ca9ea 100644
--- a/docs/data/data-grid/custom-columns/EditingWithDatePickers.tsx
+++ b/docs/data/data-grid/custom-columns/EditingWithDatePickers.tsx
@@ -20,13 +20,9 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
-import InputBase, { InputBaseProps } from '@mui/material/InputBase';
import { enUS as locale } from 'date-fns/locale';
-import { styled } from '@mui/material/styles';
-import { TextFieldProps } from '@mui/material/TextField';
-
-const dateAdapter = new AdapterDateFns({ locale });
-
+import { format } from 'date-fns/format';
+import { unstable_useEnhancedEffect as useEnhancedEffect } from '@mui/utils';
/**
* `date` column
*/
@@ -44,44 +40,53 @@ const dateColumnType: GridColTypeDef = {
})),
valueFormatter: (value) => {
if (value) {
- return dateAdapter.format(value, 'keyboardDate');
+ return format(value, 'MM/dd/yyyy', { locale });
}
return '';
},
};
-const GridEditDateInput = styled(InputBase)({
- fontSize: 'inherit',
- padding: '0 9px',
-});
-
-function WrappedGridEditDateInput(props: TextFieldProps) {
- const { InputProps, focused, ...other } = props;
- return (
-
- );
-}
-
function GridEditDateCell({
id,
field,
value,
colDef,
+ hasFocus,
}: GridRenderEditCellParams) {
const apiRef = useGridApiContext();
-
+ const inputRef = React.useRef(null);
const Component = colDef.type === 'dateTime' ? DateTimePicker : DatePicker;
const handleChange = (newValue: unknown) => {
apiRef.current.setEditCellValue({ id, field, value: newValue });
};
+ useEnhancedEffect(() => {
+ if (hasFocus) {
+ inputRef.current!.focus();
+ }
+ }, [hasFocus]);
+
return (
);
}
@@ -102,18 +107,7 @@ function GridFilterDateInput(
value={item.value ? new Date(item.value) : null}
autoFocus
label={apiRef.current.getLocaleText('filterPanelInputLabel')}
- slotProps={{
- textField: {
- variant: 'standard',
- },
- inputAdornment: {
- sx: {
- '& .MuiButtonBase-root': {
- marginRight: -1,
- },
- },
- },
- }}
+ slotProps={{ textField: { size: 'small' } }}
onChange={handleFilterChange}
/>
);
@@ -136,7 +130,7 @@ const dateTimeColumnType: GridColTypeDef = {
})),
valueFormatter: (value) => {
if (value) {
- return dateAdapter.format(value, 'keyboardDateTime');
+ return format(value, 'MM/dd/yyyy hh:mm a', { locale });
}
return '';
},
diff --git a/docs/data/data-grid/export/CustomExport.tsx b/docs/data/data-grid/export/CustomExport.tsx
index eef4ed05cd01e..22800b00cdb61 100644
--- a/docs/data/data-grid/export/CustomExport.tsx
+++ b/docs/data/data-grid/export/CustomExport.tsx
@@ -16,7 +16,7 @@ import {
import MenuItem from '@mui/material/MenuItem';
import { ButtonProps } from '@mui/material/Button';
-const getJson = (apiRef: React.MutableRefObject) => {
+const getJson = (apiRef: React.RefObject) => {
// Select rows and columns
const filteredSortedRowIds = gridFilteredSortedRowIdsSelector(apiRef);
const visibleColumnsField = gridVisibleColumnFieldsSelector(apiRef);
diff --git a/docs/data/data-grid/filtering-recipes/QuickFilterOutsideOfGrid.js b/docs/data/data-grid/filtering-recipes/QuickFilterOutsideOfGrid.js
index 5649acb1c0ea7..7d5ae0bc0b06a 100644
--- a/docs/data/data-grid/filtering-recipes/QuickFilterOutsideOfGrid.js
+++ b/docs/data/data-grid/filtering-recipes/QuickFilterOutsideOfGrid.js
@@ -2,14 +2,21 @@ import * as React from 'react';
import Portal from '@mui/material/Portal';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
-import { DataGrid, GridToolbarQuickFilter, GridToolbar } from '@mui/x-data-grid';
+import {
+ DataGrid,
+ GridPortalWrapper,
+ GridToolbarQuickFilter,
+ GridToolbar,
+} from '@mui/x-data-grid';
import { useDemoData } from '@mui/x-data-grid-generator';
function MyCustomToolbar(props) {
return (
document.getElementById('filter-panel')}>
-
+
+
+
diff --git a/docs/data/data-grid/filtering-recipes/QuickFilterOutsideOfGrid.tsx b/docs/data/data-grid/filtering-recipes/QuickFilterOutsideOfGrid.tsx
index 71b254306e90c..45e49efecb35d 100644
--- a/docs/data/data-grid/filtering-recipes/QuickFilterOutsideOfGrid.tsx
+++ b/docs/data/data-grid/filtering-recipes/QuickFilterOutsideOfGrid.tsx
@@ -2,14 +2,21 @@ import * as React from 'react';
import Portal from '@mui/material/Portal';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
-import { DataGrid, GridToolbarQuickFilter, GridToolbar } from '@mui/x-data-grid';
+import {
+ DataGrid,
+ GridPortalWrapper,
+ GridToolbarQuickFilter,
+ GridToolbar,
+} from '@mui/x-data-grid';
import { useDemoData } from '@mui/x-data-grid-generator';
function MyCustomToolbar(props: any) {
return (
document.getElementById('filter-panel')!}>
-
+
+
+
diff --git a/docs/data/data-grid/filtering/CustomMultiValueOperator.js b/docs/data/data-grid/filtering/CustomMultiValueOperator.js
index 7155e509dc995..4dbfb04c5a87e 100644
--- a/docs/data/data-grid/filtering/CustomMultiValueOperator.js
+++ b/docs/data/data-grid/filtering/CustomMultiValueOperator.js
@@ -9,7 +9,7 @@ function InputNumberInterval(props) {
const rootProps = useGridRootProps();
const { item, applyValue, focusElementRef = null } = props;
- const filterTimeout = React.useRef();
+ const filterTimeout = React.useRef(undefined);
const [filterValueState, setFilterValueState] = React.useState(item.value ?? '');
const [applying, setIsApplying] = React.useState(false);
diff --git a/docs/data/data-grid/filtering/CustomMultiValueOperator.tsx b/docs/data/data-grid/filtering/CustomMultiValueOperator.tsx
index 0ac2080fb364b..d1d777e8b4848 100644
--- a/docs/data/data-grid/filtering/CustomMultiValueOperator.tsx
+++ b/docs/data/data-grid/filtering/CustomMultiValueOperator.tsx
@@ -15,7 +15,7 @@ function InputNumberInterval(props: GridFilterInputValueProps) {
const rootProps = useGridRootProps();
const { item, applyValue, focusElementRef = null } = props;
- const filterTimeout = React.useRef();
+ const filterTimeout = React.useRef>(undefined);
const [filterValueState, setFilterValueState] = React.useState<[string, string]>(
item.value ?? '',
);
diff --git a/docs/data/data-grid/layout/MinMaxHeightGrid.tsx b/docs/data/data-grid/layout/MinMaxHeightGrid.tsx
index 79bb6db519699..66d52455a4a87 100644
--- a/docs/data/data-grid/layout/MinMaxHeightGrid.tsx
+++ b/docs/data/data-grid/layout/MinMaxHeightGrid.tsx
@@ -53,7 +53,7 @@ export default function MinMaxHeightGrid() {
function ContainerMeasurements({
containerRef,
}: {
- containerRef: React.RefObject;
+ containerRef: React.RefObject;
}) {
const [containerHeight, setContainerHeight] = React.useState(0);
diff --git a/docs/data/data-grid/localization/data.json b/docs/data/data-grid/localization/data.json
index ebda6d194b985..4c3d17272b69b 100644
--- a/docs/data/data-grid/localization/data.json
+++ b/docs/data/data-grid/localization/data.json
@@ -75,7 +75,7 @@
"languageTag": "nl-NL",
"importName": "nlNL",
"localeName": "Dutch",
- "missingKeysCount": 14,
+ "missingKeysCount": 9,
"totalKeysCount": 132,
"githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/nlNL.ts"
},
diff --git a/docs/data/data-grid/pagination/CursorPaginationGrid.js b/docs/data/data-grid/pagination/CursorPaginationGrid.js
index 1815b05771007..2bc291793b737 100644
--- a/docs/data/data-grid/pagination/CursorPaginationGrid.js
+++ b/docs/data/data-grid/pagination/CursorPaginationGrid.js
@@ -48,7 +48,7 @@ export default function CursorPaginationGrid() {
}
};
- const paginationMetaRef = React.useRef();
+ const paginationMetaRef = React.useRef(undefined);
// Memoize to avoid flickering when the `hasNextPage` is `undefined` during refetch
const paginationMeta = React.useMemo(() => {
diff --git a/docs/data/data-grid/pagination/CursorPaginationGrid.tsx b/docs/data/data-grid/pagination/CursorPaginationGrid.tsx
index 113e1b7945c45..4d92a2a68be5c 100644
--- a/docs/data/data-grid/pagination/CursorPaginationGrid.tsx
+++ b/docs/data/data-grid/pagination/CursorPaginationGrid.tsx
@@ -55,7 +55,7 @@ export default function CursorPaginationGrid() {
}
};
- const paginationMetaRef = React.useRef();
+ const paginationMetaRef = React.useRef(undefined);
// Memoize to avoid flickering when the `hasNextPage` is `undefined` during refetch
const paginationMeta = React.useMemo(() => {
diff --git a/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js
index 2de106edc5d3e..239db20b0e972 100644
--- a/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js
+++ b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.js
@@ -23,7 +23,7 @@ export default function ServerPaginationGridNoRowCount() {
pageInfo: { hasNextPage },
} = useQuery(paginationModel);
- const paginationMetaRef = React.useRef();
+ const paginationMetaRef = React.useRef(undefined);
// Memoize to avoid flickering when the `hasNextPage` is `undefined` during refetch
const paginationMeta = React.useMemo(() => {
diff --git a/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx
index bb35ce14cebd5..98fdf4dcc1513 100644
--- a/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx
+++ b/docs/data/data-grid/pagination/ServerPaginationGridNoRowCount.tsx
@@ -23,7 +23,7 @@ export default function ServerPaginationGridNoRowCount() {
pageInfo: { hasNextPage },
} = useQuery(paginationModel);
- const paginationMetaRef = React.useRef();
+ const paginationMetaRef = React.useRef(undefined);
// Memoize to avoid flickering when the `hasNextPage` is `undefined` during refetch
const paginationMeta = React.useMemo(() => {
diff --git a/docs/data/data-grid/server-side-data/ServerSideDataGridAggregation.js b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregation.js
new file mode 100644
index 0000000000000..abeb33447b124
--- /dev/null
+++ b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregation.js
@@ -0,0 +1,70 @@
+import * as React from 'react';
+import { DataGridPremium } from '@mui/x-data-grid-premium';
+import { useMockServer } from '@mui/x-data-grid-generator';
+
+const aggregationFunctions = {
+ sum: { columnTypes: ['number'] },
+ avg: { columnTypes: ['number'] },
+ min: { columnTypes: ['number', 'date', 'dateTime'] },
+ max: { columnTypes: ['number', 'date', 'dateTime'] },
+ size: {},
+};
+
+export default function ServerSideDataGridAggregation() {
+ const { columns, initialState, fetchRows } = useMockServer(
+ {},
+ { useCursorPagination: false },
+ );
+
+ const dataSource = React.useMemo(
+ () => ({
+ getRows: async (params) => {
+ const urlParams = new URLSearchParams({
+ paginationModel: JSON.stringify(params.paginationModel),
+ filterModel: JSON.stringify(params.filterModel),
+ sortModel: JSON.stringify(params.sortModel),
+ aggregationModel: JSON.stringify(params.aggregationModel),
+ });
+ const getRowsResponse = await fetchRows(
+ `https://mui.com/x/api/data-grid?${urlParams.toString()}`,
+ );
+ return {
+ rows: getRowsResponse.rows,
+ rowCount: getRowsResponse.rowCount,
+ aggregateRow: getRowsResponse.aggregateRow,
+ };
+ },
+ getAggregatedValue: (row, field) => {
+ return row[`${field}Aggregate`];
+ },
+ }),
+ [fetchRows],
+ );
+
+ const initialStateWithPagination = React.useMemo(
+ () => ({
+ ...initialState,
+ pagination: {
+ paginationModel: { pageSize: 10, page: 0 },
+ rowCount: 0,
+ },
+ aggregation: {
+ model: { commodity: 'size', quantity: 'sum' },
+ },
+ }),
+ [initialState],
+ );
+
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/data-grid/server-side-data/ServerSideDataGridAggregation.tsx b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregation.tsx
new file mode 100644
index 0000000000000..c24b761cb58a9
--- /dev/null
+++ b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregation.tsx
@@ -0,0 +1,70 @@
+import * as React from 'react';
+import { DataGridPremium, GridDataSource } from '@mui/x-data-grid-premium';
+import { useMockServer } from '@mui/x-data-grid-generator';
+
+const aggregationFunctions = {
+ sum: { columnTypes: ['number'] },
+ avg: { columnTypes: ['number'] },
+ min: { columnTypes: ['number', 'date', 'dateTime'] },
+ max: { columnTypes: ['number', 'date', 'dateTime'] },
+ size: {},
+};
+
+export default function ServerSideDataGridAggregation() {
+ const { columns, initialState, fetchRows } = useMockServer(
+ {},
+ { useCursorPagination: false },
+ );
+
+ const dataSource: GridDataSource = React.useMemo(
+ () => ({
+ getRows: async (params) => {
+ const urlParams = new URLSearchParams({
+ paginationModel: JSON.stringify(params.paginationModel),
+ filterModel: JSON.stringify(params.filterModel),
+ sortModel: JSON.stringify(params.sortModel),
+ aggregationModel: JSON.stringify(params.aggregationModel),
+ });
+ const getRowsResponse = await fetchRows(
+ `https://mui.com/x/api/data-grid?${urlParams.toString()}`,
+ );
+ return {
+ rows: getRowsResponse.rows,
+ rowCount: getRowsResponse.rowCount,
+ aggregateRow: getRowsResponse.aggregateRow,
+ };
+ },
+ getAggregatedValue: (row, field) => {
+ return row[`${field}Aggregate`];
+ },
+ }),
+ [fetchRows],
+ );
+
+ const initialStateWithPagination = React.useMemo(
+ () => ({
+ ...initialState,
+ pagination: {
+ paginationModel: { pageSize: 10, page: 0 },
+ rowCount: 0,
+ },
+ aggregation: {
+ model: { commodity: 'size', quantity: 'sum' },
+ },
+ }),
+ [initialState],
+ );
+
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/data-grid/server-side-data/ServerSideDataGridAggregation.tsx.preview b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregation.tsx.preview
new file mode 100644
index 0000000000000..aee05887a7c35
--- /dev/null
+++ b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregation.tsx.preview
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationLazyLoading.js b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationLazyLoading.js
new file mode 100644
index 0000000000000..0b7c242658a8b
--- /dev/null
+++ b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationLazyLoading.js
@@ -0,0 +1,71 @@
+import * as React from 'react';
+import { DataGridPremium } from '@mui/x-data-grid-premium';
+import { useMockServer } from '@mui/x-data-grid-generator';
+
+const aggregationFunctions = {
+ sum: { columnTypes: ['number'] },
+ avg: { columnTypes: ['number'] },
+ min: { columnTypes: ['number', 'date', 'dateTime'] },
+ max: { columnTypes: ['number', 'date', 'dateTime'] },
+ size: {},
+};
+
+export default function ServerSideDataGridAggregationLazyLoading() {
+ const {
+ columns,
+ initialState: initState,
+ fetchRows,
+ } = useMockServer({}, { useCursorPagination: false });
+
+ const dataSource = React.useMemo(
+ () => ({
+ getRows: async (params) => {
+ const urlParams = new URLSearchParams({
+ filterModel: JSON.stringify(params.filterModel),
+ sortModel: JSON.stringify(params.sortModel),
+ aggregationModel: JSON.stringify(params.aggregationModel),
+ start: `${params.start}`,
+ end: `${params.end}`,
+ });
+ const getRowsResponse = await fetchRows(
+ `https://mui.com/x/api/data-grid?${urlParams.toString()}`,
+ );
+ return {
+ rows: getRowsResponse.rows,
+ rowCount: getRowsResponse.rowCount,
+ aggregateRow: getRowsResponse.aggregateRow,
+ };
+ },
+ getAggregatedValue: (row, field) => {
+ return row[`${field}Aggregate`];
+ },
+ }),
+ [fetchRows],
+ );
+
+ const initialState = React.useMemo(
+ () => ({
+ ...initState,
+ pagination: {
+ paginationModel: { pageSize: 10, page: 0 },
+ rowCount: 0,
+ },
+ aggregation: {
+ model: { commodity: 'size', quantity: 'sum' },
+ },
+ }),
+ [initState],
+ );
+
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationLazyLoading.tsx b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationLazyLoading.tsx
new file mode 100644
index 0000000000000..eaeda275e5281
--- /dev/null
+++ b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationLazyLoading.tsx
@@ -0,0 +1,71 @@
+import * as React from 'react';
+import { DataGridPremium, GridDataSource } from '@mui/x-data-grid-premium';
+import { useMockServer } from '@mui/x-data-grid-generator';
+
+const aggregationFunctions = {
+ sum: { columnTypes: ['number'] },
+ avg: { columnTypes: ['number'] },
+ min: { columnTypes: ['number', 'date', 'dateTime'] },
+ max: { columnTypes: ['number', 'date', 'dateTime'] },
+ size: {},
+};
+
+export default function ServerSideDataGridAggregationLazyLoading() {
+ const {
+ columns,
+ initialState: initState,
+ fetchRows,
+ } = useMockServer({}, { useCursorPagination: false });
+
+ const dataSource: GridDataSource = React.useMemo(
+ () => ({
+ getRows: async (params) => {
+ const urlParams = new URLSearchParams({
+ filterModel: JSON.stringify(params.filterModel),
+ sortModel: JSON.stringify(params.sortModel),
+ aggregationModel: JSON.stringify(params.aggregationModel),
+ start: `${params.start}`,
+ end: `${params.end}`,
+ });
+ const getRowsResponse = await fetchRows(
+ `https://mui.com/x/api/data-grid?${urlParams.toString()}`,
+ );
+ return {
+ rows: getRowsResponse.rows,
+ rowCount: getRowsResponse.rowCount,
+ aggregateRow: getRowsResponse.aggregateRow,
+ };
+ },
+ getAggregatedValue: (row, field) => {
+ return row[`${field}Aggregate`];
+ },
+ }),
+ [fetchRows],
+ );
+
+ const initialState = React.useMemo(
+ () => ({
+ ...initState,
+ pagination: {
+ paginationModel: { pageSize: 10, page: 0 },
+ rowCount: 0,
+ },
+ aggregation: {
+ model: { commodity: 'size', quantity: 'sum' },
+ },
+ }),
+ [initState],
+ );
+
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationLazyLoading.tsx.preview b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationLazyLoading.tsx.preview
new file mode 100644
index 0000000000000..1856db25a89aa
--- /dev/null
+++ b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationLazyLoading.tsx.preview
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationRowGrouping.js b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationRowGrouping.js
new file mode 100644
index 0000000000000..a2467cefcc01a
--- /dev/null
+++ b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationRowGrouping.js
@@ -0,0 +1,76 @@
+import * as React from 'react';
+import {
+ DataGridPremium,
+ useKeepGroupedColumnsHidden,
+ useGridApiRef,
+} from '@mui/x-data-grid-premium';
+import { useMockServer } from '@mui/x-data-grid-generator';
+
+const aggregationFunctions = {
+ sum: { columnTypes: ['number'] },
+ avg: { columnTypes: ['number'] },
+ min: { columnTypes: ['number', 'date', 'dateTime'] },
+ max: { columnTypes: ['number', 'date', 'dateTime'] },
+ size: {},
+};
+
+export default function ServerSideDataGridAggregationRowGrouping() {
+ const apiRef = useGridApiRef();
+ const {
+ columns,
+ initialState: initState,
+ fetchRows,
+ } = useMockServer({ rowGrouping: true });
+
+ const dataSource = React.useMemo(
+ () => ({
+ getRows: async (params) => {
+ const urlParams = new URLSearchParams({
+ paginationModel: JSON.stringify(params.paginationModel),
+ filterModel: JSON.stringify(params.filterModel),
+ sortModel: JSON.stringify(params.sortModel),
+ groupKeys: JSON.stringify(params.groupKeys),
+ groupFields: JSON.stringify(params.groupFields),
+ aggregationModel: JSON.stringify(params.aggregationModel),
+ });
+ const getRowsResponse = await fetchRows(
+ `https://mui.com/x/api/data-grid?${urlParams.toString()}`,
+ );
+ return {
+ rows: getRowsResponse.rows,
+ rowCount: getRowsResponse.rowCount,
+ aggregateRow: getRowsResponse.aggregateRow,
+ };
+ },
+ getGroupKey: (row) => row.group,
+ getChildrenCount: (row) => row.descendantCount,
+ getAggregatedValue: (row, field) => row[`${field}Aggregate`],
+ }),
+ [fetchRows],
+ );
+
+ const initialState = useKeepGroupedColumnsHidden({
+ apiRef,
+ initialState: {
+ ...initState,
+ rowGrouping: {
+ model: ['company', 'director'],
+ },
+ aggregation: {
+ model: { title: 'size', gross: 'sum', year: 'max' },
+ },
+ },
+ });
+
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationRowGrouping.tsx b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationRowGrouping.tsx
new file mode 100644
index 0000000000000..afca284bd7259
--- /dev/null
+++ b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationRowGrouping.tsx
@@ -0,0 +1,77 @@
+import * as React from 'react';
+import {
+ DataGridPremium,
+ useKeepGroupedColumnsHidden,
+ useGridApiRef,
+ GridDataSource,
+} from '@mui/x-data-grid-premium';
+import { useMockServer } from '@mui/x-data-grid-generator';
+
+const aggregationFunctions = {
+ sum: { columnTypes: ['number'] },
+ avg: { columnTypes: ['number'] },
+ min: { columnTypes: ['number', 'date', 'dateTime'] },
+ max: { columnTypes: ['number', 'date', 'dateTime'] },
+ size: {},
+};
+
+export default function ServerSideDataGridAggregationRowGrouping() {
+ const apiRef = useGridApiRef();
+ const {
+ columns,
+ initialState: initState,
+ fetchRows,
+ } = useMockServer({ rowGrouping: true });
+
+ const dataSource: GridDataSource = React.useMemo(
+ () => ({
+ getRows: async (params) => {
+ const urlParams = new URLSearchParams({
+ paginationModel: JSON.stringify(params.paginationModel),
+ filterModel: JSON.stringify(params.filterModel),
+ sortModel: JSON.stringify(params.sortModel),
+ groupKeys: JSON.stringify(params.groupKeys),
+ groupFields: JSON.stringify(params.groupFields),
+ aggregationModel: JSON.stringify(params.aggregationModel),
+ });
+ const getRowsResponse = await fetchRows(
+ `https://mui.com/x/api/data-grid?${urlParams.toString()}`,
+ );
+ return {
+ rows: getRowsResponse.rows,
+ rowCount: getRowsResponse.rowCount,
+ aggregateRow: getRowsResponse.aggregateRow,
+ };
+ },
+ getGroupKey: (row) => row.group,
+ getChildrenCount: (row) => row.descendantCount,
+ getAggregatedValue: (row, field) => row[`${field}Aggregate`],
+ }),
+ [fetchRows],
+ );
+
+ const initialState = useKeepGroupedColumnsHidden({
+ apiRef,
+ initialState: {
+ ...initState,
+ rowGrouping: {
+ model: ['company', 'director'],
+ },
+ aggregation: {
+ model: { title: 'size', gross: 'sum', year: 'max' },
+ },
+ },
+ });
+
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationRowGrouping.tsx.preview b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationRowGrouping.tsx.preview
new file mode 100644
index 0000000000000..b2d4bf7135a06
--- /dev/null
+++ b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationRowGrouping.tsx.preview
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationTreeData.js b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationTreeData.js
new file mode 100644
index 0000000000000..3834980e55020
--- /dev/null
+++ b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationTreeData.js
@@ -0,0 +1,75 @@
+import * as React from 'react';
+import { DataGridPremium, useGridApiRef } from '@mui/x-data-grid-premium';
+import { useMockServer } from '@mui/x-data-grid-generator';
+
+const dataSetOptions = {
+ dataSet: 'Employee',
+ rowLength: 1000,
+ treeData: { maxDepth: 3, groupingField: 'name', averageChildren: 5 },
+};
+
+const aggregationFunctions = {
+ sum: { columnTypes: ['number'] },
+ avg: { columnTypes: ['number'] },
+ min: { columnTypes: ['number', 'date', 'dateTime'] },
+ max: { columnTypes: ['number', 'date', 'dateTime'] },
+ size: {},
+};
+
+export default function ServerSideDataGridAggregationTreeData() {
+ const apiRef = useGridApiRef();
+ const {
+ fetchRows,
+ columns,
+ initialState: initState,
+ } = useMockServer(dataSetOptions);
+
+ const initialState = React.useMemo(
+ () => ({
+ ...initState,
+ aggregation: {
+ model: { rating: 'avg', website: 'size' },
+ },
+ }),
+ [initState],
+ );
+
+ const dataSource = React.useMemo(
+ () => ({
+ getRows: async (params) => {
+ const urlParams = new URLSearchParams({
+ paginationModel: JSON.stringify(params.paginationModel),
+ filterModel: JSON.stringify(params.filterModel),
+ sortModel: JSON.stringify(params.sortModel),
+ groupKeys: JSON.stringify(params.groupKeys),
+ aggregationModel: JSON.stringify(params.aggregationModel),
+ });
+ const getRowsResponse = await fetchRows(
+ `https://mui.com/x/api/data-grid?${urlParams.toString()}`,
+ );
+ return {
+ rows: getRowsResponse.rows,
+ rowCount: getRowsResponse.rowCount,
+ aggregateRow: getRowsResponse.aggregateRow,
+ };
+ },
+ getGroupKey: (row) => row[dataSetOptions.treeData.groupingField],
+ getChildrenCount: (row) => row.descendantCount,
+ getAggregatedValue: (row, field) => row[`${field}Aggregate`],
+ }),
+ [fetchRows],
+ );
+
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationTreeData.tsx b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationTreeData.tsx
new file mode 100644
index 0000000000000..cf55fabc541d5
--- /dev/null
+++ b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationTreeData.tsx
@@ -0,0 +1,80 @@
+import * as React from 'react';
+import {
+ DataGridPremium,
+ useGridApiRef,
+ GridInitialState,
+ GridDataSource,
+} from '@mui/x-data-grid-premium';
+import { useMockServer } from '@mui/x-data-grid-generator';
+
+const dataSetOptions = {
+ dataSet: 'Employee' as const,
+ rowLength: 1000,
+ treeData: { maxDepth: 3, groupingField: 'name', averageChildren: 5 },
+};
+
+const aggregationFunctions = {
+ sum: { columnTypes: ['number'] },
+ avg: { columnTypes: ['number'] },
+ min: { columnTypes: ['number', 'date', 'dateTime'] },
+ max: { columnTypes: ['number', 'date', 'dateTime'] },
+ size: {},
+};
+
+export default function ServerSideDataGridAggregationTreeData() {
+ const apiRef = useGridApiRef();
+ const {
+ fetchRows,
+ columns,
+ initialState: initState,
+ } = useMockServer(dataSetOptions);
+
+ const initialState: GridInitialState = React.useMemo(
+ () => ({
+ ...initState,
+ aggregation: {
+ model: { rating: 'avg', website: 'size' },
+ },
+ }),
+ [initState],
+ );
+
+ const dataSource: GridDataSource = React.useMemo(
+ () => ({
+ getRows: async (params) => {
+ const urlParams = new URLSearchParams({
+ paginationModel: JSON.stringify(params.paginationModel),
+ filterModel: JSON.stringify(params.filterModel),
+ sortModel: JSON.stringify(params.sortModel),
+ groupKeys: JSON.stringify(params.groupKeys),
+ aggregationModel: JSON.stringify(params.aggregationModel),
+ });
+ const getRowsResponse = await fetchRows(
+ `https://mui.com/x/api/data-grid?${urlParams.toString()}`,
+ );
+ return {
+ rows: getRowsResponse.rows,
+ rowCount: getRowsResponse.rowCount,
+ aggregateRow: getRowsResponse.aggregateRow,
+ };
+ },
+ getGroupKey: (row) => row[dataSetOptions.treeData.groupingField],
+ getChildrenCount: (row) => row.descendantCount,
+ getAggregatedValue: (row, field) => row[`${field}Aggregate`],
+ }),
+ [fetchRows],
+ );
+
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationTreeData.tsx.preview b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationTreeData.tsx.preview
new file mode 100644
index 0000000000000..6eee2299964e8
--- /dev/null
+++ b/docs/data/data-grid/server-side-data/ServerSideDataGridAggregationTreeData.tsx.preview
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/docs/data/data-grid/server-side-data/aggregation.md b/docs/data/data-grid/server-side-data/aggregation.md
index e11485b7d80ce..7b94fbaf0e76d 100644
--- a/docs/data/data-grid/server-side-data/aggregation.md
+++ b/docs/data/data-grid/server-side-data/aggregation.md
@@ -2,14 +2,103 @@
title: React Data Grid - Server-side aggregation
---
-# Data Grid - Server-side aggregation [ ](/x/introduction/licensing/#premium-plan 'Premium plan')🚧
+# Data Grid - Server-side aggregation [ ](/x/introduction/licensing/#premium-plan 'Premium plan')🧪
Aggregation with server-side data source.
-:::warning
-This feature isn't implemented yet. It's coming.
+To dynamically load tree data from the server, you must create a data source and pass the `unstable_dataSource` prop to the Data Grid, as detailed in the [Server-side data overview](/x/react-data-grid/server-side-data/).
-👍 Upvote [issue #10860](https://github.com/mui/mui-x/issues/10860) if you want to see it land faster.
+:::info
+If you're looking for aggregation on the client side, see [Aggregation](/x/react-data-grid/aggregation/).
+:::
+
+Server-side aggregation requires some additional steps to implement:
+
+1. Pass the available aggregation functions of type `GridAggregationFunctionDataSource` to the Data Grid using the `aggregationFunctions` prop. Its default value is empty when the Data Grid is used with server-side data.
+
+ ```tsx
+ const aggregationFunctions: Record = {
+ size: { label: 'Size' },
+ sum: { label: 'Sum', columnTypes: ['number'] },
+ }
+
+
+ ```
+
+ The `GridAggregationFunctionDataSource` interface is similar to `GridAggregationFunction`, but it doesn't have `apply` or `getCellValue` properties because the computation is done on the server.
+
+ See the [GridAggregationFunctionDataSource API page](/x/api/data-grid/grid-aggregation-function-data-source/) for more details.
+
+2. Use `aggregationModel` passed in the `getRows` method of `GridDataSource` to fetch the aggregated values.
+ For the root level footer aggregation row, pass `aggregateRow` containing the aggregated values in the `GetRowsResponse`.
+
+ ```diff
+ const dataSource = {
+ getRows: async ({
+ sortModel,
+ filterModel,
+ paginationModel,
+ + aggregationModel,
+ }) => {
+ const rows = await fetchRows();
+ - const response = await fetchData({ sortModel, filterModel, paginationModel });
+ + const response = await fetchData({ sortModel, filterModel, paginationModel, aggregationModel });
+ return {
+ rows: response.rows,
+ rowCount: getRowsResponse.totalCount,
+ + aggregateRow: response.aggregateRow,
+ }
+ }
+ }
+ ```
+
+3. Pass the getter method `getAggregatedValue` in `GridDataSource` that defines how to get the aggregated value for a parent row (including the `aggregateRow`).
+
+ ```tsx
+ const dataSource = {
+ getRows: async ({
+ ...
+ }) => {
+ ...
+ },
+ getAggregatedValue: (row, field) => {
+ return row[`${field}Aggregate`];
+ },
+ }
+ ```
+
+The following example demonstrates basic server-side aggregation.
-Don't hesitate to leave a comment on the same issue to influence what gets built. Especially if you already have a use case for this component, or if you are facing a pain point with your current solution.
+{{"demo": "ServerSideDataGridAggregation.js", "bg": "inline"}}
+
+:::info
+The data source mock server (`useMockServer()`) mocks the built-in aggregation functions listed in the [built-in functions section](/x/react-data-grid/aggregation/#built-in-functions) of the client-side aggregation documentation.
+Provide the function names and minimal configuration to demonstrate the aggregation, as shown in the demo.
:::
+
+## Usage with lazy loading
+
+Server-side aggregation can be implemented along with [server-side lazy loading](/x/react-data-grid/server-side-data/lazy-loading/) as shown in the demo below.
+
+{{"demo": "ServerSideDataGridAggregationLazyLoading.js", "bg": "inline"}}
+
+## Usage with row grouping
+
+Server-side aggregation works with row grouping in a similar way as described in [Aggregation—usage with row grouping](/x/react-data-grid/aggregation/#usage-with-row-grouping).
+The aggregated values are acquired from the parent rows using the `getAggregatedValue` method.
+
+{{"demo": "ServerSideDataGridAggregationRowGrouping.js", "bg": "inline"}}
+
+## Usage with tree data
+
+Server-side aggregation can be used with tree data in a similar way as described in [Aggregation—usage with tree data](/x/react-data-grid/aggregation/#usage-with-tree-data).
+The aggregated values are acquired from the parent rows using the `getAggregatedValue` method.
+
+{{"demo": "ServerSideDataGridAggregationTreeData.js", "bg": "inline"}}
+
+## API
+
+- [DataGrid](/x/api/data-grid/data-grid/)
+- [DataGridPro](/x/api/data-grid/data-grid-pro/)
+- [DataGridPremium](/x/api/data-grid/data-grid-premium/)
+- [GridAggregationFunctionDataSource](/x/api/data-grid/grid-aggregation-function-data-source/)
diff --git a/docs/data/date-pickers/accessibility/accessibility.md b/docs/data/date-pickers/accessibility/accessibility.md
index 57dae01cb098b..98f23859ce1e2 100644
--- a/docs/data/date-pickers/accessibility/accessibility.md
+++ b/docs/data/date-pickers/accessibility/accessibility.md
@@ -17,7 +17,7 @@ Common conformance guidelines for accessibility include:
- US:
- [ADA](https://www.ada.gov/) - US Department of Justice
- [Section 508](https://www.section508.gov/) - US federal agencies
-- Europe: [EAA](https://ec.europa.eu/social/main.jsp?catId=1202) (European Accessibility Act)
+- Europe: [EAA](https://employment-social-affairs.ec.europa.eu/policies-and-activities/social-protection-social-inclusion/persons-disabilities/union-equality-strategy-rights-persons-disabilities-2021-2030/european-accessibility-act_en) (European Accessibility Act)
WCAG 2.1 has three levels of conformance: A, AA, and AAA.
Level AA exceeds the basic criteria for accessibility and is a common target for most organizations, so this is what we aim to support.
diff --git a/docs/data/date-pickers/adapters-locale/adapters-locale.md b/docs/data/date-pickers/adapters-locale/adapters-locale.md
index 524ffb3df60c5..1bd9784e16292 100644
--- a/docs/data/date-pickers/adapters-locale/adapters-locale.md
+++ b/docs/data/date-pickers/adapters-locale/adapters-locale.md
@@ -45,18 +45,18 @@ We support `date-fns` package v2.x, v3.x, and v4.x major versions.
A single adapter cannot work for all `date-fns` versions, because the way functions are exported has been changed in v3.x.
-To use `date-fns` v3.x or v4.x, you need to import the adapter from `@mui/x-date-pickers/AdapterDateFnsV3` instead of `@mui/x-date-pickers/AdapterDateFns`.
+To use `date-fns` v2.x, you need to import the adapter from `@mui/x-date-pickers/AdapterDateFnsV2` instead of `@mui/x-date-pickers/AdapterDateFns`.
:::
```tsx
-// with date-fns v2.x
-import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
// with date-fns v3.x or v4.x
-import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV3';
+import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
// with date-fns v2.x
-import de from 'date-fns/locale/de';
+import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV2';
// with date-fns v3.x or v4.x
import { de } from 'date-fns/locale/de';
+// with date-fns v2.x
+import de from 'date-fns/locale/de';
{children}
@@ -303,10 +303,10 @@ For `date-fns`, override the `options.weekStartsOn` of the used locale:
```ts
import { Locale } from 'date-fns';
+// with date-fns v3.x or v4.x
+import { enUS } from 'date-fns/locale/en-US';
// with date-fns v2.x
import enUS from 'date-fns/locale/en-US';
-// with date-fns v3.x
-import { enUS } from 'date-fns/locale/en-US';
const customEnLocale: Locale = {
...enUS,
diff --git a/docs/data/date-pickers/base-concepts/base-concepts.md b/docs/data/date-pickers/base-concepts/base-concepts.md
index ca61ae0cbe0f0..37ee4a4c2c584 100644
--- a/docs/data/date-pickers/base-concepts/base-concepts.md
+++ b/docs/data/date-pickers/base-concepts/base-concepts.md
@@ -113,10 +113,10 @@ Each _Picker_ is available in a responsive, desktop and mobile variant:
- The responsive component (for example `DatePicker`) which renders the desktop component or the mobile one depending on the device it runs on.
- The desktop component (for example `DesktopDatePicker`) which works best for mouse devices and large screens.
- It renders the views inside a popover and allows editing values directly inside the field.
+ It renders the views inside a popover and a field for keyboard editing.
- The mobile component (for example `MobileDatePicker`) which works best for touch devices and small screens.
- It renders the view inside a modal and does not allow editing values directly inside the field.
+ It renders the view inside a modal and a field for keyboard editing.
{{"demo": "ResponsivePickers.js"}}
diff --git a/docs/data/date-pickers/calendar-systems/AdapterHijri.js b/docs/data/date-pickers/calendar-systems/AdapterHijri.js
index 377759e113758..fb8974424621a 100644
--- a/docs/data/date-pickers/calendar-systems/AdapterHijri.js
+++ b/docs/data/date-pickers/calendar-systems/AdapterHijri.js
@@ -24,36 +24,28 @@ const cacheRtl = createCache({
function ButtonDateTimeField(props) {
const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');
- const { value, timezone, format } = internalProps;
- const {
- InputProps,
- slotProps,
- slots,
- ownerState,
- label,
- focused,
- name,
- ...other
- } = forwardedProps;
+ const { ownerState, label, focused, name, ...other } = forwardedProps;
const pickerContext = usePickerContext();
-
- const parsedFormat = useParsedFormat(internalProps);
+ const parsedFormat = useParsedFormat();
const { hasValidationError } = useValidation({
validator: validateDate,
- value,
- timezone,
+ value: pickerContext.value,
+ timezone: pickerContext.timezone,
props: internalProps,
});
- const valueStr = value == null ? parsedFormat : value.format(format);
+ const valueStr =
+ pickerContext.value == null
+ ? parsedFormat
+ : pickerContext.value.format(pickerContext.fieldFormat);
return (
pickerContext.setOpen((prev) => !prev)}
>
{label ? `${label}: ${valueStr}` : valueStr}
diff --git a/docs/data/date-pickers/calendar-systems/AdapterHijri.tsx b/docs/data/date-pickers/calendar-systems/AdapterHijri.tsx
index 704dbee32ae79..fc6da7e2e11bb 100644
--- a/docs/data/date-pickers/calendar-systems/AdapterHijri.tsx
+++ b/docs/data/date-pickers/calendar-systems/AdapterHijri.tsx
@@ -28,36 +28,28 @@ const cacheRtl = createCache({
function ButtonDateTimeField(props: DateTimePickerFieldProps) {
const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');
- const { value, timezone, format } = internalProps;
- const {
- InputProps,
- slotProps,
- slots,
- ownerState,
- label,
- focused,
- name,
- ...other
- } = forwardedProps;
+ const { ownerState, label, focused, name, ...other } = forwardedProps;
const pickerContext = usePickerContext();
-
- const parsedFormat = useParsedFormat(internalProps);
+ const parsedFormat = useParsedFormat();
const { hasValidationError } = useValidation({
validator: validateDate,
- value,
- timezone,
+ value: pickerContext.value,
+ timezone: pickerContext.timezone,
props: internalProps,
});
- const valueStr = value == null ? parsedFormat : value.format(format);
+ const valueStr =
+ pickerContext.value == null
+ ? parsedFormat
+ : pickerContext.value.format(pickerContext.fieldFormat);
return (
pickerContext.setOpen((prev) => !prev)}
>
{label ? `${label}: ${valueStr}` : valueStr}
diff --git a/docs/data/date-pickers/calendar-systems/calendar-systems.md b/docs/data/date-pickers/calendar-systems/calendar-systems.md
index 86de2cea4c4c0..7ae8b6d3f7f51 100644
--- a/docs/data/date-pickers/calendar-systems/calendar-systems.md
+++ b/docs/data/date-pickers/calendar-systems/calendar-systems.md
@@ -20,17 +20,17 @@ The following demo shows how to use the `date-fns-jalali` adapter:
{{"demo": "AdapterJalali.js"}}
:::info
-Both `date-fns-jalali` major versions (v2.x and v3.x) are supported.
+We support `date-fns-jalali` package v2.x, v3.x, and v4.x major versions.
-A single adapter cannot work for both `date-fns-jalali` v2.x and v3.x, because the way functions are exported has been changed in v3.x.
+A single adapter cannot work for all `date-fns-jalali` versions, because the way functions are exported has been changed in v3.x.
-To use `date-fns-jalali` v3.x, you will have to import the adapter from `@mui/x-date-pickers/AdapterDateFnsJalaliV3` instead of `@mui/x-date-pickers/AdapterDateFnsJalali`.
+To use `date-fns-jalali` v2.x, you need to import the adapter from `@mui/x-date-pickers/AdapterDateFnsJalaliV2` instead of `@mui/x-date-pickers/AdapterDateFnsJalali`.
```tsx
-// with date-fns-jalali v2.x
+// with date-fns-jalali v3.x or v4.x
import { AdapterDateFnsJalali } from '@mui/x-date-pickers/AdapterDateFnsJalali';
-// with date-fns-jalali v3.x
-import { AdapterDateFnsJalali } from '@mui/x-date-pickers/AdapterDateFnsJalaliV3';
+// with date-fns-jalali v2.x
+import { AdapterDateFnsJalali } from '@mui/x-date-pickers/AdapterDateFnsJalaliV2';
```
:::
diff --git a/docs/data/date-pickers/custom-field/BrowserV7Field.js b/docs/data/date-pickers/custom-field/BrowserV7Field.js
index 11f5b168ae3fc..f4a7c867b747a 100644
--- a/docs/data/date-pickers/custom-field/BrowserV7Field.js
+++ b/docs/data/date-pickers/custom-field/BrowserV7Field.js
@@ -1,11 +1,14 @@
import * as React from 'react';
import useForkRef from '@mui/utils/useForkRef';
import { styled } from '@mui/material/styles';
+import IconButton from '@mui/material/IconButton';
+import { CalendarIcon } from '@mui/x-date-pickers/icons';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { unstable_useDateField as useDateField } from '@mui/x-date-pickers/DateField';
import { Unstable_PickersSectionList as PickersSectionList } from '@mui/x-date-pickers/PickersSectionList';
+import { usePickerContext } from '@mui/x-date-pickers/hooks';
const BrowserFieldRoot = styled('div', { name: 'BrowserField', slot: 'Root' })({
display: 'flex',
@@ -41,6 +44,8 @@ const BrowserDateField = React.forwardRef((props, ref) => {
onInput,
onPaste,
onKeyDown,
+ // Should be passed to the button that opens the picker
+ openPickerAriaLabel,
// Can be passed to a hidden element
onChange,
value,
@@ -55,16 +60,15 @@ const BrowserDateField = React.forwardRef((props, ref) => {
readOnly,
focused,
error,
- InputProps: { ref: InputPropsRef, startAdornment, endAdornment } = {},
// The rest can be passed to the root element
...other
} = fieldResponse;
- const handleRef = useForkRef(InputPropsRef, ref);
+ const pickerContext = usePickerContext();
+ const handleRef = useForkRef(pickerContext.triggerRef, ref);
return (
- {startAdornment}
{
onKeyDown={onKeyDown}
/>
- {endAdornment}
+ pickerContext.setOpen((prev) => !prev)}
+ sx={{ marginLeft: 1.5 }}
+ aria-label={openPickerAriaLabel}
+ >
+
+
);
});
diff --git a/docs/data/date-pickers/custom-field/BrowserV7Field.tsx b/docs/data/date-pickers/custom-field/BrowserV7Field.tsx
index a218b603030fb..75545b14ce8cc 100644
--- a/docs/data/date-pickers/custom-field/BrowserV7Field.tsx
+++ b/docs/data/date-pickers/custom-field/BrowserV7Field.tsx
@@ -1,6 +1,8 @@
import * as React from 'react';
import useForkRef from '@mui/utils/useForkRef';
import { styled } from '@mui/material/styles';
+import IconButton from '@mui/material/IconButton';
+import { CalendarIcon } from '@mui/x-date-pickers/icons';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import {
@@ -10,6 +12,7 @@ import {
} from '@mui/x-date-pickers/DatePicker';
import { unstable_useDateField as useDateField } from '@mui/x-date-pickers/DateField';
import { Unstable_PickersSectionList as PickersSectionList } from '@mui/x-date-pickers/PickersSectionList';
+import { usePickerContext } from '@mui/x-date-pickers/hooks';
const BrowserFieldRoot = styled('div', { name: 'BrowserField', slot: 'Root' })({
display: 'flex',
@@ -48,6 +51,9 @@ const BrowserDateField = React.forwardRef(
onPaste,
onKeyDown,
+ // Should be passed to the button that opens the picker
+ openPickerAriaLabel,
+
// Can be passed to a hidden element
onChange,
value,
@@ -66,17 +72,15 @@ const BrowserDateField = React.forwardRef(
focused,
error,
- InputProps: { ref: InputPropsRef, startAdornment, endAdornment } = {},
-
// The rest can be passed to the root element
...other
} = fieldResponse;
- const handleRef = useForkRef(InputPropsRef, ref);
+ const pickerContext = usePickerContext();
+ const handleRef = useForkRef(pickerContext.triggerRef, ref);
return (
- {startAdornment}
- {endAdornment}
+ pickerContext.setOpen((prev) => !prev)}
+ sx={{ marginLeft: 1.5 }}
+ aria-label={openPickerAriaLabel}
+ >
+
+
);
},
diff --git a/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.js b/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.js
index 5f7d2d494688d..7743be94934f5 100644
--- a/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.js
+++ b/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.js
@@ -80,18 +80,12 @@ const BrowserTextField = React.forwardRef((props, ref) => {
const BrowserMultiInputDateRangeField = React.forwardRef((props, ref) => {
const {
slotProps,
- value,
- format,
- onChange,
readOnly,
- disabled,
shouldDisableDate,
minDate,
maxDate,
disableFuture,
disablePast,
- selectedSections,
- onSelectedSectionsChange,
className,
unstableStartFieldRef,
unstableEndFieldRef,
@@ -100,29 +94,23 @@ const BrowserMultiInputDateRangeField = React.forwardRef((props, ref) => {
const startTextFieldProps = useSlotProps({
elementType: 'input',
externalSlotProps: slotProps?.textField,
- ownerState: { ...props, position: 'start' },
+ ownerState: { position: 'start' },
});
const endTextFieldProps = useSlotProps({
elementType: 'input',
externalSlotProps: slotProps?.textField,
- ownerState: { ...props, position: 'end' },
+ ownerState: { position: 'end' },
});
const fieldResponse = useMultiInputDateRangeField({
sharedProps: {
- value,
- format,
- onChange,
readOnly,
- disabled,
shouldDisableDate,
minDate,
maxDate,
disableFuture,
disablePast,
- selectedSections,
- onSelectedSectionsChange,
enableAccessibleFieldDOMStructure: true,
},
startTextFieldProps,
@@ -131,6 +119,21 @@ const BrowserMultiInputDateRangeField = React.forwardRef((props, ref) => {
unstableEndFieldRef,
});
+ const {
+ // The multi input range field do not support clearable and onClear
+ onClear: onClearStartDate,
+ clearable: isStartDateClearable,
+ openPickerAriaLabel: openPickerStartDateAriaLabel,
+ ...startDateProps
+ } = fieldResponse.startDate;
+ const {
+ // The multi input range field do not support clearable and onClear
+ onClear: onClearEndDate,
+ clearable: isEndDateClearable,
+ openPickerAriaLabel: openPickerEndDateAriaLabel,
+ ...endDateProps
+ } = fieldResponse.endDate;
+
return (
{
overflow="auto"
className={className}
>
-
- —
-
+
+ –
+
);
});
diff --git a/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.tsx b/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.tsx
index 4c60152c21d0f..b5c605dc0ac8f 100644
--- a/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.tsx
+++ b/docs/data/date-pickers/custom-field/BrowserV7MultiInputRangeField.tsx
@@ -106,7 +106,11 @@ interface BrowserMultiInputDateRangeFieldProps
DateRangePickerFieldProps,
'unstableFieldRef' | 'clearable' | 'onClear'
>,
- MultiInputFieldRefs {}
+ MultiInputFieldRefs {
+ slotProps: {
+ textField: any;
+ };
+}
type BrowserMultiInputDateRangeFieldComponent = ((
props: BrowserMultiInputDateRangeFieldProps & React.RefAttributes,
@@ -116,18 +120,12 @@ const BrowserMultiInputDateRangeField = React.forwardRef(
(props: BrowserMultiInputDateRangeFieldProps, ref: React.Ref) => {
const {
slotProps,
- value,
- format,
- onChange,
readOnly,
- disabled,
shouldDisableDate,
minDate,
maxDate,
disableFuture,
disablePast,
- selectedSections,
- onSelectedSectionsChange,
className,
unstableStartFieldRef,
unstableEndFieldRef,
@@ -136,13 +134,13 @@ const BrowserMultiInputDateRangeField = React.forwardRef(
const startTextFieldProps = useSlotProps({
elementType: 'input',
externalSlotProps: slotProps?.textField,
- ownerState: { ...props, position: 'start' },
+ ownerState: { position: 'start' } as any,
}) as MultiInputFieldSlotTextFieldProps;
const endTextFieldProps = useSlotProps({
elementType: 'input',
externalSlotProps: slotProps?.textField,
- ownerState: { ...props, position: 'end' },
+ ownerState: { position: 'end' } as any,
}) as MultiInputFieldSlotTextFieldProps;
const fieldResponse = useMultiInputDateRangeField<
@@ -150,18 +148,12 @@ const BrowserMultiInputDateRangeField = React.forwardRef(
MultiInputFieldSlotTextFieldProps
>({
sharedProps: {
- value,
- format,
- onChange,
readOnly,
- disabled,
shouldDisableDate,
minDate,
maxDate,
disableFuture,
disablePast,
- selectedSections,
- onSelectedSectionsChange,
enableAccessibleFieldDOMStructure: true,
},
startTextFieldProps,
@@ -170,6 +162,21 @@ const BrowserMultiInputDateRangeField = React.forwardRef(
unstableEndFieldRef,
});
+ const {
+ // The multi input range field do not support clearable and onClear
+ onClear: onClearStartDate,
+ clearable: isStartDateClearable,
+ openPickerAriaLabel: openPickerStartDateAriaLabel,
+ ...startDateProps
+ } = fieldResponse.startDate;
+ const {
+ // The multi input range field do not support clearable and onClear
+ onClear: onClearEndDate,
+ clearable: isEndDateClearable,
+ openPickerAriaLabel: openPickerEndDateAriaLabel,
+ ...endDateProps
+ } = fieldResponse.endDate;
+
return (
-
- —
-
+
+ –
+
);
},
diff --git a/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.js b/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.js
index ae55770f60785..5c2fdcc30f448 100644
--- a/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.js
+++ b/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.js
@@ -59,13 +59,12 @@ const BrowserSingleInputDateRangeField = React.forwardRef((props, ref) => {
readOnly,
focused,
error,
- InputProps: { ref: InputPropsRef, startAdornment, endAdornment } = {},
// The rest can be passed to the root element
...other
} = fieldResponse;
const pickerContext = usePickerContext();
- const handleRef = useForkRef(InputPropsRef, ref);
+ const handleRef = useForkRef(pickerContext.triggerRef, ref);
return (
{
minWidth: 300,
}}
>
- {startAdornment}
{
onKeyDown={onKeyDown}
/>
- {endAdornment}
pickerContext.setOpen((prev) => !prev)}>
diff --git a/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.tsx b/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.tsx
index 68f5f4c21737b..bcb591ba663af 100644
--- a/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.tsx
+++ b/docs/data/date-pickers/custom-field/BrowserV7SingleInputRangeField.tsx
@@ -77,14 +77,12 @@ const BrowserSingleInputDateRangeField = React.forwardRef(
focused,
error,
- InputProps: { ref: InputPropsRef, startAdornment, endAdornment } = {},
-
// The rest can be passed to the root element
...other
} = fieldResponse;
const pickerContext = usePickerContext();
- const handleRef = useForkRef(InputPropsRef, ref);
+ const handleRef = useForkRef(pickerContext.triggerRef, ref);
return (
- {startAdornment}
- {endAdornment}
pickerContext.setOpen((prev) => !prev)}>
diff --git a/docs/data/date-pickers/custom-field/JoyV6Field.js b/docs/data/date-pickers/custom-field/JoyV6Field.js
index 156d8200c2f5f..6efa50f33ba40 100644
--- a/docs/data/date-pickers/custom-field/JoyV6Field.js
+++ b/docs/data/date-pickers/custom-field/JoyV6Field.js
@@ -11,45 +11,63 @@ import {
THEME_ID,
} from '@mui/joy/styles';
import Input from '@mui/joy/Input';
+import IconButton from '@mui/joy/IconButton';
import FormControl from '@mui/joy/FormControl';
import FormLabel from '@mui/joy/FormLabel';
+import { createSvgIcon } from '@mui/joy/utils';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { unstable_useDateField as useDateField } from '@mui/x-date-pickers/DateField';
+import { usePickerContext } from '@mui/x-date-pickers/hooks';
+
+const CalendarIcon = createSvgIcon(
+ ,
+ 'Calendar',
+);
const joyTheme = extendJoyTheme();
const JoyDateField = React.forwardRef((props, ref) => {
- const fieldResponse = useDateField({
- ...props,
- enableAccessibleFieldDOMStructure: false,
- });
+ const fieldResponse = useDateField(props);
const {
// Should be ignored
enableAccessibleFieldDOMStructure,
+ // Should be passed to the button that opens the picker
+ openPickerAriaLabel,
// Can be passed to the button that clears the value
onClear,
clearable,
- disabled,
- id,
+ // Can be used to render a custom label
label,
- InputProps: { ref: containerRef, startAdornment, endAdornment } = {},
+ // Can be used to style the component
+ disabled,
+ readOnly,
+ focused,
+ error,
inputRef,
- slots,
- slotProps,
+ // The rest can be passed to the root element
+ id,
...other
} = fieldResponse;
+ const pickerContext = usePickerContext();
+
return (
{label}
pickerContext.setOpen((prev) => !prev)}
+ aria-label={openPickerAriaLabel}
+ >
+
+
+ }
slotProps={{
input: { ref: inputRef },
}}
@@ -64,6 +82,7 @@ const JoyDatePicker = React.forwardRef((props, ref) => {
);
diff --git a/docs/data/date-pickers/custom-field/JoyV6Field.tsx b/docs/data/date-pickers/custom-field/JoyV6Field.tsx
index 9e30ca9e0b717..97d8c85b4be5d 100644
--- a/docs/data/date-pickers/custom-field/JoyV6Field.tsx
+++ b/docs/data/date-pickers/custom-field/JoyV6Field.tsx
@@ -11,8 +11,10 @@ import {
THEME_ID,
} from '@mui/joy/styles';
import Input from '@mui/joy/Input';
+import IconButton from '@mui/joy/IconButton';
import FormControl from '@mui/joy/FormControl';
import FormLabel from '@mui/joy/FormLabel';
+import { createSvgIcon } from '@mui/joy/utils';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import {
@@ -21,42 +23,61 @@ import {
DatePickerProps,
} from '@mui/x-date-pickers/DatePicker';
import { unstable_useDateField as useDateField } from '@mui/x-date-pickers/DateField';
+import { usePickerContext } from '@mui/x-date-pickers/hooks';
+
+const CalendarIcon = createSvgIcon(
+ ,
+ 'Calendar',
+);
const joyTheme = extendJoyTheme();
const JoyDateField = React.forwardRef(
(props: DatePickerFieldProps, ref: React.Ref) => {
- const fieldResponse = useDateField({
- ...props,
- enableAccessibleFieldDOMStructure: false,
- });
+ const fieldResponse = useDateField(props);
const {
// Should be ignored
enableAccessibleFieldDOMStructure,
+ // Should be passed to the button that opens the picker
+ openPickerAriaLabel,
+
// Can be passed to the button that clears the value
onClear,
clearable,
- disabled,
- id,
+ // Can be used to render a custom label
label,
- InputProps: { ref: containerRef, startAdornment, endAdornment } = {},
+
+ // Can be used to style the component
+ disabled,
+ readOnly,
+ focused,
+ error,
inputRef,
- slots,
- slotProps,
+
+ // The rest can be passed to the root element
+ id,
...other
} = fieldResponse;
+ const pickerContext = usePickerContext();
+
return (
{label}
pickerContext.setOpen((prev) => !prev)}
+ aria-label={openPickerAriaLabel}
+ >
+
+
+ }
slotProps={{
input: { ref: inputRef },
}}
@@ -73,6 +94,7 @@ const JoyDatePicker = React.forwardRef(
);
diff --git a/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.js b/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.js
index 61a8a5d231f7d..6adab583bfa13 100644
--- a/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.js
+++ b/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.js
@@ -91,7 +91,7 @@ const MultiInputJoyDateRangeFieldSeparator = styled(
{/* Ensure that the separator is correctly aligned */}
- {props.children ?? ' — '}
+ {props.children ?? ' – '}
),
{
@@ -104,18 +104,12 @@ const MultiInputJoyDateRangeFieldSeparator = styled(
const JoyMultiInputDateRangeField = React.forwardRef((props, ref) => {
const {
slotProps,
- value,
- format,
- onChange,
readOnly,
- disabled,
shouldDisableDate,
minDate,
maxDate,
disableFuture,
disablePast,
- selectedSections,
- onSelectedSectionsChange,
className,
unstableStartFieldRef,
unstableEndFieldRef,
@@ -124,29 +118,23 @@ const JoyMultiInputDateRangeField = React.forwardRef((props, ref) => {
const startTextFieldProps = useSlotProps({
elementType: FormControl,
externalSlotProps: slotProps?.textField,
- ownerState: { ...props, position: 'start' },
+ ownerState: { position: 'start' },
});
const endTextFieldProps = useSlotProps({
elementType: FormControl,
externalSlotProps: slotProps?.textField,
- ownerState: { ...props, position: 'end' },
+ ownerState: { position: 'end' },
});
const fieldResponse = useMultiInputDateRangeField({
sharedProps: {
- value,
- format,
- onChange,
readOnly,
- disabled,
shouldDisableDate,
minDate,
maxDate,
disableFuture,
disablePast,
- selectedSections,
- onSelectedSectionsChange,
enableAccessibleFieldDOMStructure: false,
},
startTextFieldProps,
@@ -155,11 +143,26 @@ const JoyMultiInputDateRangeField = React.forwardRef((props, ref) => {
unstableEndFieldRef,
});
+ const {
+ // The multi input range field do not support clearable, onClear and openPickerAriaLabel
+ onClear: onClearStartDate,
+ clearable: isStartDateClearable,
+ openPickerAriaLabel: openPickerStartDateAriaLabel,
+ ...startDateProps
+ } = fieldResponse.startDate;
+ const {
+ // The multi input range field do not support clearable, onClear and openPickerAriaLabel
+ onClear: onClearEndDate,
+ clearable: isEndDateClearable,
+ openPickerAriaLabel: openPickerEndDateAriaLabel,
+ ...endDateProps
+ } = fieldResponse.endDate;
+
return (
-
+
-
+
);
});
@@ -169,6 +172,7 @@ const JoyDateRangePicker = React.forwardRef((props, ref) => {
);
diff --git a/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.tsx b/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.tsx
index f4c28e574437b..9f5cbc60eff72 100644
--- a/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.tsx
+++ b/docs/data/date-pickers/custom-field/JoyV6MultiInputRangeField.tsx
@@ -117,7 +117,7 @@ const MultiInputJoyDateRangeFieldSeparator = styled(
{/* Ensure that the separator is correctly aligned */}
- {props.children ?? ' — '}
+ {props.children ?? ' – '}
),
{
@@ -132,7 +132,11 @@ interface JoyMultiInputDateRangeFieldProps
DateRangePickerFieldProps,
'unstableFieldRef' | 'clearable' | 'onClear'
>,
- MultiInputFieldRefs {}
+ MultiInputFieldRefs {
+ slotProps: {
+ textField: any;
+ };
+}
type JoyMultiInputDateRangeFieldComponent = ((
props: JoyMultiInputDateRangeFieldProps & React.RefAttributes,
@@ -142,18 +146,12 @@ const JoyMultiInputDateRangeField = React.forwardRef(
(props: JoyMultiInputDateRangeFieldProps, ref: React.Ref) => {
const {
slotProps,
- value,
- format,
- onChange,
readOnly,
- disabled,
shouldDisableDate,
minDate,
maxDate,
disableFuture,
disablePast,
- selectedSections,
- onSelectedSectionsChange,
className,
unstableStartFieldRef,
unstableEndFieldRef,
@@ -162,13 +160,13 @@ const JoyMultiInputDateRangeField = React.forwardRef(
const startTextFieldProps = useSlotProps({
elementType: FormControl,
externalSlotProps: slotProps?.textField,
- ownerState: { ...props, position: 'start' },
+ ownerState: { position: 'start' } as any,
}) as MultiInputFieldSlotTextFieldProps;
const endTextFieldProps = useSlotProps({
elementType: FormControl,
externalSlotProps: slotProps?.textField,
- ownerState: { ...props, position: 'end' },
+ ownerState: { position: 'end' } as any,
}) as MultiInputFieldSlotTextFieldProps;
const fieldResponse = useMultiInputDateRangeField<
@@ -176,18 +174,12 @@ const JoyMultiInputDateRangeField = React.forwardRef(
MultiInputFieldSlotTextFieldProps
>({
sharedProps: {
- value,
- format,
- onChange,
readOnly,
- disabled,
shouldDisableDate,
minDate,
maxDate,
disableFuture,
disablePast,
- selectedSections,
- onSelectedSectionsChange,
enableAccessibleFieldDOMStructure: false,
},
startTextFieldProps,
@@ -196,11 +188,26 @@ const JoyMultiInputDateRangeField = React.forwardRef(
unstableEndFieldRef,
});
+ const {
+ // The multi input range field do not support clearable, onClear and openPickerAriaLabel
+ onClear: onClearStartDate,
+ clearable: isStartDateClearable,
+ openPickerAriaLabel: openPickerStartDateAriaLabel,
+ ...startDateProps
+ } = fieldResponse.startDate;
+ const {
+ // The multi input range field do not support clearable, onClear and openPickerAriaLabel
+ onClear: onClearEndDate,
+ clearable: isEndDateClearable,
+ openPickerAriaLabel: openPickerEndDateAriaLabel,
+ ...endDateProps
+ } = fieldResponse.endDate;
+
return (
-
+
-
+
);
},
@@ -212,6 +219,7 @@ const JoyDateRangePicker = React.forwardRef(
);
diff --git a/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.js b/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.js
index 69123038f8508..eadea36d43b24 100644
--- a/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.js
+++ b/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.js
@@ -19,7 +19,9 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DateRangePicker } from '@mui/x-date-pickers-pro/DateRangePicker';
import { unstable_useSingleInputDateRangeField as useSingleInputDateRangeField } from '@mui/x-date-pickers-pro/SingleInputDateRangeField';
-export const DateRangeIcon = createSvgIcon(
+import { usePickerContext } from '@mui/x-date-pickers/hooks';
+
+const DateRangeIcon = createSvgIcon(
,
'DateRange',
);
@@ -27,10 +29,7 @@ export const DateRangeIcon = createSvgIcon(
const joyTheme = extendJoyTheme();
const JoySingleInputDateRangeField = React.forwardRef((props, ref) => {
- const fieldResponse = useSingleInputDateRangeField({
- ...props,
- enableAccessibleFieldDOMStructure: false,
- });
+ const fieldResponse = useSingleInputDateRangeField(props);
const {
// Should be ignored
@@ -41,13 +40,12 @@ const JoySingleInputDateRangeField = React.forwardRef((props, ref) => {
disabled,
id,
label,
- InputProps: { ref: containerRef } = {},
inputRef,
- slots,
- slotProps,
...other
} = fieldResponse;
+ const pickerContext = usePickerContext();
+
return (
{
>
{label}
}
slotProps={{
@@ -78,6 +76,7 @@ const JoySingleInputDateRangePicker = React.forwardRef((props, ref) => {
);
diff --git a/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.tsx b/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.tsx
index 822610feac08b..31da6b6756efe 100644
--- a/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.tsx
+++ b/docs/data/date-pickers/custom-field/JoyV6SingleInputRangeField.tsx
@@ -23,8 +23,9 @@ import {
} from '@mui/x-date-pickers-pro/DateRangePicker';
import { unstable_useSingleInputDateRangeField as useSingleInputDateRangeField } from '@mui/x-date-pickers-pro/SingleInputDateRangeField';
import { FieldType } from '@mui/x-date-pickers-pro/models';
+import { usePickerContext } from '@mui/x-date-pickers/hooks';
-export const DateRangeIcon = createSvgIcon(
+const DateRangeIcon = createSvgIcon(
,
'DateRange',
);
@@ -37,10 +38,7 @@ type JoySingleInputDateRangeFieldComponent = ((
const JoySingleInputDateRangeField = React.forwardRef(
(props: DateRangePickerFieldProps, ref: React.Ref) => {
- const fieldResponse = useSingleInputDateRangeField({
- ...props,
- enableAccessibleFieldDOMStructure: false,
- });
+ const fieldResponse = useSingleInputDateRangeField(props);
const {
// Should be ignored
@@ -53,13 +51,12 @@ const JoySingleInputDateRangeField = React.forwardRef(
disabled,
id,
label,
- InputProps: { ref: containerRef } = {},
inputRef,
- slots,
- slotProps,
...other
} = fieldResponse;
+ const pickerContext = usePickerContext();
+
return (
{label}
}
slotProps={{
@@ -92,6 +89,7 @@ const JoySingleInputDateRangePicker = React.forwardRef(
);
diff --git a/docs/data/date-pickers/custom-field/behavior-autocomplete/MaterialDatePicker.js b/docs/data/date-pickers/custom-field/behavior-autocomplete/MaterialDatePicker.js
index 2856b3af1a2d5..324a11699329c 100644
--- a/docs/data/date-pickers/custom-field/behavior-autocomplete/MaterialDatePicker.js
+++ b/docs/data/date-pickers/custom-field/behavior-autocomplete/MaterialDatePicker.js
@@ -1,30 +1,29 @@
import * as React from 'react';
import dayjs from 'dayjs';
import Autocomplete from '@mui/material/Autocomplete';
+import IconButton from '@mui/material/IconButton';
+import { CalendarIcon } from '@mui/x-date-pickers/icons';
import TextField from '@mui/material/TextField';
-import Stack from '@mui/material/Stack';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
-import { useSplitFieldProps } from '@mui/x-date-pickers/hooks';
+import { usePickerContext, useSplitFieldProps } from '@mui/x-date-pickers/hooks';
import { useValidation, validateDate } from '@mui/x-date-pickers/validation';
function AutocompleteField(props) {
- const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');
- const { value, timezone, onChange } = internalProps;
+ const { forwardedProps, internalProps } = useSplitFieldProps(props, 'date');
+ const { timezone, value, setValue } = usePickerContext();
const {
- InputProps,
- slotProps,
- slots,
ownerState,
label,
focused,
name,
options = [],
- inputProps,
...other
} = forwardedProps;
+ const pickerContext = usePickerContext();
+
const { hasValidationError, getValidationErrorForNewValue } = useValidation({
validator: validateDate,
value,
@@ -32,50 +31,38 @@ function AutocompleteField(props) {
props: internalProps,
});
- const mergeAdornments = (...adornments) => {
- const nonNullAdornments = adornments.filter((el) => el != null);
- if (nonNullAdornments.length === 0) {
- return null;
- }
-
- if (nonNullAdornments.length === 1) {
- return nonNullAdornments[0];
- }
-
- return (
-
- {nonNullAdornments.map((adornment, index) => (
- {adornment}
- ))}
-
- );
- };
-
return (
(
-
- )}
+ renderInput={(params) => {
+ const endAdornment = params.InputProps.endAdornment;
+ return (
+
+ pickerContext.setOpen((prev) => !prev)}
+ size="small"
+ >
+
+
+ {endAdornment.props.children}
+
+ ),
+ }),
+ }}
+ />
+ );
+ }}
getOptionLabel={(option) => {
if (!dayjs.isDayjs(option)) {
return '';
@@ -85,7 +72,7 @@ function AutocompleteField(props) {
}}
value={value}
onChange={(_, newValue) => {
- onChange(newValue, {
+ setValue(newValue, {
validationError: getValidationErrorForNewValue(newValue),
});
}}
diff --git a/docs/data/date-pickers/custom-field/behavior-autocomplete/MaterialDatePicker.tsx b/docs/data/date-pickers/custom-field/behavior-autocomplete/MaterialDatePicker.tsx
index 7633f42d7199e..e4884ca5b144d 100644
--- a/docs/data/date-pickers/custom-field/behavior-autocomplete/MaterialDatePicker.tsx
+++ b/docs/data/date-pickers/custom-field/behavior-autocomplete/MaterialDatePicker.tsx
@@ -1,8 +1,9 @@
import * as React from 'react';
import dayjs, { Dayjs } from 'dayjs';
import Autocomplete from '@mui/material/Autocomplete';
+import IconButton from '@mui/material/IconButton';
+import { CalendarIcon } from '@mui/x-date-pickers/icons';
import TextField from '@mui/material/TextField';
-import Stack from '@mui/material/Stack';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import {
@@ -10,7 +11,7 @@ import {
DatePickerFieldProps,
DatePickerProps,
} from '@mui/x-date-pickers/DatePicker';
-import { useSplitFieldProps } from '@mui/x-date-pickers/hooks';
+import { usePickerContext, useSplitFieldProps } from '@mui/x-date-pickers/hooks';
import { useValidation, validateDate } from '@mui/x-date-pickers/validation';
interface AutocompleteFieldProps extends DatePickerFieldProps {
@@ -21,21 +22,19 @@ interface AutocompleteFieldProps extends DatePickerFieldProps {
}
function AutocompleteField(props: AutocompleteFieldProps) {
- const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');
- const { value, timezone, onChange } = internalProps;
+ const { forwardedProps, internalProps } = useSplitFieldProps(props, 'date');
+ const { timezone, value, setValue } = usePickerContext();
const {
- InputProps,
- slotProps,
- slots,
ownerState,
label,
focused,
name,
options = [],
- inputProps,
...other
} = forwardedProps;
+ const pickerContext = usePickerContext();
+
const { hasValidationError, getValidationErrorForNewValue } = useValidation({
validator: validateDate,
value,
@@ -43,50 +42,39 @@ function AutocompleteField(props: AutocompleteFieldProps) {
props: internalProps,
});
- const mergeAdornments = (...adornments: React.ReactNode[]) => {
- const nonNullAdornments = adornments.filter((el) => el != null);
- if (nonNullAdornments.length === 0) {
- return null;
- }
-
- if (nonNullAdornments.length === 1) {
- return nonNullAdornments[0];
- }
-
- return (
-
- {nonNullAdornments.map((adornment, index) => (
- {adornment}
- ))}
-
- );
- };
-
return (
(
-
- )}
+ renderInput={(params) => {
+ const endAdornment = params.InputProps
+ .endAdornment as React.ReactElement;
+ return (
+
+ pickerContext.setOpen((prev) => !prev)}
+ size="small"
+ >
+
+
+ {endAdornment.props.children}
+
+ ),
+ }),
+ }}
+ />
+ );
+ }}
getOptionLabel={(option) => {
if (!dayjs.isDayjs(option)) {
return '';
@@ -96,7 +84,7 @@ function AutocompleteField(props: AutocompleteFieldProps) {
}}
value={value}
onChange={(_, newValue) => {
- onChange(newValue, {
+ setValue(newValue, {
validationError: getValidationErrorForNewValue(newValue),
});
}}
diff --git a/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.js b/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.js
index 0e30e8d2605aa..d6690a9fee09a 100644
--- a/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.js
+++ b/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.js
@@ -12,36 +12,28 @@ import {
function ButtonDateField(props) {
const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');
- const { value, timezone, format } = internalProps;
- const {
- InputProps,
- slotProps,
- slots,
- ownerState,
- label,
- focused,
- name,
- ...other
- } = forwardedProps;
+ const { ownerState, label, focused, name, ...other } = forwardedProps;
const pickerContext = usePickerContext();
-
- const parsedFormat = useParsedFormat(internalProps);
+ const parsedFormat = useParsedFormat();
const { hasValidationError } = useValidation({
validator: validateDate,
- value,
- timezone,
+ value: pickerContext.value,
+ timezone: pickerContext.timezone,
props: internalProps,
});
- const valueStr = value == null ? parsedFormat : value.format(format);
+ const valueStr =
+ pickerContext.value == null
+ ? parsedFormat
+ : pickerContext.value.format(pickerContext.fieldFormat);
return (
pickerContext.setOpen((prev) => !prev)}
>
{label ? `${label}: ${valueStr}` : valueStr}
diff --git a/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.tsx b/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.tsx
index 1da1b675decd7..56af8ea31b4dc 100644
--- a/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.tsx
+++ b/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.tsx
@@ -16,36 +16,28 @@ import {
function ButtonDateField(props: DatePickerFieldProps) {
const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');
- const { value, timezone, format } = internalProps;
- const {
- InputProps,
- slotProps,
- slots,
- ownerState,
- label,
- focused,
- name,
- ...other
- } = forwardedProps;
+ const { ownerState, label, focused, name, ...other } = forwardedProps;
const pickerContext = usePickerContext();
-
- const parsedFormat = useParsedFormat(internalProps);
+ const parsedFormat = useParsedFormat();
const { hasValidationError } = useValidation({
validator: validateDate,
- value,
- timezone,
+ value: pickerContext.value,
+ timezone: pickerContext.timezone,
props: internalProps,
});
- const valueStr = value == null ? parsedFormat : value.format(format);
+ const valueStr =
+ pickerContext.value == null
+ ? parsedFormat
+ : pickerContext.value.format(pickerContext.fieldFormat);
return (
pickerContext.setOpen((prev) => !prev)}
>
{label ? `${label}: ${valueStr}` : valueStr}
diff --git a/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.js b/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.js
index 0884deadb8b48..84c9ce3f03eb0 100644
--- a/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.js
+++ b/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.js
@@ -14,30 +14,21 @@ import {
function ButtonDateRangeField(props) {
const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');
- const { value, timezone, format } = internalProps;
- const {
- InputProps,
- slotProps,
- slots,
- ownerState,
- label,
- focused,
- name,
- ...other
- } = forwardedProps;
+ const { ownerState, label, focused, name, ...other } = forwardedProps;
const pickerContext = usePickerContext();
-
- const parsedFormat = useParsedFormat(internalProps);
+ const parsedFormat = useParsedFormat();
const { hasValidationError } = useValidation({
validator: validateDateRange,
- value,
- timezone,
+ value: pickerContext.value,
+ timezone: pickerContext.timezone,
props: internalProps,
});
- const formattedValue = (value ?? [null, null])
- .map((date) => (date == null ? parsedFormat : date.format(format)))
+ const formattedValue = pickerContext.value
+ .map((date) =>
+ date == null ? parsedFormat : date.format(pickerContext.fieldFormat),
+ )
.join(' – ');
return (
@@ -45,7 +36,7 @@ function ButtonDateRangeField(props) {
{...other}
variant="outlined"
color={hasValidationError ? 'error' : 'primary'}
- ref={InputProps?.ref}
+ ref={pickerContext.triggerRef}
onClick={() => pickerContext.setOpen((prev) => !prev)}
>
{label ? `${label}: ${formattedValue}` : formattedValue}
diff --git a/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.tsx b/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.tsx
index 9361a51c256a1..135ddbd46ec80 100644
--- a/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.tsx
+++ b/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.tsx
@@ -18,30 +18,21 @@ import {
function ButtonDateRangeField(props: DateRangePickerFieldProps) {
const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');
- const { value, timezone, format } = internalProps;
- const {
- InputProps,
- slotProps,
- slots,
- ownerState,
- label,
- focused,
- name,
- ...other
- } = forwardedProps;
+ const { ownerState, label, focused, name, ...other } = forwardedProps;
const pickerContext = usePickerContext();
-
- const parsedFormat = useParsedFormat(internalProps);
+ const parsedFormat = useParsedFormat();
const { hasValidationError } = useValidation({
validator: validateDateRange,
- value,
- timezone,
+ value: pickerContext.value,
+ timezone: pickerContext.timezone,
props: internalProps,
});
- const formattedValue = (value ?? [null, null])
- .map((date: Dayjs) => (date == null ? parsedFormat : date.format(format)))
+ const formattedValue = pickerContext.value
+ .map((date: Dayjs) =>
+ date == null ? parsedFormat : date.format(pickerContext.fieldFormat),
+ )
.join(' – ');
return (
@@ -49,7 +40,7 @@ function ButtonDateRangeField(props: DateRangePickerFieldProps) {
{...other}
variant="outlined"
color={hasValidationError ? 'error' : 'primary'}
- ref={InputProps?.ref}
+ ref={pickerContext.triggerRef}
onClick={() => pickerContext.setOpen((prev) => !prev)}
>
{label ? `${label}: ${formattedValue}` : formattedValue}
diff --git a/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.js b/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.js
index 107fb273f4964..4ee50736a08b2 100644
--- a/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.js
+++ b/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.js
@@ -2,11 +2,18 @@ import * as React from 'react';
import dayjs from 'dayjs';
import { useRifm } from 'rifm';
import TextField from '@mui/material/TextField';
+import InputAdornment from '@mui/material/InputAdornment';
+import IconButton from '@mui/material/IconButton';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
-import { useSplitFieldProps, useParsedFormat } from '@mui/x-date-pickers/hooks';
+import {
+ useSplitFieldProps,
+ useParsedFormat,
+ usePickerContext,
+} from '@mui/x-date-pickers/hooks';
import { useValidation, validateDate } from '@mui/x-date-pickers/validation';
+import { CalendarIcon } from '@mui/x-date-pickers/icons';
const MASK_USER_INPUT_SYMBOL = '_';
const ACCEPT_REGEX = /[\d]/gi;
@@ -23,29 +30,28 @@ function getInputValueFromValue(value, format) {
}
function MaskedDateField(props) {
- const { slots, slotProps, ...other } = props;
-
- const { forwardedProps, internalProps } = useSplitFieldProps(other, 'date');
-
- const { format, value, onChange, timezone } = internalProps;
+ const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');
+ const pickerContext = usePickerContext();
+ const parsedFormat = useParsedFormat();
// Control the input text
const [inputValue, setInputValue] = React.useState(() =>
- getInputValueFromValue(value, format),
+ getInputValueFromValue(pickerContext.value, pickerContext.fieldFormat),
);
React.useEffect(() => {
- if (value && value.isValid()) {
- const newDisplayDate = getInputValueFromValue(value, format);
+ if (pickerContext.value && pickerContext.value.isValid()) {
+ const newDisplayDate = getInputValueFromValue(
+ pickerContext.value,
+ pickerContext.fieldFormat,
+ );
setInputValue(newDisplayDate);
}
- }, [format, value]);
-
- const parsedFormat = useParsedFormat(internalProps);
+ }, [pickerContext.fieldFormat, pickerContext.value]);
const { hasValidationError, getValidationErrorForNewValue } = useValidation({
- value,
- timezone,
+ value: pickerContext.value,
+ timezone: pickerContext.timezone,
props: internalProps,
validator: validateDate,
});
@@ -53,20 +59,22 @@ function MaskedDateField(props) {
const handleInputValueChange = (newInputValue) => {
setInputValue(newInputValue);
- const newValue = dayjs(newInputValue, format);
- onChange(newValue, {
+ const newValue = dayjs(newInputValue, pickerContext.fieldFormat);
+ pickerContext.setValue(newValue, {
validationError: getValidationErrorForNewValue(newValue),
});
};
const rifmFormat = React.useMemo(() => {
- const formattedDateWith1Digit = staticDateWith1DigitTokens.format(format);
+ const formattedDateWith1Digit = staticDateWith1DigitTokens.format(
+ pickerContext.fieldFormat,
+ );
const inferredFormatPatternWith1Digits = formattedDateWith1Digit.replace(
ACCEPT_REGEX,
MASK_USER_INPUT_SYMBOL,
);
const inferredFormatPatternWith2Digits = staticDateWith2DigitTokens
- .format(format)
+ .format(pickerContext.fieldFormat)
.replace(ACCEPT_REGEX, '_');
if (inferredFormatPatternWith1Digits !== inferredFormatPatternWith2Digits) {
@@ -113,7 +121,7 @@ function MaskedDateField(props) {
})
.join('');
};
- }, [format]);
+ }, [pickerContext.fieldFormat]);
const rifmProps = useRifm({
value: inputValue,
@@ -124,9 +132,22 @@ function MaskedDateField(props) {
return (
+ pickerContext.setOpen((prev) => !prev)}
+ edge="end"
+ >
+
+
+
+ ),
+ }}
/>
);
}
diff --git a/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.tsx b/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.tsx
index aa7b8023fcaf5..7e61770e25ad6 100644
--- a/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.tsx
+++ b/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.tsx
@@ -2,6 +2,8 @@ import * as React from 'react';
import dayjs, { Dayjs } from 'dayjs';
import { useRifm } from 'rifm';
import TextField from '@mui/material/TextField';
+import InputAdornment from '@mui/material/InputAdornment';
+import IconButton from '@mui/material/IconButton';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import {
@@ -9,8 +11,13 @@ import {
DatePickerProps,
DatePickerFieldProps,
} from '@mui/x-date-pickers/DatePicker';
-import { useSplitFieldProps, useParsedFormat } from '@mui/x-date-pickers/hooks';
+import {
+ useSplitFieldProps,
+ useParsedFormat,
+ usePickerContext,
+} from '@mui/x-date-pickers/hooks';
import { useValidation, validateDate } from '@mui/x-date-pickers/validation';
+import { CalendarIcon } from '@mui/x-date-pickers/icons';
const MASK_USER_INPUT_SYMBOL = '_';
const ACCEPT_REGEX = /[\d]/gi;
@@ -27,29 +34,28 @@ function getInputValueFromValue(value: Dayjs | null, format: string) {
}
function MaskedDateField(props: DatePickerFieldProps) {
- const { slots, slotProps, ...other } = props;
-
- const { forwardedProps, internalProps } = useSplitFieldProps(other, 'date');
-
- const { format, value, onChange, timezone } = internalProps;
+ const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');
+ const pickerContext = usePickerContext();
+ const parsedFormat = useParsedFormat();
// Control the input text
const [inputValue, setInputValue] = React.useState(() =>
- getInputValueFromValue(value, format),
+ getInputValueFromValue(pickerContext.value, pickerContext.fieldFormat),
);
React.useEffect(() => {
- if (value && value.isValid()) {
- const newDisplayDate = getInputValueFromValue(value, format);
+ if (pickerContext.value && pickerContext.value.isValid()) {
+ const newDisplayDate = getInputValueFromValue(
+ pickerContext.value,
+ pickerContext.fieldFormat!,
+ );
setInputValue(newDisplayDate);
}
- }, [format, value]);
-
- const parsedFormat = useParsedFormat(internalProps);
+ }, [pickerContext.fieldFormat, pickerContext.value]);
const { hasValidationError, getValidationErrorForNewValue } = useValidation({
- value,
- timezone,
+ value: pickerContext.value,
+ timezone: pickerContext.timezone,
props: internalProps,
validator: validateDate,
});
@@ -57,20 +63,22 @@ function MaskedDateField(props: DatePickerFieldProps) {
const handleInputValueChange = (newInputValue: string) => {
setInputValue(newInputValue);
- const newValue = dayjs(newInputValue, format);
- onChange(newValue, {
+ const newValue = dayjs(newInputValue, pickerContext.fieldFormat);
+ pickerContext.setValue(newValue, {
validationError: getValidationErrorForNewValue(newValue),
});
};
const rifmFormat = React.useMemo(() => {
- const formattedDateWith1Digit = staticDateWith1DigitTokens.format(format);
+ const formattedDateWith1Digit = staticDateWith1DigitTokens.format(
+ pickerContext.fieldFormat,
+ );
const inferredFormatPatternWith1Digits = formattedDateWith1Digit.replace(
ACCEPT_REGEX,
MASK_USER_INPUT_SYMBOL,
);
const inferredFormatPatternWith2Digits = staticDateWith2DigitTokens
- .format(format)
+ .format(pickerContext.fieldFormat)
.replace(ACCEPT_REGEX, '_');
if (inferredFormatPatternWith1Digits !== inferredFormatPatternWith2Digits) {
@@ -117,7 +125,7 @@ function MaskedDateField(props: DatePickerFieldProps) {
})
.join('');
};
- }, [format]);
+ }, [pickerContext.fieldFormat]);
const rifmProps = useRifm({
value: inputValue,
@@ -128,9 +136,22 @@ function MaskedDateField(props: DatePickerFieldProps) {
return (
+ pickerContext.setOpen((prev) => !prev)}
+ edge="end"
+ >
+
+
+
+ ),
+ }}
/>
);
}
diff --git a/docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.js b/docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.js
new file mode 100644
index 0000000000000..a386d83c03392
--- /dev/null
+++ b/docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.js
@@ -0,0 +1,74 @@
+import * as React from 'react';
+import TextField from '@mui/material/TextField';
+import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
+import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
+import { DatePicker } from '@mui/x-date-pickers/DatePicker';
+import { useValidation, validateDate } from '@mui/x-date-pickers/validation';
+import {
+ useSplitFieldProps,
+ useParsedFormat,
+ usePickerContext,
+} from '@mui/x-date-pickers/hooks';
+import { CalendarIcon } from '@mui/x-date-pickers/icons';
+import { DateField } from '@mui/x-date-pickers/DateField';
+
+function ReadOnlyDateField(props) {
+ const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');
+
+ const pickerContext = usePickerContext();
+
+ const parsedFormat = useParsedFormat();
+ const { hasValidationError } = useValidation({
+ validator: validateDate,
+ value: pickerContext.value,
+ timezone: pickerContext.timezone,
+ props: internalProps,
+ });
+
+ return (
+ ,
+ sx: { cursor: 'pointer', '& *': { cursor: 'inherit' } },
+ }}
+ error={hasValidationError}
+ onClick={() => pickerContext.setOpen((prev) => !prev)}
+ />
+ );
+}
+
+function ReadOnlyOnMobileDateField(props) {
+ const pickerContext = usePickerContext();
+
+ if (pickerContext.variant === 'mobile') {
+ return ;
+ }
+
+ return ;
+}
+
+function ReadOnlyFieldDatePicker(props) {
+ return (
+
+ );
+}
+
+export default function MaterialDatePicker() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.tsx b/docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.tsx
new file mode 100644
index 0000000000000..3b3e0e22bdc96
--- /dev/null
+++ b/docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.tsx
@@ -0,0 +1,78 @@
+import * as React from 'react';
+import TextField from '@mui/material/TextField';
+import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
+import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
+import {
+ DatePicker,
+ DatePickerProps,
+ DatePickerFieldProps,
+} from '@mui/x-date-pickers/DatePicker';
+import { useValidation, validateDate } from '@mui/x-date-pickers/validation';
+import {
+ useSplitFieldProps,
+ useParsedFormat,
+ usePickerContext,
+} from '@mui/x-date-pickers/hooks';
+import { CalendarIcon } from '@mui/x-date-pickers/icons';
+import { DateField } from '@mui/x-date-pickers/DateField';
+
+function ReadOnlyDateField(props: DatePickerFieldProps) {
+ const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');
+
+ const pickerContext = usePickerContext();
+
+ const parsedFormat = useParsedFormat();
+ const { hasValidationError } = useValidation({
+ validator: validateDate,
+ value: pickerContext.value,
+ timezone: pickerContext.timezone,
+ props: internalProps,
+ });
+
+ return (
+ ,
+ sx: { cursor: 'pointer', '& *': { cursor: 'inherit' } },
+ }}
+ error={hasValidationError}
+ onClick={() => pickerContext.setOpen((prev) => !prev)}
+ />
+ );
+}
+
+function ReadOnlyOnMobileDateField(props: DatePickerFieldProps) {
+ const pickerContext = usePickerContext();
+
+ if (pickerContext.variant === 'mobile') {
+ return ;
+ }
+
+ return ;
+}
+
+function ReadOnlyFieldDatePicker(props: DatePickerProps) {
+ return (
+
+ );
+}
+
+export default function MaterialDatePicker() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.tsx.preview b/docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.tsx.preview
new file mode 100644
index 0000000000000..e3842a12cb5d3
--- /dev/null
+++ b/docs/data/date-pickers/custom-field/behavior-read-only-mobile-text-field/MaterialDatePicker.tsx.preview
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.js b/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.js
index c5637b320e0ed..c1dbf31a52867 100644
--- a/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.js
+++ b/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.js
@@ -13,26 +13,27 @@ import { CalendarIcon } from '@mui/x-date-pickers/icons';
function ReadOnlyDateField(props) {
const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');
- const { value, timezone, format } = internalProps;
- const { InputProps, slotProps, slots, ...other } = forwardedProps;
const pickerContext = usePickerContext();
-
- const parsedFormat = useParsedFormat(internalProps);
+ const parsedFormat = useParsedFormat();
const { hasValidationError } = useValidation({
validator: validateDate,
- value,
- timezone,
+ value: pickerContext.value,
+ timezone: pickerContext.timezone,
props: internalProps,
});
return (
,
sx: { cursor: 'pointer', '& *': { cursor: 'inherit' } },
diff --git a/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.tsx b/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.tsx
index a42da5e10407c..ab5c6c38f0cc7 100644
--- a/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.tsx
+++ b/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.tsx
@@ -17,26 +17,27 @@ import { CalendarIcon } from '@mui/x-date-pickers/icons';
function ReadOnlyDateField(props: DatePickerFieldProps) {
const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');
- const { value, timezone, format } = internalProps;
- const { InputProps, slotProps, slots, ...other } = forwardedProps;
const pickerContext = usePickerContext();
-
- const parsedFormat = useParsedFormat(internalProps);
+ const parsedFormat = useParsedFormat();
const { hasValidationError } = useValidation({
validator: validateDate,
- value,
- timezone,
+ value: pickerContext.value,
+ timezone: pickerContext.timezone,
props: internalProps,
});
return (
,
sx: { cursor: 'pointer', '& *': { cursor: 'inherit' } },
diff --git a/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.js b/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.js
new file mode 100644
index 0000000000000..24f16cc407460
--- /dev/null
+++ b/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.js
@@ -0,0 +1,86 @@
+import * as React from 'react';
+import dayjs from 'dayjs';
+import TextField from '@mui/material/TextField';
+import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
+import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
+import { DatePicker } from '@mui/x-date-pickers/DatePicker';
+import {
+ useSplitFieldProps,
+ useParsedFormat,
+ usePickerContext,
+} from '@mui/x-date-pickers/hooks';
+import { useValidation, validateDate } from '@mui/x-date-pickers/validation';
+
+function CustomDateField(props) {
+ const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');
+
+ const pickerContext = usePickerContext();
+ const placeholder = useParsedFormat();
+ const [inputValue, setInputValue] = useInputValue();
+
+ // Check if the current value is valid or not.
+ const { hasValidationError } = useValidation({
+ value: pickerContext.value,
+ timezone: pickerContext.timezone,
+ props: internalProps,
+ validator: validateDate,
+ });
+
+ const handleChange = (event) => {
+ const newInputValue = event.target.value;
+ const newValue = dayjs(newInputValue, pickerContext.fieldFormat);
+ setInputValue(newInputValue);
+ pickerContext.setValue(newValue);
+ };
+
+ return (
+
+ );
+}
+
+function useInputValue() {
+ const pickerContext = usePickerContext();
+ const [lastValueProp, setLastValueProp] = React.useState(pickerContext.value);
+ const [inputValue, setInputValue] = React.useState(() =>
+ createInputValue(pickerContext.value, pickerContext.fieldFormat),
+ );
+
+ if (lastValueProp !== pickerContext.value) {
+ setLastValueProp(pickerContext.value);
+ if (pickerContext.value && pickerContext.value.isValid()) {
+ setInputValue(
+ createInputValue(pickerContext.value, pickerContext.fieldFormat),
+ );
+ }
+ }
+
+ return [inputValue, setInputValue];
+}
+
+function createInputValue(value, format) {
+ if (value == null) {
+ return '';
+ }
+
+ return value.isValid() ? value.format(format) : '';
+}
+
+function CustomFieldDatePicker(props) {
+ return (
+
+ );
+}
+
+export default function MaterialDatePicker() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.tsx b/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.tsx
new file mode 100644
index 0000000000000..9d5f1711c88c6
--- /dev/null
+++ b/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.tsx
@@ -0,0 +1,90 @@
+import * as React from 'react';
+import dayjs, { Dayjs } from 'dayjs';
+import TextField from '@mui/material/TextField';
+import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
+import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
+import {
+ DatePicker,
+ DatePickerProps,
+ DatePickerFieldProps,
+} from '@mui/x-date-pickers/DatePicker';
+import {
+ useSplitFieldProps,
+ useParsedFormat,
+ usePickerContext,
+} from '@mui/x-date-pickers/hooks';
+import { useValidation, validateDate } from '@mui/x-date-pickers/validation';
+
+function CustomDateField(props: DatePickerFieldProps) {
+ const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');
+
+ const pickerContext = usePickerContext();
+ const placeholder = useParsedFormat();
+ const [inputValue, setInputValue] = useInputValue();
+
+ // Check if the current value is valid or not.
+ const { hasValidationError } = useValidation({
+ value: pickerContext.value,
+ timezone: pickerContext.timezone,
+ props: internalProps,
+ validator: validateDate,
+ });
+
+ const handleChange = (event: React.ChangeEvent) => {
+ const newInputValue = event.target.value;
+ const newValue = dayjs(newInputValue, pickerContext.fieldFormat);
+ setInputValue(newInputValue);
+ pickerContext.setValue(newValue);
+ };
+
+ return (
+
+ );
+}
+
+function useInputValue() {
+ const pickerContext = usePickerContext();
+ const [lastValueProp, setLastValueProp] = React.useState(pickerContext.value);
+ const [inputValue, setInputValue] = React.useState(() =>
+ createInputValue(pickerContext.value, pickerContext.fieldFormat),
+ );
+
+ if (lastValueProp !== pickerContext.value) {
+ setLastValueProp(pickerContext.value);
+ if (pickerContext.value && pickerContext.value.isValid()) {
+ setInputValue(
+ createInputValue(pickerContext.value, pickerContext.fieldFormat),
+ );
+ }
+ }
+
+ return [inputValue, setInputValue] as const;
+}
+
+function createInputValue(value: Dayjs | null, format: string) {
+ if (value == null) {
+ return '';
+ }
+
+ return value.isValid() ? value.format(format) : '';
+}
+
+function CustomFieldDatePicker(props: DatePickerProps) {
+ return (
+
+ );
+}
+
+export default function MaterialDatePicker() {
+ return (
+
+
+
+ );
+}
diff --git a/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.tsx.preview b/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.tsx.preview
new file mode 100644
index 0000000000000..63be53d3e536f
--- /dev/null
+++ b/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.tsx.preview
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/data/date-pickers/custom-field/custom-field.md b/docs/data/date-pickers/custom-field/custom-field.md
index e30850de8502d..197857b20fc4f 100644
--- a/docs/data/date-pickers/custom-field/custom-field.md
+++ b/docs/data/date-pickers/custom-field/custom-field.md
@@ -144,6 +144,12 @@ but you still want the UI to look like a Text Field, you can replace the field w
{{"demo": "behavior-read-only-text-field/MaterialDatePicker.js", "defaultCodeOpen": false}}
+### Using a read-only Text Field on mobile
+
+If you want to keep the default behavior on desktop but have a read-only TextField on mobile, you can conditionally render the custom field presented in the previous section:
+
+{{"demo": "behavior-read-only-mobile-text-field/MaterialDatePicker.js", "defaultCodeOpen": false}}
+
### Using a Button
If you want users to select a value exclusively through the views
@@ -154,3 +160,157 @@ and you don't want the UI to look like a Text Field, you can replace the field w
The same logic can be applied to any Range Picker:
{{"demo": "behavior-button/MaterialDateRangePicker.js", "defaultCodeOpen": false}}
+
+## Build your own custom field
+
+:::success
+The sections below show how to build a field for your Picker.
+Unlike the field components exposed by `@mui/x-date-pickers` and `@mui/x-date-pickers-pro`, those fields are not suitable for a standalone usage.
+:::
+
+### Typing
+
+Each Picker component exposes an interface describing the props it passes to its field.
+You can import it from the same endpoint as the Picker component and use it to type the props of your field:
+
+```tsx
+import { DatePickerFieldProps } from '@mui/x-date-pickers/DatePicker';
+import { DateRangePickerFieldProps } from '@mui/x-date-pickers-pro/DateRangePicker';
+
+function CustomDateField(props: DatePickerFieldProps) {
+ // Your custom field
+}
+
+function CustomDateRangeField(props: DateRangePickerFieldProps) {
+ // Your custom field
+}
+```
+
+#### Import
+
+| Picker component | Field props interface |
+| ---------------------: | :------------------------------ |
+| Date Picker | `DatePickerFieldProps` |
+| Time Picker | `TimePickerFieldProps` |
+| Date Time Picker | `DateTimePickerFieldProps` |
+| Date Range Picker | `DateRangePickerFieldProps` |
+| Date Time Range Picker | `DateTimeRangePickerFieldProps` |
+
+### Validation
+
+You can use the `useValidation` hook to check if the current value passed to your field is valid or not:
+
+```ts
+import { useValidation, validateDate } from '@mui/x-date-pickers/validation';
+
+const {
+ // The error associated with the current value.
+ // For example: "minDate" if `props.value < props.minDate`.
+ validationError,
+ // `true` if the value is invalid.
+ // On range Pickers it is true if the start date or the end date is invalid.
+ hasValidationError,
+ // Imperatively get the error of a value.
+ getValidationErrorForNewValue,
+} = useValidation({
+ // If you have a value in an internal state, you should pass it here.
+ // Otherwise, you can pass the value returned by `usePickerContext()`.
+ value,
+ timezone,
+ props,
+ validator: validateDate,
+});
+```
+
+#### Import
+
+Each Picker component has a validator adapted to its value type:
+
+| Picker component | Import validator |
+| ---------------------: | :--------------------------------------------------------------------------- |
+| Date Picker | `import { validateDate } from '@mui/x-date-pickers/validation'` |
+| Time Picker | `import { validateTime } from '@mui/x-date-pickers/validation'` |
+| Date Time Picker | `import { validateDateTime } from '@mui/x-date-pickers/validation'` |
+| Date Range Picker | `import { validateDateRange } from '@mui/x-date-pickers-pro/validation'` |
+| Date Time Range Picker | `import { validateDateTimeRange } from '@mui/x-date-pickers-pro/validation'` |
+
+### Localized placeholder
+
+You can use the `useParsedFormat` to get a clean placeholder.
+This hook applies two main transformations on the format:
+
+1. It replaces all the localized tokens (for example `L` for a date with `dayjs`) with their expanded value (`DD/MM/YYYY` for the same date with `dayjs`).
+2. It replaces each token with its token from the localization object (for example `YYYY` remains `YYYY` for the English locale but becomes `AAAA` for the French locale).
+
+:::warning
+The format returned by `useParsedFormat` cannot be parsed by your date library.
+:::
+
+```js
+import { useParsedFormat } from '@mui/x-date-pickers/hooks';
+
+// Uses the format defined by your Picker
+const parsedFormat = useParsedFormat();
+
+// Uses the custom format provided
+const parsedFormat = useParsedFormat({ format: 'MM/DD/YYYY' });
+```
+
+### Spread props to the DOM
+
+The field receives a lot of props that cannot be forwarded to the DOM element without warnings.
+You can use the `useSplitFieldProps` hook to get the props that can be forwarded safely to the DOM:
+
+```tsx
+const { internalProps, forwardedProps } = useSplitFieldProps(
+ // The props received by the field component
+ props,
+ // The value type ("date", "time" or "date-time")
+ 'date',
+);
+
+return (
+
+)
+```
+
+:::success
+The `forwardedProps` contains the `sx` which is specific to MUI.
+You can omit it if the component your are forwarding the props to does not support this concept:
+
+```jsx
+const { sx, ...other } = props;
+const { internalProps, forwardedProps } = useSplitFieldProps(other, 'date');
+
+return (
+
+)
+```
+
+:::
+
+### Pass the field to the Picker
+
+You can pass your custom field to your Picker using the `field` slot:
+
+```tsx
+function DatePickerWithCustomField() {
+ return (
+
+ )
+}
+
+// Also works with the other variants of the component
+function DesktopDatePickerWithCustomField() {
+ return (
+
+ )
+}
+
+```
+
+### Full custom example
+
+Here is a live demo of the example created in all the previous sections:
+
+{{"demo": "behavior-tutorial/MaterialDatePicker.js", "defaultCodeOpen": false}}
diff --git a/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.js b/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.js
index 5ecabf3b7b204..5331d61230248 100644
--- a/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.js
+++ b/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.js
@@ -16,7 +16,7 @@ export default function CustomPropsOpeningButton() {
},
// Targets the `InputAdornment` component.
inputAdornment: {
- position: 'start',
+ component: 'span',
},
}}
/>
diff --git a/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.tsx b/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.tsx
index 5ecabf3b7b204..5331d61230248 100644
--- a/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.tsx
+++ b/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.tsx
@@ -16,7 +16,7 @@ export default function CustomPropsOpeningButton() {
},
// Targets the `InputAdornment` component.
inputAdornment: {
- position: 'start',
+ component: 'span',
},
}}
/>
diff --git a/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.tsx.preview b/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.tsx.preview
index 685e218097c64..1487b57d2a908 100644
--- a/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.tsx.preview
+++ b/docs/data/date-pickers/custom-opening-button/CustomPropsOpeningButton.tsx.preview
@@ -6,7 +6,7 @@
},
// Targets the `InputAdornment` component.
inputAdornment: {
- position: 'start',
+ component: 'span',
},
}}
/>
\ No newline at end of file
diff --git a/docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.js b/docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.js
new file mode 100644
index 0000000000000..04a1a2803acd9
--- /dev/null
+++ b/docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.js
@@ -0,0 +1,19 @@
+import * as React from 'react';
+import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
+import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
+import { DemoContainer } from '@mui/x-date-pickers/internals/demo';
+import { DatePicker } from '@mui/x-date-pickers/DatePicker';
+
+export default function StartEdgeOpeningButton() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.tsx b/docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.tsx
new file mode 100644
index 0000000000000..04a1a2803acd9
--- /dev/null
+++ b/docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.tsx
@@ -0,0 +1,19 @@
+import * as React from 'react';
+import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
+import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
+import { DemoContainer } from '@mui/x-date-pickers/internals/demo';
+import { DatePicker } from '@mui/x-date-pickers/DatePicker';
+
+export default function StartEdgeOpeningButton() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.tsx.preview b/docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.tsx.preview
new file mode 100644
index 0000000000000..0a9731df02dd7
--- /dev/null
+++ b/docs/data/date-pickers/custom-opening-button/StartEdgeOpeningButton.tsx.preview
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/docs/data/date-pickers/custom-opening-button/custom-opening-button.md b/docs/data/date-pickers/custom-opening-button/custom-opening-button.md
index c0ef70d69eb9f..eb5061b66a5c7 100644
--- a/docs/data/date-pickers/custom-opening-button/custom-opening-button.md
+++ b/docs/data/date-pickers/custom-opening-button/custom-opening-button.md
@@ -39,6 +39,12 @@ If you want to track the opening of the picker, you should use the `onOpen` / `o
:::
+## Render the opening button at the start of the input
+
+You can use the `openPickerButtonPosition` on the `field` slot to position the opening button at the start or the end of the input:
+
+{{"demo": "StartEdgeOpeningButton.js"}}
+
## Add an icon next to the opening button
If you want to add an icon next to the opening button, you can use the `inputAdornment` slot.
diff --git a/docs/data/date-pickers/date-picker/date-picker.md b/docs/data/date-pickers/date-picker/date-picker.md
index 8467168363613..f702279ebb37d 100644
--- a/docs/data/date-pickers/date-picker/date-picker.md
+++ b/docs/data/date-pickers/date-picker/date-picker.md
@@ -46,10 +46,10 @@ Learn more about the _Controlled and uncontrolled_ pattern in the [React documen
The component is available in four variants:
- The `DesktopDatePicker` component which works best for mouse devices and large screens.
- It renders the views inside a popover and allows editing values directly inside the field.
+ It renders the views inside a popover and a field for keyboard editing.
- The `MobileDatePicker` component which works best for touch devices and small screens.
- It renders the view inside a modal and does not allow editing values directly inside the field.
+ It renders the view inside a modal and a field for keyboard editing.
- The `DatePicker` component which renders `DesktopDatePicker` or `MobileDatePicker` depending on the device it runs on.
@@ -125,12 +125,6 @@ You can enable the clearable behavior:
See [Field components—Clearable behavior](/x/react-date-pickers/fields/#clearable-behavior) for more details.
:::
-:::warning
-The clearable prop is not supported yet by the mobile Picker variants.
-
-See discussion [in this GitHub issue](https://github.com/mui/mui-x/issues/10842#issuecomment-1951887408) for more information.
-:::
-
## Localization
See the [Date format and localization](/x/react-date-pickers/adapters-locale/) and [Translated components](/x/react-date-pickers/localization/) documentation pages for more details.
diff --git a/docs/data/date-pickers/date-range-picker/date-range-picker.md b/docs/data/date-pickers/date-range-picker/date-range-picker.md
index 43e680df8d33a..227d9b971b183 100644
--- a/docs/data/date-pickers/date-range-picker/date-range-picker.md
+++ b/docs/data/date-pickers/date-range-picker/date-range-picker.md
@@ -46,10 +46,10 @@ Learn more about the _Controlled and uncontrolled_ pattern in the [React documen
The component is available in four variants:
- The `DesktopDateRangePicker` component which works best for mouse devices and large screens.
- It renders the views inside a popover and allows editing values directly inside the field.
+ It renders the views inside a popover and a field for keyboard editing.
- The `MobileDateRangePicker` component which works best for touch devices and small screens.
- It renders the view inside a modal and does not allow editing values directly inside the field.
+ It renders the view inside a modal and does not allow editing values with the keyboard in the field.
- The `DateRangePicker` component which renders `DesktopDateRangePicker` or `MobileDateRangePicker` depending on the device it runs on.
diff --git a/docs/data/date-pickers/date-time-picker/date-time-picker.md b/docs/data/date-pickers/date-time-picker/date-time-picker.md
index 075ec25dd1567..dcead399e2fc0 100644
--- a/docs/data/date-pickers/date-time-picker/date-time-picker.md
+++ b/docs/data/date-pickers/date-time-picker/date-time-picker.md
@@ -48,10 +48,10 @@ Learn more about the _Controlled and uncontrolled_ pattern in the [React documen
The component is available in four variants:
- The `DesktopDateTimePicker` component which works best for mouse devices and large screens.
- It renders the views inside a popover and allows editing values directly inside the field.
+ It renders the views inside a popover and a field for keyboard editing.
- The `MobileDateTimePicker` component which works best for touch devices and small screens.
- It renders the view inside a modal and does not allow editing values directly inside the field.
+ It renders the view inside a modal and a field for keyboard editing.
- The `DateTimePicker` component which renders `DesktopDateTimePicker` or `MobileDateTimePicker` depending on the device it runs on.
diff --git a/docs/data/date-pickers/date-time-range-picker/date-time-range-picker.md b/docs/data/date-pickers/date-time-range-picker/date-time-range-picker.md
index 92137b72ba818..3b94e397925fb 100644
--- a/docs/data/date-pickers/date-time-range-picker/date-time-range-picker.md
+++ b/docs/data/date-pickers/date-time-range-picker/date-time-range-picker.md
@@ -47,10 +47,10 @@ Learn more about the _Controlled and uncontrolled_ pattern in the [React documen
The component is available in three variants:
- The `DesktopDateTimeRangePicker` component which works best for mouse devices and large screens.
- It renders the views inside a popover and allows editing values directly inside the field.
+ It renders the views inside a popover and a field for keyboard editing.
- The `MobileDateTimeRangePicker` component which works best for touch devices and small screens.
- It renders the view inside a modal and does not allow editing values directly inside the field.
+ It renders the view inside a modal and does not allow editing values with the keyboard in the field.
- The `DateTimeRangePicker` component which renders `DesktopDateTimeRangePicker` or `MobileDateTimeRangePicker` depending on the device it runs on.
diff --git a/docs/data/date-pickers/lifecycle/lifecycle.md b/docs/data/date-pickers/lifecycle/lifecycle.md
index 4d4bf1bc62ecf..2b57bc1d3b822 100644
--- a/docs/data/date-pickers/lifecycle/lifecycle.md
+++ b/docs/data/date-pickers/lifecycle/lifecycle.md
@@ -51,9 +51,12 @@ In all the below scenarios, the picker closes when `onClose` is called, except i
#### When the last view is completed
When a selection in the last view is made, `onClose` will be called only if the `closeOnSelect` prop is equal to `true`.
-By default, it is set to `true` on desktop and `false` on mobile.
+The default value of `closeOnSelect` depends on the component:
-Here are a few examples:
+- Date Picker and Date Range Picker: `true` on desktop and `false` on mobile variants;
+- Time Picker, Date Time Picker, and Date Time Range Picker: `false` on desktop and mobile variants.
+
+ Here are a few examples:
:::info
The examples below are using the desktop and mobile variants of the pickers, but the behavior is exactly the same when using the responsive variant (`DatePicker`, `TimePicker`, ...) on a mobile or desktop environment.
@@ -75,7 +78,7 @@ The examples below are using the desktop and mobile variants of the pickers, but
- Default `views` prop: `['year', 'day']`
- Explicit `closeOnSelect` prop: `false`
- **Behavior:** The picker won't close when selecting a day. The user will have to click on the _OK_ action to close it.
+ **Behavior:** The picker will not close when selecting a day. The user will have to click on the _OK_ action to close it.
:::success
If you want to set `closeOnSelect` to `false` on a desktop picker, you should consider enabling the action bar to allow the user to validate the value:
@@ -96,7 +99,7 @@ The examples below are using the desktop and mobile variants of the pickers, but
- Default `views` prop: `['year', 'day']`
- Default `closeOnSelect` prop: `false`
- **Behavior:** The picker won't close when selecting a day. The user will have to click on the _OK_ action to close it.
+ **Behavior:** The picker will not close when selecting a day. The user will have to click on the _OK_ action to close it.
- ```tsx
@@ -112,9 +115,9 @@ The examples below are using the desktop and mobile variants of the pickers, but
```
- Default `views` prop: `['hours', 'minutes']` (plus a `meridiem` view if the locale is in 12-hours format)
- - Default `closeOnSelect` prop: `true`
+ - Default `closeOnSelect` prop: `false`
- **Behavior:** The picker will close when selecting the minutes or meridiem (if a 12-hour clock is used).
+ **Behavior:** The picker will not close when selecting the minutes or meridiem (if a 12-hour clock is used).
:::info
You don't have to fill all the views for the picker to close automatically.
@@ -225,7 +228,10 @@ You can use the second argument passed to the `onAccept` callback to get the val
#### When the last view is completed
When a selection in the last view is made, `onAccept` will be called only if the `closeOnSelect` prop is equal to `true` and the value has been modified since the last time `onAccept` was called.
-By default, `closeOnSelect`, is set to `true` on desktop and `false` on mobile.
+The default value of `closeOnSelect` depends on the component used:
+
+- Date Picker and Date Range Picker: `true` on desktop and `false` on mobile variants;
+- Time Picker, Date Time Picker, and Date Time Range Picker: `false` on desktop and mobile variants.
Here are a few examples:
@@ -249,7 +255,7 @@ The examples below are using the desktop and mobile variants of the pickers, but
- Default `views` prop: `['year', 'day']`
- Explicit `closeOnSelect` prop: `false`
- **Behavior:** The picker won't call `onAccept` when selecting a value.
+ **Behavior:** The picker will not call `onAccept` when selecting a value.
:::success
If you want to set `closeOnSelect` to `false` on a desktop picker, you should consider enabling the action bar to allow the user to validate the value:
diff --git a/docs/data/date-pickers/localization/data.json b/docs/data/date-pickers/localization/data.json
index 6874c9a28d40a..fb6709dc7b2ef 100644
--- a/docs/data/date-pickers/localization/data.json
+++ b/docs/data/date-pickers/localization/data.json
@@ -47,6 +47,14 @@
"totalKeysCount": 50,
"githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/zhCN.ts"
},
+ {
+ "languageTag": "zh-TW",
+ "importName": "zhTW",
+ "localeName": "Chinese (Taiwan)",
+ "missingKeysCount": 0,
+ "totalKeysCount": 50,
+ "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/zhTW.ts"
+ },
{
"languageTag": "hr-HR",
"importName": "hrHR",
@@ -179,7 +187,7 @@
"languageTag": "nb-NO",
"importName": "nbNO",
"localeName": "Norwegian (Bokmål)",
- "missingKeysCount": 14,
+ "missingKeysCount": 0,
"totalKeysCount": 50,
"githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-date-pickers/src/locales/nbNO.ts"
},
diff --git a/docs/data/date-pickers/shortcuts/ChangeImportance.tsx b/docs/data/date-pickers/shortcuts/ChangeImportance.tsx
index 5ef6c1e18d3eb..f494a5358849c 100644
--- a/docs/data/date-pickers/shortcuts/ChangeImportance.tsx
+++ b/docs/data/date-pickers/shortcuts/ChangeImportance.tsx
@@ -4,10 +4,8 @@ import Stack from '@mui/material/Stack';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
-import {
- PickersShortcutsItem,
- PickerShortcutChangeImportance,
-} from '@mui/x-date-pickers/PickersShortcuts';
+import { PickersShortcutsItem } from '@mui/x-date-pickers/PickersShortcuts';
+import { PickerChangeImportance } from '@mui/x-date-pickers/models';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import ToggleButton from '@mui/material/ToggleButton';
@@ -80,7 +78,7 @@ const shortcutsItems: PickersShortcutsItem[] = [
export default function ChangeImportance() {
const [changeImportance, setChangeImportance] =
- React.useState('accept');
+ React.useState('accept');
return (
diff --git a/docs/data/date-pickers/shortcuts/CustomizedRangeShortcuts.js b/docs/data/date-pickers/shortcuts/CustomizedRangeShortcuts.js
index 478f2dcb54553..c7aec7387fd89 100644
--- a/docs/data/date-pickers/shortcuts/CustomizedRangeShortcuts.js
+++ b/docs/data/date-pickers/shortcuts/CustomizedRangeShortcuts.js
@@ -9,6 +9,8 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { StaticDateRangePicker } from '@mui/x-date-pickers-pro/StaticDateRangePicker';
+import { useIsValidValue, usePickerContext } from '@mui/x-date-pickers/hooks';
+
const shortcutsItems = [
{
label: 'This Week',
@@ -51,7 +53,9 @@ const shortcutsItems = [
];
function CustomRangeShortcuts(props) {
- const { items, onChange, isValid, changeImportance = 'accept' } = props;
+ const { items, changeImportance = 'accept' } = props;
+ const isValid = useIsValidValue();
+ const { setValue } = usePickerContext();
if (items == null || items.length === 0) {
return null;
@@ -63,7 +67,7 @@ function CustomRangeShortcuts(props) {
return {
label: item.label,
onClick: () => {
- onChange(newValue, changeImportance, item);
+ setValue(newValue, { changeImportance, shortcut: item });
},
disabled: !isValid(newValue),
};
diff --git a/docs/data/date-pickers/shortcuts/CustomizedRangeShortcuts.tsx b/docs/data/date-pickers/shortcuts/CustomizedRangeShortcuts.tsx
index 87530990c3a87..3f8d779ac9662 100644
--- a/docs/data/date-pickers/shortcuts/CustomizedRangeShortcuts.tsx
+++ b/docs/data/date-pickers/shortcuts/CustomizedRangeShortcuts.tsx
@@ -13,6 +13,7 @@ import {
PickersShortcutsProps,
} from '@mui/x-date-pickers/PickersShortcuts';
import { DateRange } from '@mui/x-date-pickers-pro/models';
+import { useIsValidValue, usePickerContext } from '@mui/x-date-pickers/hooks';
const shortcutsItems: PickersShortcutsItem>[] = [
{
@@ -56,7 +57,9 @@ const shortcutsItems: PickersShortcutsItem>[] = [
];
function CustomRangeShortcuts(props: PickersShortcutsProps>) {
- const { items, onChange, isValid, changeImportance = 'accept' } = props;
+ const { items, changeImportance = 'accept' } = props;
+ const isValid = useIsValidValue>();
+ const { setValue } = usePickerContext>();
if (items == null || items.length === 0) {
return null;
@@ -68,7 +71,7 @@ function CustomRangeShortcuts(props: PickersShortcutsProps>) {
return {
label: item.label,
onClick: () => {
- onChange(newValue, changeImportance, item);
+ setValue(newValue, { changeImportance, shortcut: item });
},
disabled: !isValid(newValue),
};
diff --git a/docs/data/date-pickers/time-picker/time-picker.md b/docs/data/date-pickers/time-picker/time-picker.md
index d6db6b9d53c99..3332371393684 100644
--- a/docs/data/date-pickers/time-picker/time-picker.md
+++ b/docs/data/date-pickers/time-picker/time-picker.md
@@ -47,10 +47,10 @@ Learn more about the _Controlled and uncontrolled_ pattern in the [React documen
The component is available in four variants:
- The `DesktopTimePicker` component which works best for mouse devices and large screens.
- It renders the views inside a popover and allows editing values directly inside the field.
+ It renders the views inside a popover and a field for keyboard editing.
- The `MobileTimePicker` component which works best for touch devices and small screens.
- It renders the view inside a modal and does not allow editing values directly inside the field.
+ It renders the view inside a modal and a field for keyboard editing.
- The `TimePicker` component which renders `DesktopTimePicker` or `MobileTimePicker` depending on the device it runs on.
diff --git a/docs/data/migration/migration-charts-v7/migration-charts-v7.md b/docs/data/migration/migration-charts-v7/migration-charts-v7.md
index 72a8ce9490119..68df47f3abe3e 100644
--- a/docs/data/migration/migration-charts-v7/migration-charts-v7.md
+++ b/docs/data/migration/migration-charts-v7/migration-charts-v7.md
@@ -92,6 +92,25 @@ To pass props to the legend, use the `slotProps.legend`.
+
```
+## Legend direction value change ✅
+
+The `direction` prop of the legend has been changed to accept `'vertical'` and `'horizontal'` instead of `'column'` and `'row'`.
+
+```diff
+
+```
+
+## The `getSeriesToDisplay` function was removed
+
+The `getSeriesToDisplay` function was removed in favor of the `useLegend` hook. You can check the [HTML Components example](/x/react-charts/components/#html-components) for usage information.
+
## Removing ResponsiveChartContainer ✅
The `ResponsiveChartContainer` has been removed.
@@ -122,7 +141,7 @@ The ` ` by error had the code to render axes.
This code has been removed in v8, which implies removing the following props: `axisHighlight`, `topAxis`, `rightAxis`, `bottomAxis`, and `leftAxis`.
This should not impact your code.
-If you used axes in a pie chart please open an issue, we would be curious to get more information about the use-case.
+If you used axes in a pie chart please open an issue, we would be curious to get more information about the use case.
## Remove `resolveSizeBeforeRender` prop
diff --git a/docs/data/migration/migration-data-grid-v7/migration-data-grid-v7.md b/docs/data/migration/migration-data-grid-v7/migration-data-grid-v7.md
index 0ab2d97e6e86a..37ceef86ab6b7 100644
--- a/docs/data/migration/migration-data-grid-v7/migration-data-grid-v7.md
+++ b/docs/data/migration/migration-data-grid-v7/migration-data-grid-v7.md
@@ -36,7 +36,7 @@ Below are described the steps you need to make to migrate from v7 to v8.
### Props
-- Passing additional props (like `data-*`, `aria-*`) directly on the Data Grid component is no longer supported. To pass the props, use `slotProps`.
+- Passing additional props (like `data-*`, `aria-*`) directly on the Data Grid component is no longer supported. To pass the props, use `slotProps`:
- For the `.root` element, use `slotProps.root`
- For the `.main` element (the one with `role="grid"`), use `slotProps.main`
@@ -52,11 +52,12 @@ Below are described the steps you need to make to migrate from v7 to v8.
- The `rowPositionsDebounceMs` prop was removed.
- The `apiRef.current.resize()` method was removed.
- The ` ` component is not exported anymore.
+- The `sanitizeFilterItemValue()` utility is not exported anymore.
- `gridRowsDataRowIdToIdLookupSelector` was removed. Use `gridRowsLookupSelector` in combination with `getRowId()` API method instead.
```diff
-const idToIdLookup = gridRowsDataRowIdToIdLookupSelector(apiRef);
- -const rowId = idToIdLookup[id]
+ -const rowId = idToIdLookup[id];
+const rowsLookup = gridRowsLookupSelector(apiRef);
+const rowId = apiRef.current.getRowId(rowsLookup[id]);
```
@@ -90,13 +91,14 @@ Below are described the steps you need to make to migrate from v7 to v8.
- The `useGridSelector` signature has been updated due to the introduction of arguments parameter in the selectors. Pass `undefined` as `arguments` if the selector doesn't use any arguments.
```diff
- -const output = useGridSelector(apiRef, selector, equals)
- +const output = useGridSelector(apiRef, selector, arguments, equals)
+ -const output = useGridSelector(apiRef, selector, equals);
+ +const output = useGridSelector(apiRef, selector, arguments, equals);
```
### Other exports
-- `ariaV8` experimental flag is removed.
+- `ariaV8` experimental flag is removed. It's now the default behavior.
+- Sub-components that are in a React Portal must now be wrapped with `GridPortalWrapper`
+
+```bash
+npx @mui/x-codemod@next v8.0.0/pickers/rename-adapter-date-fns-imports
+```
+
#### `rename-and-move-field-value-type`
Renames `FieldValueType` to `PickerValueType`.
diff --git a/packages/x-codemod/package.json b/packages/x-codemod/package.json
index 4d41f0fce71c9..3e1c9126ee5b2 100644
--- a/packages/x-codemod/package.json
+++ b/packages/x-codemod/package.json
@@ -1,6 +1,6 @@
{
"name": "@mui/x-codemod",
- "version": "8.0.0-alpha.5",
+ "version": "8.0.0-alpha.7",
"bin": "./codemod.js",
"private": false,
"author": "MUI Team",
@@ -34,9 +34,9 @@
"dependencies": {
"@babel/core": "^7.26.0",
"@babel/runtime": "^7.26.0",
- "@babel/traverse": "^7.26.4",
+ "@babel/traverse": "^7.26.5",
"@mui/x-internals": "workspace:*",
- "jscodeshift": "17.1.1",
+ "jscodeshift": "17.1.2",
"yargs": "^17.7.2"
},
"devDependencies": {
diff --git a/packages/x-codemod/src/v8.0.0/charts/preset-safe/actual.spec.tsx b/packages/x-codemod/src/v8.0.0/charts/preset-safe/actual.spec.tsx
index 849585a97e2fb..9e949121e03c7 100644
--- a/packages/x-codemod/src/v8.0.0/charts/preset-safe/actual.spec.tsx
+++ b/packages/x-codemod/src/v8.0.0/charts/preset-safe/actual.spec.tsx
@@ -1,7 +1,7 @@
// @ts-nocheck
import * as React from 'react';
import { PieChart } from '@mui/x-charts/PieChart';
-import { BarPlot } from '@mui/x-charts/BarChart';
+import { BarPlot, BarChart } from '@mui/x-charts/BarChart';
import { ResponsiveChartContainer } from '@mui/x-charts/ResponsiveChartContainer';
import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis';
@@ -25,4 +25,7 @@ import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis';
labelStyle={{ fontWeight: 'bold', fontSize: 10 }}
tickStyle={{ fontWeight: 'bold', fontSize: 12 }}
/>
+
+
+
;
diff --git a/packages/x-codemod/src/v8.0.0/charts/preset-safe/expected.spec.tsx b/packages/x-codemod/src/v8.0.0/charts/preset-safe/expected.spec.tsx
index 139cad04ca12c..b674eb48cbe82 100644
--- a/packages/x-codemod/src/v8.0.0/charts/preset-safe/expected.spec.tsx
+++ b/packages/x-codemod/src/v8.0.0/charts/preset-safe/expected.spec.tsx
@@ -1,7 +1,7 @@
// @ts-nocheck
import * as React from 'react';
import { PieChart } from '@mui/x-charts/PieChart';
-import { BarPlot } from '@mui/x-charts/BarChart';
+import { BarPlot, BarChart } from '@mui/x-charts/BarChart';
import { ChartContainer } from '@mui/x-charts/ChartContainer';
import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis';
@@ -44,4 +44,23 @@ import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis';
fontWeight: 'bold',
fontSize: 12
}} />
+
+
+
;
diff --git a/packages/x-codemod/src/v8.0.0/charts/preset-safe/index.ts b/packages/x-codemod/src/v8.0.0/charts/preset-safe/index.ts
index 3fdc6150db550..54cc36d4595ab 100644
--- a/packages/x-codemod/src/v8.0.0/charts/preset-safe/index.ts
+++ b/packages/x-codemod/src/v8.0.0/charts/preset-safe/index.ts
@@ -1,6 +1,7 @@
import transformLegendToSlots from '../rename-legend-to-slots-legend';
import transformRemoveResponsiveContainer from '../rename-responsive-chart-container';
import transformRenameLabelAndTickFontSize from '../rename-label-and-tick-font-size';
+import transformReplaceLegendDirectionValues from '../replace-legend-direction-values';
import { JsCodeShiftAPI, JsCodeShiftFileInfo } from '../../../types';
@@ -8,6 +9,8 @@ export default function transformer(file: JsCodeShiftFileInfo, api: JsCodeShiftA
file.source = transformLegendToSlots(file, api, options);
file.source = transformRemoveResponsiveContainer(file, api, options);
file.source = transformRenameLabelAndTickFontSize(file, api, options);
+ file.source = transformRenameLabelAndTickFontSize(file, api, options);
+ file.source = transformReplaceLegendDirectionValues(file, api, options);
return file.source;
}
diff --git a/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/actual.spec.tsx b/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/actual.spec.tsx
new file mode 100644
index 0000000000000..930f06082a354
--- /dev/null
+++ b/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/actual.spec.tsx
@@ -0,0 +1,10 @@
+// @ts-nocheck
+import * as React from 'react';
+import { BarChart } from '@mui/x-charts/BarChart';
+
+// prettier-ignore
+
+
+
+
+
;
diff --git a/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/expected.spec.tsx b/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/expected.spec.tsx
new file mode 100644
index 0000000000000..13791eaec74f8
--- /dev/null
+++ b/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/expected.spec.tsx
@@ -0,0 +1,26 @@
+// @ts-nocheck
+import * as React from 'react';
+import { BarChart } from '@mui/x-charts/BarChart';
+
+// prettier-ignore
+
+
+
+
+
;
diff --git a/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/index.ts b/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/index.ts
new file mode 100644
index 0000000000000..2c1f748a75a3f
--- /dev/null
+++ b/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/index.ts
@@ -0,0 +1,75 @@
+import { JSXAttribute, JSXExpressionContainer, ObjectExpression } from 'jscodeshift';
+import { JsCodeShiftAPI, JsCodeShiftFileInfo } from '../../../types';
+import { transformNestedProp } from '../../../util/addComponentsSlots';
+/**
+ * @param {import('jscodeshift').FileInfo} file
+ * @param {import('jscodeshift').API} api
+ */
+export default function transformer(file: JsCodeShiftFileInfo, api: JsCodeShiftAPI, options: any) {
+ const j = api.jscodeshift;
+
+ const printOptions = options.printOptions;
+
+ const root = j(file.source);
+
+ root
+ .find(j.ImportDeclaration)
+ .filter(({ node }) => {
+ return typeof node.source.value === 'string' && node.source.value.startsWith('@mui/x-charts');
+ })
+ .forEach((path) => {
+ path.node.specifiers?.forEach((node) => {
+ root.findJSXElements(node.local?.name).forEach((elementPath) => {
+ if (elementPath.node.type !== 'JSXElement') {
+ return;
+ }
+
+ const slotProps = elementPath.node.openingElement.attributes?.find(
+ (elementNode) =>
+ elementNode.type === 'JSXAttribute' && elementNode.name.name === 'slotProps',
+ ) as JSXAttribute | null;
+
+ if (slotProps === null) {
+ // No slotProps to manage
+ return;
+ }
+
+ const direction = (
+ (slotProps?.value as JSXExpressionContainer | null)?.expression as ObjectExpression
+ )?.properties
+ // @ts-expect-error
+ ?.find((v) => v?.key?.name === 'legend')
+ // @ts-expect-error
+ ?.value?.properties?.find((v) => v?.key?.name === 'direction');
+
+ if (
+ direction === undefined ||
+ direction?.value === undefined ||
+ direction?.value?.value === undefined
+ ) {
+ return;
+ }
+ const directionValue = direction.value;
+
+ directionValue.value = mapFix(directionValue.value);
+
+ transformNestedProp(elementPath, 'slotProps', 'legend.direction', directionValue, j);
+ });
+ });
+ });
+
+ const transformed = root.findJSXElements();
+
+ return transformed.toSource(printOptions);
+}
+
+function mapFix(v?: string) {
+ switch (v) {
+ case 'row':
+ return 'horizontal';
+ case 'column':
+ return 'vertical';
+ default:
+ return v;
+ }
+}
diff --git a/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/replace-legend-direction-values.test.ts b/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/replace-legend-direction-values.test.ts
new file mode 100644
index 0000000000000..0d3f4daddffaa
--- /dev/null
+++ b/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/replace-legend-direction-values.test.ts
@@ -0,0 +1,38 @@
+import path from 'path';
+import { expect } from 'chai';
+import jscodeshift from 'jscodeshift';
+import transform from '.';
+import readFile from '../../../util/readFile';
+
+function read(fileName) {
+ return readFile(path.join(__dirname, fileName));
+}
+
+describe('v8.0.0/charts', () => {
+ describe('rename-label-and-tick-font-size.test', () => {
+ const actualPath = `./actual.spec.tsx`;
+ const expectedPath = `./expected.spec.tsx`;
+
+ it('transforms imports as needed', () => {
+ const actual = transform(
+ { source: read(actualPath) },
+ { jscodeshift: jscodeshift.withParser('tsx') },
+ {},
+ );
+
+ const expected = read(expectedPath);
+ expect(actual).to.equal(expected, 'The transformed version should be correct');
+ });
+
+ it('should be idempotent', () => {
+ const actual = transform(
+ { source: read(expectedPath) },
+ { jscodeshift: jscodeshift.withParser('tsx') },
+ {},
+ );
+
+ const expected = read(expectedPath);
+ expect(actual).to.equal(expected, 'The transformed version should be correct');
+ });
+ });
+});
diff --git a/packages/x-codemod/src/v8.0.0/pickers/preset-safe/index.ts b/packages/x-codemod/src/v8.0.0/pickers/preset-safe/index.ts
index 38f3f1ff543be..2fdaaf49ca46e 100644
--- a/packages/x-codemod/src/v8.0.0/pickers/preset-safe/index.ts
+++ b/packages/x-codemod/src/v8.0.0/pickers/preset-safe/index.ts
@@ -1,8 +1,10 @@
+import transformAdapterDateFnsImports from '../rename-adapter-date-fns-imports';
import transformFieldValue from '../rename-and-move-field-value-type';
import { JsCodeShiftAPI, JsCodeShiftFileInfo } from '../../../types';
export default function transformer(file: JsCodeShiftFileInfo, api: JsCodeShiftAPI, options: any) {
+ file.source = transformAdapterDateFnsImports(file, api, options);
file.source = transformFieldValue(file, api, options);
return file.source;
diff --git a/packages/x-codemod/src/v8.0.0/pickers/rename-adapter-date-fns-imports/actual-community-import.spec.tsx b/packages/x-codemod/src/v8.0.0/pickers/rename-adapter-date-fns-imports/actual-community-import.spec.tsx
new file mode 100644
index 0000000000000..53e6dbde1a33f
--- /dev/null
+++ b/packages/x-codemod/src/v8.0.0/pickers/rename-adapter-date-fns-imports/actual-community-import.spec.tsx
@@ -0,0 +1,5 @@
+// @ts-nocheck
+import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV3';
+import { AdapterDateFns as DateFns } from '@mui/x-date-pickers/AdapterDateFns';
+import { AdapterDateFnsJalali } from '@mui/x-date-pickers/AdapterDateFnsJalaliV3';
+import { AdapterDateFnsJalali as DateFnsJalali } from '@mui/x-date-pickers/AdapterDateFnsJalali';
diff --git a/packages/x-codemod/src/v8.0.0/pickers/rename-adapter-date-fns-imports/actual-pro-import.spec.tsx b/packages/x-codemod/src/v8.0.0/pickers/rename-adapter-date-fns-imports/actual-pro-import.spec.tsx
new file mode 100644
index 0000000000000..ca85909296a2b
--- /dev/null
+++ b/packages/x-codemod/src/v8.0.0/pickers/rename-adapter-date-fns-imports/actual-pro-import.spec.tsx
@@ -0,0 +1,5 @@
+// @ts-nocheck
+import { AdapterDateFns } from '@mui/x-date-pickers-pro/AdapterDateFnsV3';
+import { AdapterDateFns as DateFns } from '@mui/x-date-pickers-pro/AdapterDateFns';
+import { AdapterDateFnsJalali } from '@mui/x-date-pickers-pro/AdapterDateFnsJalaliV3';
+import { AdapterDateFnsJalali as DateFnsJalali } from '@mui/x-date-pickers-pro/AdapterDateFnsJalali';
diff --git a/packages/x-codemod/src/v8.0.0/pickers/rename-adapter-date-fns-imports/expected-community-import.spec.tsx b/packages/x-codemod/src/v8.0.0/pickers/rename-adapter-date-fns-imports/expected-community-import.spec.tsx
new file mode 100644
index 0000000000000..bb36e95b6e5e3
--- /dev/null
+++ b/packages/x-codemod/src/v8.0.0/pickers/rename-adapter-date-fns-imports/expected-community-import.spec.tsx
@@ -0,0 +1,4 @@
+import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
+import { AdapterDateFns as DateFns } from '@mui/x-date-pickers/AdapterDateFnsV2';
+import { AdapterDateFnsJalali } from '@mui/x-date-pickers/AdapterDateFnsJalali';
+import { AdapterDateFnsJalali as DateFnsJalali } from '@mui/x-date-pickers/AdapterDateFnsJalaliV2';
diff --git a/packages/x-codemod/src/v8.0.0/pickers/rename-adapter-date-fns-imports/expected-pro-import.spec.tsx b/packages/x-codemod/src/v8.0.0/pickers/rename-adapter-date-fns-imports/expected-pro-import.spec.tsx
new file mode 100644
index 0000000000000..f1c056ca9a87e
--- /dev/null
+++ b/packages/x-codemod/src/v8.0.0/pickers/rename-adapter-date-fns-imports/expected-pro-import.spec.tsx
@@ -0,0 +1,4 @@
+import { AdapterDateFns } from '@mui/x-date-pickers-pro/AdapterDateFns';
+import { AdapterDateFns as DateFns } from '@mui/x-date-pickers-pro/AdapterDateFnsV2';
+import { AdapterDateFnsJalali } from '@mui/x-date-pickers-pro/AdapterDateFnsJalali';
+import { AdapterDateFnsJalali as DateFnsJalali } from '@mui/x-date-pickers-pro/AdapterDateFnsJalaliV2';
diff --git a/packages/x-codemod/src/v8.0.0/pickers/rename-adapter-date-fns-imports/index.ts b/packages/x-codemod/src/v8.0.0/pickers/rename-adapter-date-fns-imports/index.ts
new file mode 100644
index 0000000000000..904753d38107b
--- /dev/null
+++ b/packages/x-codemod/src/v8.0.0/pickers/rename-adapter-date-fns-imports/index.ts
@@ -0,0 +1,49 @@
+import type { JsCodeShiftAPI, JsCodeShiftFileInfo } from '../../../types';
+import { renameImports } from '../../../util/renameImports';
+
+export default function transformer(file: JsCodeShiftFileInfo, api: JsCodeShiftAPI, options: any) {
+ const j = api.jscodeshift;
+ const root = j(file.source);
+
+ const printOptions = options.printOptions || {
+ quote: 'single',
+ trailingComma: true,
+ };
+
+ renameImports({
+ j,
+ root,
+ packageNames: ['@mui/x-date-pickers', '@mui/x-date-pickers-pro'],
+ imports: [
+ {
+ oldEndpoint: 'AdapterDateFns',
+ newEndpoint: 'AdapterDateFnsV2',
+ importsMapping: {
+ AdapterDateFns: 'AdapterDateFns',
+ },
+ },
+ {
+ oldEndpoint: 'AdapterDateFnsV3',
+ newEndpoint: 'AdapterDateFns',
+ importsMapping: {
+ AdapterDateFns: 'AdapterDateFns',
+ },
+ },
+ {
+ oldEndpoint: 'AdapterDateFnsJalali',
+ newEndpoint: 'AdapterDateFnsJalaliV2',
+ importsMapping: {
+ AdapterDateFnsJalali: 'AdapterDateFnsJalali',
+ },
+ },
+ {
+ oldEndpoint: 'AdapterDateFnsJalaliV3',
+ newEndpoint: 'AdapterDateFnsJalali',
+ importsMapping: {
+ AdapterDateFnsJalali: 'AdapterDateFnsJalali',
+ },
+ },
+ ],
+ });
+ return root.toSource(printOptions);
+}
diff --git a/packages/x-codemod/src/v8.0.0/pickers/rename-adapter-date-fns-imports/rename-adapter-date-fns-imports.test.ts b/packages/x-codemod/src/v8.0.0/pickers/rename-adapter-date-fns-imports/rename-adapter-date-fns-imports.test.ts
new file mode 100644
index 0000000000000..adf48ace48983
--- /dev/null
+++ b/packages/x-codemod/src/v8.0.0/pickers/rename-adapter-date-fns-imports/rename-adapter-date-fns-imports.test.ts
@@ -0,0 +1,33 @@
+import path from 'path';
+import { expect } from 'chai';
+import jscodeshift from 'jscodeshift';
+import transform from '.';
+import readFile from '../../../util/readFile';
+
+function read(fileName) {
+ return readFile(path.join(__dirname, fileName));
+}
+
+const TEST_FILES = ['community-import', 'pro-import'];
+
+describe('v8.0.0/pickers', () => {
+ describe('rename-adapter-date-fns-imports', () => {
+ TEST_FILES.forEach((testFile) => {
+ const actualPath = `./actual-${testFile}.spec.tsx`;
+ const expectedPath = `./expected-${testFile}.spec.tsx`;
+
+ describe(`${testFile.replace(/-/g, ' ')}`, () => {
+ it('transforms imports as needed', () => {
+ const actual = transform(
+ { source: read(actualPath) },
+ { jscodeshift: jscodeshift.withParser('tsx') },
+ {},
+ );
+
+ const expected = read(expectedPath);
+ expect(actual).to.equal(expected, 'The transformed version should be correct');
+ });
+ });
+ });
+ });
+});
diff --git a/packages/x-codemod/tsconfig.json b/packages/x-codemod/tsconfig.json
index 8ed9735012229..97636e2180272 100644
--- a/packages/x-codemod/tsconfig.json
+++ b/packages/x-codemod/tsconfig.json
@@ -11,7 +11,8 @@
"moment-timezone",
"node"
],
- "noImplicitAny": false
+ "noImplicitAny": false,
+ "skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["src/**/actual*.ts*"]
diff --git a/packages/x-data-grid-generator/package.json b/packages/x-data-grid-generator/package.json
index 4e259bf5a51e3..0d244d3f74931 100644
--- a/packages/x-data-grid-generator/package.json
+++ b/packages/x-data-grid-generator/package.json
@@ -1,6 +1,6 @@
{
"name": "@mui/x-data-grid-generator",
- "version": "8.0.0-alpha.5",
+ "version": "8.0.0-alpha.7",
"description": "Generate fake data for demo purposes only.",
"author": "MUI Team",
"main": "src/index.ts",
@@ -40,8 +40,8 @@
"lru-cache": "^11.0.2"
},
"devDependencies": {
- "@mui/icons-material": "^5.16.11",
- "@mui/material": "^5.16.11",
+ "@mui/icons-material": "^5.16.14",
+ "@mui/material": "^5.16.14",
"@types/chance": "^1.1.6",
"rimraf": "^6.0.1"
},
diff --git a/packages/x-data-grid-generator/src/hooks/serverUtils.ts b/packages/x-data-grid-generator/src/hooks/serverUtils.ts
index c0643d608e923..b44a26a1bf7f4 100644
--- a/packages/x-data-grid-generator/src/hooks/serverUtils.ts
+++ b/packages/x-data-grid-generator/src/hooks/serverUtils.ts
@@ -8,12 +8,32 @@ import {
GridRowId,
GridPaginationModel,
GridValidRowModel,
-} from '@mui/x-data-grid-pro';
+ GRID_AGGREGATION_FUNCTIONS,
+ GridAggregationModel,
+ GridAggregationFunction,
+} from '@mui/x-data-grid-premium';
import { GridStateColDef } from '@mui/x-data-grid-pro/internals';
import { randomInt } from '../services/random-generator';
+const getAvailableAggregationFunctions = (columnType: GridColDef['type']) => {
+ const availableAggregationFunctions = new Map();
+ Object.keys(GRID_AGGREGATION_FUNCTIONS).forEach((functionName) => {
+ const columnTypes =
+ GRID_AGGREGATION_FUNCTIONS[functionName as keyof typeof GRID_AGGREGATION_FUNCTIONS]
+ .columnTypes;
+ if (!columnTypes || columnTypes.includes(columnType ?? 'string')) {
+ availableAggregationFunctions.set(
+ functionName,
+ GRID_AGGREGATION_FUNCTIONS[functionName as keyof typeof GRID_AGGREGATION_FUNCTIONS],
+ );
+ }
+ });
+ return availableAggregationFunctions;
+};
+
export interface FakeServerResponse {
returnedRows: GridRowModel[];
+ aggregateRow?: GridValidRowModel;
nextCursor?: string;
hasNextPage?: boolean;
totalRowCount: number;
@@ -39,6 +59,7 @@ export interface QueryOptions {
page?: number;
pageSize?: number;
filterModel?: GridFilterModel;
+ aggregationModel?: GridAggregationModel;
sortModel?: GridSortModel;
start?: number;
end?: number;
@@ -50,6 +71,7 @@ export interface ServerSideQueryOptions {
groupKeys?: string[];
filterModel?: GridFilterModel;
sortModel?: GridSortModel;
+ aggregationModel?: GridAggregationModel;
start?: number;
end?: number;
groupFields?: string[];
@@ -261,6 +283,41 @@ const getFilteredRows = (
);
};
+const applyAggregation = (
+ aggregationModel: GridAggregationModel,
+ colDefs: GridColDef[],
+ rows: GridRowModel[],
+ groupId: string = 'root',
+) => {
+ const columnsToAggregate = Object.keys(aggregationModel);
+ if (columnsToAggregate.length === 0) {
+ return {};
+ }
+
+ const aggregateValues: GridValidRowModel = {};
+ columnsToAggregate.forEach((field) => {
+ const type = colDefs.find(({ field: f }) => f === field)?.type;
+ if (!type) {
+ return;
+ }
+ const availableAggregationFunctions = getAvailableAggregationFunctions(type);
+ if (!availableAggregationFunctions.has(aggregationModel[field])) {
+ return;
+ }
+ const aggregationFunction = availableAggregationFunctions.get(aggregationModel[field]);
+ if (!aggregationFunction) {
+ return;
+ }
+ const values = rows.map((row) => row[field]);
+ aggregateValues[`${field}Aggregate`] = aggregationFunction.apply({
+ values,
+ field,
+ groupId,
+ });
+ });
+ return aggregateValues;
+};
+
/**
* Simulates server data loading
*/
@@ -288,6 +345,15 @@ export const loadServerRows = (
const rowComparator = getRowComparator(queryOptions.sortModel, columnsWithDefaultColDef);
filteredRows = [...filteredRows].sort(rowComparator);
+ let aggregateRow = {};
+ if (queryOptions.aggregationModel) {
+ aggregateRow = applyAggregation(
+ queryOptions.aggregationModel,
+ columnsWithDefaultColDef,
+ filteredRows,
+ );
+ }
+
const totalRowCount = filteredRows.length;
if (start !== undefined && end !== undefined) {
firstRowIndex = start;
@@ -311,6 +377,7 @@ export const loadServerRows = (
hasNextPage,
nextCursor,
totalRowCount,
+ ...(queryOptions.aggregationModel ? { aggregateRow } : {}),
};
return new Promise((resolve) => {
@@ -323,6 +390,7 @@ export const loadServerRows = (
interface NestedDataRowsResponse {
rows: GridRowModel[];
rootRowCount: number;
+ aggregateRow?: GridRowModel;
}
const findTreeDataRowChildren = (
@@ -464,6 +532,19 @@ export const processTreeDataRows = (
let childRowsWithDescendantCounts = childRows.map((row) => {
const descendants = findTreeDataRowChildren(filteredRows, row[pathKey], pathKey, -1);
const descendantCount = descendants.length;
+ if (descendantCount > 0 && queryOptions.aggregationModel) {
+ // Parent row, compute aggregation
+ return {
+ ...row,
+ descendantCount,
+ ...applyAggregation(
+ queryOptions.aggregationModel,
+ columnsWithDefaultColDef,
+ descendants,
+ row.id,
+ ),
+ };
+ }
return { ...row, descendantCount } as GridRowModel;
});
@@ -473,6 +554,15 @@ export const processTreeDataRows = (
childRowsWithDescendantCounts = [...childRowsWithDescendantCounts].sort(rowComparator);
}
+ let aggregateRow: GridRowModel | undefined;
+ if (queryOptions.aggregationModel) {
+ aggregateRow = applyAggregation(
+ queryOptions.aggregationModel,
+ columnsWithDefaultColDef,
+ filteredRows,
+ );
+ }
+
if (queryOptions.paginationModel && queryOptions.groupKeys.length === 0) {
// Only paginate root rows, grid should refetch root rows when `paginationModel` updates
const { pageSize, page } = queryOptions.paginationModel;
@@ -486,7 +576,7 @@ export const processTreeDataRows = (
return new Promise((resolve) => {
setTimeout(() => {
- resolve({ rows: childRowsWithDescendantCounts, rootRowCount });
+ resolve({ rows: childRowsWithDescendantCounts, rootRowCount, aggregateRow });
}, delay); // simulate network latency
});
};
@@ -584,6 +674,19 @@ export const processRowGroupingRows = (
({ id }) => typeof id !== 'string' || !id.startsWith('auto-generated-parent-'),
);
const descendantCount = descendants.length;
+ if (descendantCount > 0 && queryOptions.aggregationModel) {
+ // Parent row, compute aggregation
+ return {
+ ...row,
+ descendantCount,
+ ...applyAggregation(
+ queryOptions.aggregationModel,
+ columnsWithDefaultColDef,
+ descendants,
+ row.id,
+ ),
+ };
+ }
return { ...row, descendantCount } as GridRowModel;
});
@@ -594,6 +697,15 @@ export const processRowGroupingRows = (
childRowsWithDescendantCounts = [...sortedMissingGroups, ...sortedChildRows];
}
+ let aggregateRow: GridRowModel | undefined;
+ if (queryOptions.aggregationModel) {
+ aggregateRow = applyAggregation(
+ queryOptions.aggregationModel,
+ columnsWithDefaultColDef,
+ filteredRows,
+ );
+ }
+
if (queryOptions.paginationModel && queryOptions.groupKeys.length === 0) {
// Only paginate root rows, grid should refetch root rows when `paginationModel` updates
const { pageSize, page } = queryOptions.paginationModel;
@@ -607,7 +719,7 @@ export const processRowGroupingRows = (
return new Promise((resolve) => {
setTimeout(() => {
- resolve({ rows: childRowsWithDescendantCounts, rootRowCount });
+ resolve({ rows: childRowsWithDescendantCounts, rootRowCount, aggregateRow });
}, delay); // simulate network latency
});
};
diff --git a/packages/x-data-grid-generator/src/hooks/useMockServer.ts b/packages/x-data-grid-generator/src/hooks/useMockServer.ts
index 9fb8aff79a8c2..a150839bb0eb4 100644
--- a/packages/x-data-grid-generator/src/hooks/useMockServer.ts
+++ b/packages/x-data-grid-generator/src/hooks/useMockServer.ts
@@ -7,7 +7,7 @@ import {
GridColDef,
GridInitialState,
GridColumnVisibilityModel,
-} from '@mui/x-data-grid-pro';
+} from '@mui/x-data-grid-premium';
import { extrapolateSeed, deepFreeze } from './useDemoData';
import { getCommodityColumns } from '../columns/commodities.columns';
import { getEmployeeColumns } from '../columns/employees.columns';
@@ -298,7 +298,7 @@ export const useMockServer = (
}
if (isTreeData) {
- const { rows, rootRowCount } = await processTreeDataRows(
+ const { rows, rootRowCount, aggregateRow } = await processTreeDataRows(
data?.rows ?? [],
params,
serverOptionsWithDefault,
@@ -308,9 +308,10 @@ export const useMockServer = (
getRowsResponse = {
rows: rows.slice().map((row) => ({ ...row, path: undefined })),
rowCount: rootRowCount,
+ ...(aggregateRow ? { aggregateRow } : {}),
};
} else if (isRowGrouping) {
- const { rows, rootRowCount } = await processRowGroupingRows(
+ const { rows, rootRowCount, aggregateRow } = await processRowGroupingRows(
data?.rows ?? [],
params,
serverOptionsWithDefault,
@@ -320,15 +321,21 @@ export const useMockServer = (
getRowsResponse = {
rows: rows.slice().map((row) => ({ ...row, path: undefined })),
rowCount: rootRowCount,
+ ...(aggregateRow ? { aggregateRow } : {}),
};
} else {
- const { returnedRows, nextCursor, totalRowCount } = await loadServerRows(
+ const { returnedRows, nextCursor, totalRowCount, aggregateRow } = await loadServerRows(
data?.rows ?? [],
{ ...params, ...params.paginationModel },
serverOptionsWithDefault,
columnsWithDefaultColDef,
);
- getRowsResponse = { rows: returnedRows, rowCount: totalRowCount, pageInfo: { nextCursor } };
+ getRowsResponse = {
+ rows: returnedRows,
+ rowCount: totalRowCount,
+ pageInfo: { nextCursor },
+ ...(aggregateRow ? { aggregateRow } : {}),
+ };
}
return new Promise((resolve) => {
diff --git a/packages/x-data-grid-premium/package.json b/packages/x-data-grid-premium/package.json
index d4dc13b8cd042..e860e3bae61b8 100644
--- a/packages/x-data-grid-premium/package.json
+++ b/packages/x-data-grid-premium/package.json
@@ -1,6 +1,6 @@
{
"name": "@mui/x-data-grid-premium",
- "version": "8.0.0-alpha.5",
+ "version": "8.0.0-alpha.7",
"description": "The Premium plan edition of the Data Grid Components (MUI X).",
"author": "MUI Team",
"main": "src/index.ts",
@@ -72,11 +72,11 @@
}
},
"devDependencies": {
- "@mui/internal-test-utils": "^1.0.23",
- "@mui/material": "^5.16.11",
- "@mui/system": "^5.16.8",
+ "@mui/internal-test-utils": "^1.0.26",
+ "@mui/material": "^5.16.14",
+ "@mui/system": "^5.16.14",
"@types/prop-types": "^15.7.14",
- "date-fns": "^2.30.0",
+ "date-fns": "^4.1.0",
"rimraf": "^6.0.1"
},
"engines": {
diff --git a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx
index 9fe11a88239bf..c744c98726147 100644
--- a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx
+++ b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx
@@ -9,6 +9,7 @@ import {
PropValidator,
validateProps,
} from '@mui/x-data-grid-pro/internals';
+import { forwardRef } from '@mui/x-internals/forwardRef';
import { useDataGridPremiumComponent } from './useDataGridPremiumComponent';
import {
DataGridPremiumProcessedProps,
@@ -35,7 +36,7 @@ if (process.env.NODE_ENV !== 'production') {
dataGridPremiumPropValidators = [...propValidatorsDataGrid, ...propValidatorsDataGridPro];
}
-const DataGridPremiumRaw = React.forwardRef(function DataGridPremium(
+const DataGridPremiumRaw = forwardRef(function DataGridPremium(
inProps: DataGridPremiumProps,
ref: React.Ref,
) {
@@ -52,8 +53,8 @@ const DataGridPremiumRaw = React.forwardRef(function DataGridPremium
@@ -68,7 +69,7 @@ DataGridPremiumRaw.propTypes = {
// ----------------------------------------------------------------------
/**
* Aggregation functions available on the grid.
- * @default GRID_AGGREGATION_FUNCTIONS
+ * @default GRID_AGGREGATION_FUNCTIONS when `unstable_dataSource` is not provided, `{}` when `unstable_dataSource` is provided
*/
aggregationFunctions: PropTypes.object,
/**
@@ -1068,6 +1069,7 @@ DataGridPremiumRaw.propTypes = {
*/
treeData: PropTypes.bool,
unstable_dataSource: PropTypes.shape({
+ getAggregatedValue: PropTypes.func,
getChildrenCount: PropTypes.func,
getGroupKey: PropTypes.func,
getRows: PropTypes.func.isRequired,
diff --git a/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx b/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx
index c537e38db3a74..d357458259595 100644
--- a/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx
+++ b/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx
@@ -67,7 +67,6 @@ import {
virtualizationStateInitializer,
useGridVirtualization,
useGridDataSourceTreeDataPreProcessors,
- useGridDataSource,
dataSourceStateInitializer,
useGridRowSpanning,
rowSpanningStateInitializer,
@@ -76,6 +75,7 @@ import {
} from '@mui/x-data-grid-pro/internals';
import { GridApiPremium, GridPrivateApiPremium } from '../models/gridApiPremium';
import { DataGridPremiumProcessedProps } from '../models/dataGridPremiumProps';
+import { useGridDataSourcePremium as useGridDataSource } from '../hooks/features/dataSource/useGridDataSourcePremium';
// Premium-only features
import {
useGridAggregation,
@@ -96,7 +96,7 @@ import {
import { useGridClipboardImport } from '../hooks/features/clipboard/useGridClipboardImport';
export const useDataGridPremiumComponent = (
- inputApiRef: React.MutableRefObject | undefined,
+ inputApiRef: React.RefObject | undefined,
props: DataGridPremiumProcessedProps,
) => {
const apiRef = useGridInitialization(inputApiRef, props);
@@ -153,6 +153,7 @@ export const useDataGridPremiumComponent = (
useGridRowGrouping(apiRef, props);
useGridHeaderFiltering(apiRef, props);
useGridTreeData(apiRef, props);
+ useGridDataSource(apiRef, props);
useGridAggregation(apiRef, props);
useGridKeyboardNavigation(apiRef, props);
useGridRowSelection(apiRef, props);
@@ -190,7 +191,6 @@ export const useDataGridPremiumComponent = (
useGridDimensions(apiRef, props);
useGridEvents(apiRef, props);
useGridStatePersistence(apiRef);
- useGridDataSource(apiRef, props);
useGridVirtualization(apiRef, props);
useGridListView(apiRef, props);
diff --git a/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumProps.ts b/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumProps.ts
index c047e56429235..bb316ad4c9e3e 100644
--- a/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumProps.ts
+++ b/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumProps.ts
@@ -84,6 +84,7 @@ export const useDataGridPremiumProps = (inProps: DataGridPremiumProps) => {
return React.useMemo(
() => ({
...DATA_GRID_PREMIUM_PROPS_DEFAULT_VALUES,
+ ...(themedProps.unstable_dataSource ? { aggregationFunctions: {} } : {}),
...themedProps,
localeText,
slots,
diff --git a/packages/x-data-grid-premium/src/components/GridAggregationHeader.tsx b/packages/x-data-grid-premium/src/components/GridAggregationHeader.tsx
index 0dd68ca313e74..5f673d0cbf381 100644
--- a/packages/x-data-grid-premium/src/components/GridAggregationHeader.tsx
+++ b/packages/x-data-grid-premium/src/components/GridAggregationHeader.tsx
@@ -8,7 +8,7 @@ import {
GridColumnHeaderParams,
GridColumnHeaderTitle,
} from '@mui/x-data-grid';
-import type { GridBaseColDef } from '@mui/x-data-grid/internals';
+import { vars, type GridBaseColDef } from '@mui/x-data-grid/internals';
import { getAggregationFunctionLabel } from '../hooks/features/aggregation/gridAggregationUtils';
import { useGridApiContext } from '../hooks/utils/useGridApiContext';
import { useGridRootProps } from '../hooks/utils/useGridRootProps';
@@ -37,14 +37,11 @@ const GridAggregationHeaderRoot = styled('div', {
const GridAggregationFunctionLabel = styled('div', {
name: 'MuiDataGrid',
slot: 'AggregationColumnHeaderLabel',
- overridesResolver: (_, styles) => styles.aggregationColumnHeaderLabel,
-})<{ ownerState: OwnerState }>(({ theme }) => {
- return {
- fontSize: theme.typography.caption.fontSize,
- lineHeight: 'normal',
- color: theme.palette.text.secondary,
- marginTop: -1,
- };
+})<{ ownerState: OwnerState }>({
+ fontSize: vars.typography.small.fontSize,
+ lineHeight: 'normal',
+ color: vars.colors.foreground.muted,
+ marginTop: -1,
});
const useUtilityClasses = (ownerState: OwnerState) => {
diff --git a/packages/x-data-grid-premium/src/components/GridColumnMenuAggregationItem.tsx b/packages/x-data-grid-premium/src/components/GridColumnMenuAggregationItem.tsx
index ff94890b83976..4bbe750029925 100644
--- a/packages/x-data-grid-premium/src/components/GridColumnMenuAggregationItem.tsx
+++ b/packages/x-data-grid-premium/src/components/GridColumnMenuAggregationItem.tsx
@@ -4,6 +4,7 @@ import { GridColumnMenuItemProps, useGridSelector } from '@mui/x-data-grid-pro';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import { unstable_useId as useId } from '@mui/utils';
+import { SelectChangeEvent } from '@mui/material/Select';
import { useGridApiContext } from '../hooks/utils/useGridApiContext';
import { useGridRootProps } from '../hooks/utils/useGridRootProps';
import {
@@ -17,39 +18,44 @@ import { GridAggregationModel } from '../hooks/features/aggregation/gridAggregat
function GridColumnMenuAggregationItem(props: GridColumnMenuItemProps) {
const { colDef } = props;
const apiRef = useGridApiContext();
+ const inputRef = React.useRef(null);
const rootProps = useGridRootProps();
const id = useId();
const aggregationModel = useGridSelector(apiRef, gridAggregationModelSelector);
-
const availableAggregationFunctions = React.useMemo(
() =>
getAvailableAggregationFunctions({
aggregationFunctions: rootProps.aggregationFunctions,
colDef,
+ isDataSource: !!rootProps.unstable_dataSource,
}),
- [colDef, rootProps.aggregationFunctions],
+ [colDef, rootProps.aggregationFunctions, rootProps.unstable_dataSource],
);
+ const { native: isBaseSelectNative = false, ...baseSelectProps } =
+ rootProps.slotProps?.baseSelect || {};
+
+ const baseSelectOptionProps = rootProps.slotProps?.baseSelectOption || {};
const selectedAggregationRule = React.useMemo(() => {
if (!colDef || !aggregationModel[colDef.field]) {
return '';
}
-
const aggregationFunctionName = aggregationModel[colDef.field];
if (
canColumnHaveAggregationFunction({
colDef,
aggregationFunctionName,
aggregationFunction: rootProps.aggregationFunctions[aggregationFunctionName],
+ isDataSource: !!rootProps.unstable_dataSource,
})
) {
return aggregationFunctionName;
}
return '';
- }, [rootProps.aggregationFunctions, aggregationModel, colDef]);
+ }, [rootProps.aggregationFunctions, rootProps.unstable_dataSource, aggregationModel, colDef]);
- const handleAggregationItemChange = (event: Event) => {
+ const handleAggregationItemChange = (event: SelectChangeEvent) => {
const newAggregationItem = (event.target as HTMLSelectElement | null)?.value || undefined;
const currentModel = gridAggregationModelSelector(apiRef);
const { [colDef.field]: columnItem, ...otherColumnItems } = currentModel;
@@ -64,26 +70,60 @@ function GridColumnMenuAggregationItem(props: GridColumnMenuItemProps) {
const label = apiRef.current.getLocaleText('aggregationMenuItemHeader');
+ const handleMenuItemKeyDown = React.useCallback((event: React.KeyboardEvent) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ inputRef.current.focus();
+ }
+ }, []);
+
+ const handleSelectKeyDown = React.useCallback((event: React.KeyboardEvent) => {
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === ' ') {
+ event.stopPropagation();
+ }
+ }, []);
+
return (
}
+ onKeyDown={handleMenuItemKeyDown}
>
- {label}
+
+ {label}
+
event.stopPropagation()}
+ native={isBaseSelectNative}
fullWidth
+ {...baseSelectProps}
>
- ...
+
+ ...
+
{availableAggregationFunctions.map((aggFunc) => (
-
+
{getAggregationFunctionLabel({
apiRef,
aggregationRule: {
@@ -91,7 +131,7 @@ function GridColumnMenuAggregationItem(props: GridColumnMenuItemProps) {
aggregationFunction: rootProps.aggregationFunctions[aggFunc],
},
})}
-
+
))}
diff --git a/packages/x-data-grid-premium/src/components/GridDataSourceGroupingCriteriaCell.tsx b/packages/x-data-grid-premium/src/components/GridDataSourceGroupingCriteriaCell.tsx
index 0f7f159ee84c8..8bc4225e51ab2 100644
--- a/packages/x-data-grid-premium/src/components/GridDataSourceGroupingCriteriaCell.tsx
+++ b/packages/x-data-grid-premium/src/components/GridDataSourceGroupingCriteriaCell.tsx
@@ -1,8 +1,7 @@
import * as React from 'react';
import { unstable_composeClasses as composeClasses } from '@mui/utils';
import Box from '@mui/material/Box';
-import CircularProgress from '@mui/material/CircularProgress';
-import { useGridPrivateApiContext } from '@mui/x-data-grid-pro/internals';
+import { vars, useGridPrivateApiContext } from '@mui/x-data-grid-pro/internals';
import {
useGridSelector,
getDataGridUtilityClass,
@@ -39,7 +38,7 @@ interface GridGroupingCriteriaCellIconProps
}
function GridGroupingCriteriaCellIcon(props: GridGroupingCriteriaCellIconProps) {
- const apiRef = useGridPrivateApiContext() as React.MutableRefObject;
+ const apiRef = useGridPrivateApiContext() as React.RefObject;
const rootProps = useGridRootProps();
const classes = useUtilityClasses(rootProps);
const { rowNode, id, field, descendantCount } = props;
@@ -67,7 +66,7 @@ function GridGroupingCriteriaCellIcon(props: GridGroupingCriteriaCellIconProps)
if (isDataLoading) {
return (
-
+
);
}
@@ -125,8 +124,7 @@ export function GridDataSourceGroupingCriteriaCell(props: GridGroupingCriteriaCe
ml:
rootProps.rowGroupingColumnMode === 'multiple'
? 0
- : (theme) =>
- `calc(var(--DataGrid-cellOffsetMultiplier) * ${theme.spacing(rowNode.depth)})`,
+ : `calc(var(--DataGrid-cellOffsetMultiplier) * ${vars.spacing(rowNode.depth)})`,
}}
>
diff --git a/packages/x-data-grid-premium/src/components/GridFooterCell.tsx b/packages/x-data-grid-premium/src/components/GridFooterCell.tsx
index d7b7c601d62cc..9c11150db5352 100644
--- a/packages/x-data-grid-premium/src/components/GridFooterCell.tsx
+++ b/packages/x-data-grid-premium/src/components/GridFooterCell.tsx
@@ -1,5 +1,6 @@
import * as React from 'react';
import { getDataGridUtilityClass, GridRenderCellParams } from '@mui/x-data-grid';
+import { vars } from '@mui/x-data-grid/internals';
import { styled, Theme } from '@mui/material/styles';
import { SxProps } from '@mui/system';
import composeClasses from '@mui/utils/composeClasses';
@@ -10,10 +11,10 @@ const GridFooterCellRoot = styled('div', {
name: 'MuiDataGrid',
slot: 'FooterCell',
overridesResolver: (_, styles) => styles.footerCell,
-})<{ ownerState: OwnerState }>(({ theme }) => ({
- fontWeight: theme.typography.fontWeightMedium,
- color: (theme.vars || theme).palette.primary.dark,
-}));
+})<{ ownerState: OwnerState }>({
+ fontWeight: vars.typography.fontWeight.medium,
+ color: vars.colors.foreground.accent,
+});
interface GridFooterCellProps extends GridRenderCellParams {
sx?: SxProps
;
diff --git a/packages/x-data-grid-premium/src/components/GridGroupingColumnFooterCell.tsx b/packages/x-data-grid-premium/src/components/GridGroupingColumnFooterCell.tsx
index d98e6947d1cdd..7e73711d27f75 100644
--- a/packages/x-data-grid-premium/src/components/GridGroupingColumnFooterCell.tsx
+++ b/packages/x-data-grid-premium/src/components/GridGroupingColumnFooterCell.tsx
@@ -1,4 +1,5 @@
import * as React from 'react';
+import { vars } from '@mui/x-data-grid/internals';
import { GridRenderCellParams } from '@mui/x-data-grid-pro';
import { SxProps, Theme } from '@mui/system';
import { useGridRootProps } from '../hooks/utils/useGridRootProps';
@@ -13,8 +14,7 @@ function GridGroupingColumnFooterCell(props: GridRenderCellParams) {
} else if (rootProps.rowGroupingColumnMode === 'multiple') {
sx.ml = 2;
} else {
- sx.ml = (theme) =>
- `calc(var(--DataGrid-cellOffsetMultiplier) * ${theme.spacing(props.rowNode.depth)})`;
+ sx.ml = `calc(var(--DataGrid-cellOffsetMultiplier) * ${vars.spacing(props.rowNode.depth)})`;
}
return ;
diff --git a/packages/x-data-grid-premium/src/components/GridGroupingColumnLeafCell.tsx b/packages/x-data-grid-premium/src/components/GridGroupingColumnLeafCell.tsx
index 30f29b060b8ac..945db9869b518 100644
--- a/packages/x-data-grid-premium/src/components/GridGroupingColumnLeafCell.tsx
+++ b/packages/x-data-grid-premium/src/components/GridGroupingColumnLeafCell.tsx
@@ -1,5 +1,6 @@
import * as React from 'react';
import Box from '@mui/material/Box';
+import { vars } from '@mui/x-data-grid/internals';
import { GridRenderCellParams } from '@mui/x-data-grid-pro';
import { useGridRootProps } from '../hooks/utils/useGridRootProps';
@@ -14,9 +15,9 @@ function GridGroupingColumnLeafCell(props: GridRenderCellParams) {
? {
ml: 1,
}
- : (theme) => ({
- ml: `calc(var(--DataGrid-cellOffsetMultiplier) * var(--depth) * ${theme.spacing(1)})`,
- }),
+ : {
+ ml: `calc(var(--DataGrid-cellOffsetMultiplier) * var(--depth) * ${vars.spacing(1)})`,
+ },
]}
style={{ '--depth': rowNode.depth } as any}
>
diff --git a/packages/x-data-grid-premium/src/components/GridGroupingCriteriaCell.tsx b/packages/x-data-grid-premium/src/components/GridGroupingCriteriaCell.tsx
index 07a397c1a0b42..d99e0c909c79a 100644
--- a/packages/x-data-grid-premium/src/components/GridGroupingCriteriaCell.tsx
+++ b/packages/x-data-grid-premium/src/components/GridGroupingCriteriaCell.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import composeClasses from '@mui/utils/composeClasses';
import Box from '@mui/material/Box';
+import { vars } from '@mui/x-data-grid/internals';
import {
useGridSelector,
gridFilteredDescendantCountLookupSelector,
@@ -80,9 +81,9 @@ export function GridGroupingCriteriaCell(props: GridGroupingCriteriaCellProps) {
? {
ml: 0,
}
- : (theme) => ({
- ml: `calc(var(--DataGrid-cellOffsetMultiplier) * var(--depth) * ${theme.spacing(1)})`,
- }),
+ : {
+ ml: `calc(var(--DataGrid-cellOffsetMultiplier) * var(--depth) * ${vars.spacing(1)})`,
+ },
]}
style={{ '--depth': rowNode.depth } as any}
>
diff --git a/packages/x-data-grid-premium/src/components/GridPremiumColumnMenu.tsx b/packages/x-data-grid-premium/src/components/GridPremiumColumnMenu.tsx
index edaee7957fc32..0849d72352871 100644
--- a/packages/x-data-grid-premium/src/components/GridPremiumColumnMenu.tsx
+++ b/packages/x-data-grid-premium/src/components/GridPremiumColumnMenu.tsx
@@ -6,6 +6,7 @@ import {
GRID_COLUMN_MENU_SLOT_PROPS,
GridColumnMenuItemProps,
} from '@mui/x-data-grid-pro';
+import { forwardRef } from '@mui/x-internals/forwardRef';
import { GridColumnMenuAggregationItem } from './GridColumnMenuAggregationItem';
import { isGroupingColumn } from '../hooks/features/rowGrouping';
import { GridColumnMenuRowGroupItem } from './GridColumnMenuRowGroupItem';
@@ -32,14 +33,14 @@ export const GRID_COLUMN_MENU_SLOT_PROPS_PREMIUM = {
columnMenuGroupingItem: { displayOrder: 27 },
};
-export const GridPremiumColumnMenu = React.forwardRef(
+export const GridPremiumColumnMenu = forwardRef(
function GridPremiumColumnMenuSimple(props, ref) {
return (
);
},
diff --git a/packages/x-data-grid-premium/src/components/promptControl/GridToolbarPromptControl.tsx b/packages/x-data-grid-premium/src/components/promptControl/GridToolbarPromptControl.tsx
index 7f08363cb4141..e9302ca121187 100644
--- a/packages/x-data-grid-premium/src/components/promptControl/GridToolbarPromptControl.tsx
+++ b/packages/x-data-grid-premium/src/components/promptControl/GridToolbarPromptControl.tsx
@@ -46,7 +46,7 @@ const GridToolbarPromptControlRoot = styled('div', {
flexDirection: 'row',
});
-function sampleData(apiRef: React.MutableRefObject) {
+function sampleData(apiRef: React.RefObject) {
const columns = gridColumnDefinitionsSelector(apiRef);
const rows = Object.values(gridRowsLookupSelector(apiRef));
const columnExamples: Record = {};
@@ -65,7 +65,7 @@ function sampleData(apiRef: React.MutableRefObject) {
}
function generateContext(
- apiRef: React.MutableRefObject,
+ apiRef: React.RefObject,
examples?: Record,
) {
const columns = gridColumnDefinitionsSelector(apiRef);
diff --git a/packages/x-data-grid-premium/src/hooks/features/aggregation/createAggregationLookup.ts b/packages/x-data-grid-premium/src/hooks/features/aggregation/createAggregationLookup.ts
index 0e91f89ecee6a..26fb252e56750 100644
--- a/packages/x-data-grid-premium/src/hooks/features/aggregation/createAggregationLookup.ts
+++ b/packages/x-data-grid-premium/src/hooks/features/aggregation/createAggregationLookup.ts
@@ -8,10 +8,11 @@ import {
gridRowTreeSelector,
GRID_ROOT_GROUP_ID,
} from '@mui/x-data-grid-pro';
-import { GridApiPremium } from '../../../models/gridApiPremium';
+import { GridPrivateApiPremium } from '../../../models/gridApiPremium';
import { DataGridPremiumProcessedProps } from '../../../models/dataGridPremiumProps';
import {
GridAggregationFunction,
+ GridAggregationFunctionDataSource,
GridAggregationLookup,
GridAggregationPosition,
GridAggregationRules,
@@ -19,23 +20,20 @@ import {
import { getAggregationRules } from './gridAggregationUtils';
import { gridAggregationModelSelector } from './gridAggregationSelectors';
-const getAggregationCellValue = ({
- apiRef,
- groupId,
- field,
- aggregationFunction,
- aggregationRowsScope,
-}: {
- apiRef: React.MutableRefObject;
- groupId: GridRowId;
- field: string;
- aggregationFunction: GridAggregationFunction;
- aggregationRowsScope: DataGridPremiumProcessedProps['aggregationRowsScope'];
-}) => {
+const getGroupAggregatedValue = (
+ groupId: GridRowId,
+ apiRef: React.RefObject,
+ aggregationRowsScope: DataGridPremiumProcessedProps['aggregationRowsScope'],
+ aggregatedFields: string[],
+ aggregationRules: GridAggregationRules,
+ position: GridAggregationPosition,
+) => {
+ const groupAggregationLookup: GridAggregationLookup[GridRowId] = {};
+ const aggregatedValues: { aggregatedField: string; values: any[] }[] = [];
+
+ const rowIds = apiRef.current.getRowGroupChildren({ groupId });
const filteredRowsLookup = gridFilteredRowsLookupSelector(apiRef);
- const rowIds: GridRowId[] = apiRef.current.getRowGroupChildren({ groupId });
- const values: any[] = [];
rowIds.forEach((rowId) => {
if (aggregationRowsScope === 'filtered' && filteredRowsLookup[rowId] === false) {
return;
@@ -53,51 +51,65 @@ const getAggregationCellValue = ({
return;
}
- if (typeof aggregationFunction.getCellValue === 'function') {
- const row = apiRef.current.getRow(rowId);
- values.push(aggregationFunction.getCellValue({ row }));
- } else {
- values.push(apiRef.current.getCellValue(rowId, field));
+ const row = apiRef.current.getRow(rowId);
+
+ for (let j = 0; j < aggregatedFields.length; j += 1) {
+ const aggregatedField = aggregatedFields[j];
+ const columnAggregationRules = aggregationRules[aggregatedField];
+
+ const aggregationFunction =
+ columnAggregationRules.aggregationFunction as GridAggregationFunction;
+ const field = aggregatedField;
+
+ if (aggregatedValues[j] === undefined) {
+ aggregatedValues[j] = {
+ aggregatedField,
+ values: [],
+ };
+ }
+
+ if (typeof aggregationFunction.getCellValue === 'function') {
+ aggregatedValues[j].values.push(aggregationFunction.getCellValue({ row }));
+ } else {
+ const colDef = apiRef.current.getColumn(field);
+ aggregatedValues[j].values.push(apiRef.current.getRowValue(row, colDef));
+ }
}
});
- return aggregationFunction.apply({
- values,
- groupId,
- field, // Added per user request in https://github.com/mui/mui-x/issues/6995#issuecomment-1327423455
- });
+ for (let i = 0; i < aggregatedValues.length; i += 1) {
+ const { aggregatedField, values } = aggregatedValues[i];
+ const aggregationFunction = aggregationRules[aggregatedField]
+ .aggregationFunction as GridAggregationFunction;
+ const value = aggregationFunction.apply({
+ values,
+ groupId,
+ field: aggregatedField, // Added per user request in https://github.com/mui/mui-x/issues/6995#issuecomment-1327423455
+ });
+
+ groupAggregationLookup[aggregatedField] = {
+ position,
+ value,
+ };
+ }
+
+ return groupAggregationLookup;
};
-const getGroupAggregatedValue = ({
- groupId,
- apiRef,
- aggregationRowsScope,
- aggregatedFields,
- aggregationRules,
- position,
-}: {
- groupId: GridRowId;
- apiRef: React.MutableRefObject;
- aggregationRowsScope: DataGridPremiumProcessedProps['aggregationRowsScope'];
- aggregatedFields: string[];
- aggregationRules: GridAggregationRules;
- position: GridAggregationPosition;
-}) => {
+const getGroupAggregatedValueDataSource = (
+ groupId: GridRowId,
+ apiRef: React.RefObject,
+ aggregatedFields: string[],
+ position: GridAggregationPosition,
+) => {
const groupAggregationLookup: GridAggregationLookup[GridRowId] = {};
for (let j = 0; j < aggregatedFields.length; j += 1) {
const aggregatedField = aggregatedFields[j];
- const columnAggregationRules = aggregationRules[aggregatedField];
groupAggregationLookup[aggregatedField] = {
position,
- value: getAggregationCellValue({
- apiRef,
- groupId,
- field: aggregatedField,
- aggregationFunction: columnAggregationRules.aggregationFunction,
- aggregationRowsScope,
- }),
+ value: apiRef.current.resolveGroupAggregation(groupId, aggregatedField),
};
}
@@ -109,17 +121,22 @@ export const createAggregationLookup = ({
aggregationFunctions,
aggregationRowsScope,
getAggregationPosition,
+ isDataSource,
}: {
- apiRef: React.MutableRefObject;
- aggregationFunctions: Record;
+ apiRef: React.RefObject;
+ aggregationFunctions:
+ | Record
+ | Record;
aggregationRowsScope: DataGridPremiumProcessedProps['aggregationRowsScope'];
getAggregationPosition: DataGridPremiumProcessedProps['getAggregationPosition'];
+ isDataSource: boolean;
}): GridAggregationLookup => {
- const aggregationRules = getAggregationRules({
- columnsLookup: gridColumnLookupSelector(apiRef),
- aggregationModel: gridAggregationModelSelector(apiRef),
+ const aggregationRules = getAggregationRules(
+ gridColumnLookupSelector(apiRef),
+ gridAggregationModelSelector(apiRef),
aggregationFunctions,
- });
+ isDataSource,
+ );
const aggregatedFields = Object.keys(aggregationRules);
if (aggregatedFields.length === 0) {
@@ -139,18 +156,25 @@ export const createAggregationLookup = ({
}
}
- const hasAggregableChildren = groupNode.children.length;
- if (hasAggregableChildren) {
- const position = getAggregationPosition(groupNode);
- if (position != null) {
- aggregationLookup[groupNode.id] = getGroupAggregatedValue({
- groupId: groupNode.id,
+ const position = getAggregationPosition(groupNode);
+
+ if (position !== null) {
+ if (isDataSource) {
+ aggregationLookup[groupNode.id] = getGroupAggregatedValueDataSource(
+ groupNode.id,
apiRef,
aggregatedFields,
+ position,
+ );
+ } else if (groupNode.children.length) {
+ aggregationLookup[groupNode.id] = getGroupAggregatedValue(
+ groupNode.id,
+ apiRef,
aggregationRowsScope,
+ aggregatedFields,
aggregationRules,
position,
- });
+ );
}
}
};
diff --git a/packages/x-data-grid-premium/src/hooks/features/aggregation/gridAggregationInterfaces.ts b/packages/x-data-grid-premium/src/hooks/features/aggregation/gridAggregationInterfaces.ts
index 212bdef1a0083..96e584d233037 100644
--- a/packages/x-data-grid-premium/src/hooks/features/aggregation/gridAggregationInterfaces.ts
+++ b/packages/x-data-grid-premium/src/hooks/features/aggregation/gridAggregationInterfaces.ts
@@ -22,6 +22,13 @@ export interface GridAggregationApi {
setAggregationModel: (model: GridAggregationModel) => void;
}
+export interface GridAggregationPrivateApi {
+ /**
+ * Applies the aggregation to the rows.
+ */
+ applyAggregation: () => void;
+}
+
export interface GridAggregationGetCellValueParams {
/**
* The row model of the row that the current cell belongs to.
@@ -44,7 +51,7 @@ export interface GridAggregationFunction {
apply: (params: GridAggregationParams) => AV | null | undefined;
/**
* Label of the aggregation function.
- * Will be used to add a label on the footer of the grouping column when this aggregation function is the only one being used.
+ * Used for adding a label to the footer of the grouping column when this aggregation function is the only one being used.
* @default apiRef.current.getLocaleText('aggregationFunctionLabel{capitalize(name)})
*/
label?: string;
@@ -54,12 +61,12 @@ export interface GridAggregationFunction {
*/
columnTypes?: string[];
/**
- * Function that allows to apply a formatter to the aggregated value.
- * If not defined, the grid will use the formatter of the column.
+ * Function for applying a formatter to the aggregated value.
+ * If not defined, the grid uses the formatter of the column.
*/
valueFormatter?: GridValueFormatter;
/**
- * Indicates if the aggregated value have the same unit as the cells used to generate it.
+ * Indicates if the aggregated value has the same unit as the cells used to generate it.
* It can be used to apply a custom cell renderer only if the aggregated value has the same unit.
* @default true
*/
@@ -74,6 +81,14 @@ export interface GridAggregationFunction {
getCellValue?: (params: GridAggregationGetCellValueParams) => V;
}
+/**
+ * Grid aggregation function data source definition interface.
+ * @demos
+ * - [Server-side aggregation](/x/react-data-grid/server-side-data/aggregation/)
+ */
+export interface GridAggregationFunctionDataSource
+ extends Omit {}
+
export interface GridAggregationParams {
values: (V | undefined)[];
groupId: GridRowId;
@@ -115,7 +130,7 @@ export interface GridAggregationHeaderMeta {
export interface GridAggregationRule {
aggregationFunctionName: string;
- aggregationFunction: GridAggregationFunction;
+ aggregationFunction: GridAggregationFunction | GridAggregationFunctionDataSource;
}
/**
diff --git a/packages/x-data-grid-premium/src/hooks/features/aggregation/gridAggregationUtils.ts b/packages/x-data-grid-premium/src/hooks/features/aggregation/gridAggregationUtils.ts
index 0f15aa74136b4..d7389dd70aea7 100644
--- a/packages/x-data-grid-premium/src/hooks/features/aggregation/gridAggregationUtils.ts
+++ b/packages/x-data-grid-premium/src/hooks/features/aggregation/gridAggregationUtils.ts
@@ -18,6 +18,7 @@ import {
} from '@mui/x-data-grid-pro/internals';
import {
GridAggregationFunction,
+ GridAggregationFunctionDataSource,
GridAggregationModel,
GridAggregationRule,
GridAggregationRules,
@@ -36,20 +37,29 @@ export const getAggregationFooterRowIdFromGroupId = (groupId: GridRowId | null)
return `auto-generated-group-footer-${groupId}`;
};
+type AggregationFunction = GridAggregationFunction | GridAggregationFunctionDataSource | undefined;
+
+const isClientSideAggregateFunction = (
+ aggregationFunction: AggregationFunction,
+): aggregationFunction is GridAggregationFunction =>
+ !!aggregationFunction && 'apply' in aggregationFunction;
+
export const canColumnHaveAggregationFunction = ({
colDef,
aggregationFunctionName,
aggregationFunction,
+ isDataSource,
}: {
colDef: GridColDef | undefined;
aggregationFunctionName: string;
- aggregationFunction: GridAggregationFunction | undefined;
+ aggregationFunction: AggregationFunction;
+ isDataSource: boolean;
}): boolean => {
if (!colDef) {
return false;
}
- if (!aggregationFunction) {
+ if (!isClientSideAggregateFunction(aggregationFunction) && !isDataSource) {
return false;
}
@@ -57,7 +67,7 @@ export const canColumnHaveAggregationFunction = ({
return colDef.availableAggregationFunctions.includes(aggregationFunctionName);
}
- if (!aggregationFunction.columnTypes) {
+ if (!aggregationFunction?.columnTypes) {
return true;
}
@@ -67,15 +77,20 @@ export const canColumnHaveAggregationFunction = ({
export const getAvailableAggregationFunctions = ({
aggregationFunctions,
colDef,
+ isDataSource,
}: {
- aggregationFunctions: Record;
+ aggregationFunctions:
+ | Record
+ | Record;
colDef: GridColDef;
+ isDataSource: boolean;
}) =>
Object.keys(aggregationFunctions).filter((aggregationFunctionName) =>
canColumnHaveAggregationFunction({
colDef,
aggregationFunctionName,
aggregationFunction: aggregationFunctions[aggregationFunctionName],
+ isDataSource,
}),
);
@@ -86,24 +101,26 @@ export const mergeStateWithAggregationModel =
aggregation: { ...state.aggregation, model: aggregationModel },
});
-export const getAggregationRules = ({
- columnsLookup,
- aggregationModel,
- aggregationFunctions,
-}: {
- columnsLookup: GridColumnRawLookup;
- aggregationModel: GridAggregationModel;
- aggregationFunctions: Record;
-}) => {
+export const getAggregationRules = (
+ columnsLookup: GridColumnRawLookup,
+ aggregationModel: GridAggregationModel,
+ aggregationFunctions:
+ | Record
+ | Record,
+ isDataSource: boolean,
+) => {
const aggregationRules: GridAggregationRules = {};
- Object.entries(aggregationModel).forEach(([field, columnItem]) => {
+ // eslint-disable-next-line guard-for-in
+ for (const field in aggregationModel) {
+ const columnItem = aggregationModel[field];
if (
columnsLookup[field] &&
canColumnHaveAggregationFunction({
colDef: columnsLookup[field],
aggregationFunctionName: columnItem,
aggregationFunction: aggregationFunctions[columnItem],
+ isDataSource,
})
) {
aggregationRules[field] = {
@@ -111,7 +128,7 @@ export const getAggregationRules = ({
aggregationFunction: aggregationFunctions[columnItem],
};
}
- });
+ }
return aggregationRules;
};
@@ -123,7 +140,7 @@ interface AddFooterRowsParams {
* If `true`, there are some aggregation rules to apply
*/
hasAggregationRule: boolean;
- apiRef: React.MutableRefObject;
+ apiRef: React.RefObject;
}
/**
@@ -248,7 +265,7 @@ export const getAggregationFunctionLabel = ({
apiRef,
aggregationRule,
}: {
- apiRef: React.MutableRefObject;
+ apiRef: React.RefObject;
aggregationRule: GridAggregationRule;
}): string => {
if (aggregationRule.aggregationFunction.label != null) {
diff --git a/packages/x-data-grid-premium/src/hooks/features/aggregation/index.ts b/packages/x-data-grid-premium/src/hooks/features/aggregation/index.ts
index 0334e48bacb90..ee9155e14144c 100644
--- a/packages/x-data-grid-premium/src/hooks/features/aggregation/index.ts
+++ b/packages/x-data-grid-premium/src/hooks/features/aggregation/index.ts
@@ -1,4 +1,20 @@
-export * from './gridAggregationInterfaces';
+export type {
+ GridAggregationState,
+ GridAggregationInitialState,
+ GridAggregationInternalCache,
+ GridAggregationApi,
+ GridAggregationGetCellValueParams,
+ GridAggregationFunction,
+ GridAggregationFunctionDataSource,
+ GridAggregationParams,
+ GridAggregationModel,
+ GridAggregationLookup,
+ GridAggregationPosition,
+ GridAggregationCellMeta,
+ GridAggregationHeaderMeta,
+ GridAggregationRule,
+ GridAggregationRules,
+} from './gridAggregationInterfaces';
export * from './gridAggregationSelectors';
export * from './gridAggregationFunctions';
export {
diff --git a/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregation.ts b/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregation.ts
index 2a9e12601509a..9dad437736300 100644
--- a/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregation.ts
+++ b/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregation.ts
@@ -4,11 +4,15 @@ import {
useGridApiEventHandler,
useGridApiMethod,
} from '@mui/x-data-grid-pro';
-import { GridStateInitializer } from '@mui/x-data-grid-pro/internals';
+import {
+ useGridRegisterPipeProcessor,
+ GridStateInitializer,
+ GridPipeProcessor,
+} from '@mui/x-data-grid-pro/internals';
import { DataGridPremiumProcessedProps } from '../../../models/dataGridPremiumProps';
import { GridPrivateApiPremium } from '../../../models/gridApiPremium';
import { gridAggregationModelSelector } from './gridAggregationSelectors';
-import { GridAggregationApi } from './gridAggregationInterfaces';
+import { GridAggregationApi, GridAggregationPrivateApi } from './gridAggregationInterfaces';
import {
getAggregationRules,
mergeStateWithAggregationModel,
@@ -34,7 +38,7 @@ export const aggregationStateInitializer: GridStateInitializer<
};
export const useGridAggregation = (
- apiRef: React.MutableRefObject,
+ apiRef: React.RefObject,
props: Pick<
DataGridPremiumProcessedProps,
| 'onAggregationModelChange'
@@ -45,6 +49,7 @@ export const useGridAggregation = (
| 'aggregationRowsScope'
| 'disableAggregation'
| 'rowGroupingColumnMode'
+ | 'unstable_dataSource'
>,
) => {
apiRef.current.registerControlState({
@@ -75,6 +80,7 @@ export const useGridAggregation = (
getAggregationPosition: props.getAggregationPosition,
aggregationFunctions: props.aggregationFunctions,
aggregationRowsScope: props.aggregationRowsScope,
+ isDataSource: !!props.unstable_dataSource,
});
apiRef.current.setState((state) => ({
@@ -86,13 +92,31 @@ export const useGridAggregation = (
props.getAggregationPosition,
props.aggregationFunctions,
props.aggregationRowsScope,
+ props.unstable_dataSource,
]);
const aggregationApi: GridAggregationApi = {
setAggregationModel,
};
+ const aggregationPrivateApi: GridAggregationPrivateApi = {
+ applyAggregation,
+ };
+
useGridApiMethod(apiRef, aggregationApi, 'public');
+ useGridApiMethod(apiRef, aggregationPrivateApi, 'private');
+
+ const addGetRowsParams = React.useCallback>(
+ (params) => {
+ return {
+ ...params,
+ aggregationModel: gridAggregationModelSelector(apiRef),
+ };
+ },
+ [apiRef],
+ );
+
+ useGridRegisterPipeProcessor(apiRef, 'getRowsParams', addGetRowsParams);
/**
* EVENTS
@@ -103,16 +127,21 @@ export const useGridAggregation = (
const aggregationRules = props.disableAggregation
? {}
- : getAggregationRules({
- columnsLookup: gridColumnLookupSelector(apiRef),
- aggregationModel: gridAggregationModelSelector(apiRef),
- aggregationFunctions: props.aggregationFunctions,
- });
+ : getAggregationRules(
+ gridColumnLookupSelector(apiRef),
+ gridAggregationModelSelector(apiRef),
+ props.aggregationFunctions,
+ !!props.unstable_dataSource,
+ );
// Re-apply the row hydration to add / remove the aggregation footers
if (!areAggregationRulesEqual(rulesOnLastRowHydration, aggregationRules)) {
- apiRef.current.requestPipeProcessorsApplication('hydrateRows');
- applyAggregation();
+ if (props.unstable_dataSource) {
+ apiRef.current.unstable_dataSource.fetchRows();
+ } else {
+ apiRef.current.requestPipeProcessorsApplication('hydrateRows');
+ applyAggregation();
+ }
}
// Re-apply the column hydration to wrap / unwrap the aggregated columns
@@ -120,7 +149,13 @@ export const useGridAggregation = (
apiRef.current.caches.aggregation.rulesOnLastColumnHydration = aggregationRules;
apiRef.current.requestPipeProcessorsApplication('hydrateColumns');
}
- }, [apiRef, applyAggregation, props.aggregationFunctions, props.disableAggregation]);
+ }, [
+ apiRef,
+ applyAggregation,
+ props.aggregationFunctions,
+ props.disableAggregation,
+ props.unstable_dataSource,
+ ]);
useGridApiEventHandler(apiRef, 'aggregationModelChange', checkAggregationRulesDiff);
useGridApiEventHandler(apiRef, 'columnsChange', checkAggregationRulesDiff);
diff --git a/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregationPreProcessors.tsx b/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregationPreProcessors.tsx
index b0f1ab6933ae0..27a28150dbbf4 100644
--- a/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregationPreProcessors.tsx
+++ b/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregationPreProcessors.tsx
@@ -22,10 +22,15 @@ import { GridInitialStatePremium } from '../../../models/gridStatePremium';
import { GridAggregationRules } from './gridAggregationInterfaces';
export const useGridAggregationPreProcessors = (
- apiRef: React.MutableRefObject,
+ apiRef: React.RefObject,
props: Pick<
DataGridPremiumProcessedProps,
- 'aggregationFunctions' | 'disableAggregation' | 'getAggregationPosition' | 'slotProps' | 'slots'
+ | 'aggregationFunctions'
+ | 'disableAggregation'
+ | 'getAggregationPosition'
+ | 'slotProps'
+ | 'slots'
+ | 'unstable_dataSource'
>,
) => {
// apiRef.current.caches.aggregation.rulesOnLastColumnHydration is not used because by the time
@@ -36,11 +41,12 @@ export const useGridAggregationPreProcessors = (
(columnsState) => {
const aggregationRules = props.disableAggregation
? {}
- : getAggregationRules({
- columnsLookup: columnsState.lookup,
- aggregationModel: gridAggregationModelSelector(apiRef),
- aggregationFunctions: props.aggregationFunctions,
- });
+ : getAggregationRules(
+ columnsState.lookup,
+ gridAggregationModelSelector(apiRef),
+ props.aggregationFunctions,
+ !!props.unstable_dataSource,
+ );
columnsState.orderedFields.forEach((field) => {
const shouldHaveAggregationValue = !!aggregationRules[field];
@@ -69,18 +75,19 @@ export const useGridAggregationPreProcessors = (
return columnsState;
},
- [apiRef, props.aggregationFunctions, props.disableAggregation],
+ [apiRef, props.aggregationFunctions, props.disableAggregation, props.unstable_dataSource],
);
const addGroupFooterRows = React.useCallback>(
(value) => {
const aggregationRules = props.disableAggregation
? {}
- : getAggregationRules({
- columnsLookup: gridColumnLookupSelector(apiRef),
- aggregationModel: gridAggregationModelSelector(apiRef),
- aggregationFunctions: props.aggregationFunctions,
- });
+ : getAggregationRules(
+ gridColumnLookupSelector(apiRef),
+ gridAggregationModelSelector(apiRef),
+ props.aggregationFunctions,
+ !!props.unstable_dataSource,
+ );
const hasAggregationRule = Object.keys(aggregationRules).length > 0;
@@ -102,7 +109,13 @@ export const useGridAggregationPreProcessors = (
hasAggregationRule,
});
},
- [apiRef, props.disableAggregation, props.getAggregationPosition, props.aggregationFunctions],
+ [
+ apiRef,
+ props.disableAggregation,
+ props.getAggregationPosition,
+ props.aggregationFunctions,
+ props.unstable_dataSource,
+ ],
);
const addColumnMenuButtons = React.useCallback>(
@@ -114,6 +127,7 @@ export const useGridAggregationPreProcessors = (
const availableAggregationFunctions = getAvailableAggregationFunctions({
aggregationFunctions: props.aggregationFunctions,
colDef,
+ isDataSource: !!props.unstable_dataSource,
});
if (availableAggregationFunctions.length === 0) {
@@ -122,7 +136,7 @@ export const useGridAggregationPreProcessors = (
return [...columnMenuItems, 'columnMenuAggregationItem'];
},
- [props.aggregationFunctions, props.disableAggregation],
+ [props.aggregationFunctions, props.disableAggregation, props.unstable_dataSource],
);
const stateExportPreProcessing = React.useCallback>(
diff --git a/packages/x-data-grid-premium/src/hooks/features/aggregation/wrapColumnWithAggregation.tsx b/packages/x-data-grid-premium/src/hooks/features/aggregation/wrapColumnWithAggregation.tsx
index e025f256f5b4c..18c5bb6bf2936 100644
--- a/packages/x-data-grid-premium/src/hooks/features/aggregation/wrapColumnWithAggregation.tsx
+++ b/packages/x-data-grid-premium/src/hooks/features/aggregation/wrapColumnWithAggregation.tsx
@@ -31,7 +31,7 @@ interface GridColDefWithAggregationWrappers extends GridBaseColDef {
}
type ColumnPropertyWrapper = (params: {
- apiRef: React.MutableRefObject;
+ apiRef: React.RefObject;
value: GridBaseColDef[P];
colDef: GridBaseColDef;
aggregationRule: GridAggregationRule;
@@ -188,7 +188,7 @@ export const wrapColumnWithAggregationValue = ({
aggregationRule,
}: {
column: GridBaseColDef;
- apiRef: React.MutableRefObject;
+ apiRef: React.RefObject;
aggregationRule: GridAggregationRule;
}): GridBaseColDef => {
const getCellAggregationResult = (
diff --git a/packages/x-data-grid-premium/src/hooks/features/cellSelection/useGridCellSelection.ts b/packages/x-data-grid-premium/src/hooks/features/cellSelection/useGridCellSelection.ts
index 6796a0d77af96..46760ef830109 100644
--- a/packages/x-data-grid-premium/src/hooks/features/cellSelection/useGridCellSelection.ts
+++ b/packages/x-data-grid-premium/src/hooks/features/cellSelection/useGridCellSelection.ts
@@ -49,7 +49,7 @@ const AUTO_SCROLL_SENSITIVITY = 50; // The distance from the edge to start scrol
const AUTO_SCROLL_SPEED = 20; // The speed to scroll once the mouse enters the sensitivity area
export const useGridCellSelection = (
- apiRef: React.MutableRefObject,
+ apiRef: React.RefObject,
props: Pick<
DataGridPremiumProcessedProps,
| 'cellSelection'
@@ -64,10 +64,10 @@ export const useGridCellSelection = (
) => {
const hasRootReference = apiRef.current.rootElementRef.current !== null;
const visibleRows = useGridVisibleRows(apiRef, props);
- const cellWithVirtualFocus = React.useRef();
- const lastMouseDownCell = React.useRef();
- const mousePosition = React.useRef<{ x: number; y: number } | null>(null);
- const autoScrollRAF = React.useRef();
+ const cellWithVirtualFocus = React.useRef(null);
+ const lastMouseDownCell = React.useRef(null);
+ const mousePosition = React.useRef<{ x: number; y: number }>(null);
+ const autoScrollRAF = React.useRef(null);
const sortedRowIds = useGridSelector(apiRef, gridSortedRowIdsSelector);
const dimensions = useGridSelector(apiRef, gridDimensionsSelector);
const totalHeaderHeight = getTotalHeaderHeight(apiRef, props);
diff --git a/packages/x-data-grid-premium/src/hooks/features/clipboard/useGridClipboardImport.ts b/packages/x-data-grid-premium/src/hooks/features/clipboard/useGridClipboardImport.ts
index 8b20678940d7d..bf6f2648cf4a9 100644
--- a/packages/x-data-grid-premium/src/hooks/features/clipboard/useGridClipboardImport.ts
+++ b/packages/x-data-grid-premium/src/hooks/features/clipboard/useGridClipboardImport.ts
@@ -89,7 +89,7 @@ class CellValueUpdater {
updateRow: (row: GridRowModel) => void;
options: {
- apiRef: React.MutableRefObject;
+ apiRef: React.RefObject;
processRowUpdate: DataGridPremiumProcessedProps['processRowUpdate'];
onProcessRowUpdateError: DataGridPremiumProcessedProps['onProcessRowUpdateError'];
getRowId: DataGridPremiumProcessedProps['getRowId'];
@@ -212,7 +212,7 @@ function defaultPasteResolver({
paginationMode,
}: {
pastedData: string[][];
- apiRef: React.MutableRefObject;
+ apiRef: React.RefObject;
updateCell: CellValueUpdater['updateCell'];
pagination: DataGridPremiumProcessedProps['pagination'];
paginationMode: DataGridPremiumProcessedProps['paginationMode'];
@@ -321,7 +321,7 @@ function defaultPasteResolver({
}
export const useGridClipboardImport = (
- apiRef: React.MutableRefObject,
+ apiRef: React.RefObject,
props: Pick<
DataGridPremiumProcessedProps,
| 'pagination'
diff --git a/packages/x-data-grid-premium/src/hooks/features/dataSource/cache.ts b/packages/x-data-grid-premium/src/hooks/features/dataSource/cache.ts
new file mode 100644
index 0000000000000..db35e1db051d9
--- /dev/null
+++ b/packages/x-data-grid-premium/src/hooks/features/dataSource/cache.ts
@@ -0,0 +1,13 @@
+import { GridGetRowsParamsPremium } from './models';
+
+export function getKeyPremium(params: GridGetRowsParamsPremium) {
+ return JSON.stringify([
+ params.filterModel,
+ params.sortModel,
+ params.groupKeys,
+ params.groupFields,
+ params.start,
+ params.end,
+ params.aggregationModel,
+ ]);
+}
diff --git a/packages/x-data-grid-premium/src/hooks/features/dataSource/models.ts b/packages/x-data-grid-premium/src/hooks/features/dataSource/models.ts
new file mode 100644
index 0000000000000..ef69f680ad459
--- /dev/null
+++ b/packages/x-data-grid-premium/src/hooks/features/dataSource/models.ts
@@ -0,0 +1,63 @@
+import type {
+ GridColDef,
+ GridRowId,
+ GridValidRowModel,
+ GridDataSource,
+ GridGetRowsResponse,
+ GridGetRowsParams,
+ GridDataSourceApiBase,
+ GridDataSourcePrivateApi,
+} from '@mui/x-data-grid-pro';
+
+import type { GridAggregationModel } from '../aggregation/gridAggregationInterfaces';
+
+export interface GridGetRowsResponsePremium extends GridGetRowsResponse {
+ /**
+ * Row to be used for aggregation footer row.
+ * It must provide the values for the aggregated columns passed in
+ * `GridGetRowsParams.aggregationModel`.
+ */
+ aggregateRow?: GridValidRowModel;
+}
+
+export interface GridGetRowsParamsPremium extends GridGetRowsParams {
+ aggregationModel?: GridAggregationModel;
+}
+
+export interface GridDataSourcePremium extends Omit {
+ /**
+ * This method will be called when the grid needs to fetch some rows.
+ * @param {GridGetRowsParamsPremium} params The parameters required to fetch the rows.
+ * @returns {Promise} A promise that resolves to the data of type [GridGetRowsResponsePremium].
+ */
+ getRows(params: GridGetRowsParamsPremium): Promise;
+ /**
+ * Used to get the aggregated value for a parent row.
+ * @param {GridValidRowModel} row The row to extract the aggregated value from.
+ * @param {GridColDef['field']} field The field to extract the aggregated value for.
+ * @returns {string} The aggregated value for a specific aggregated column.
+ */
+ getAggregatedValue?: (row: GridValidRowModel, field: GridColDef['field']) => string;
+}
+
+export interface GridDataSourceApiBasePremium extends Omit {
+ /**
+ * Fetches the rows from the server.
+ * If no `parentId` option is provided, it fetches the root rows.
+ * Any missing parameter from `params` will be filled from the state (sorting, filtering, etc.).
+ * @param {GridRowId} parentId The id of the parent node (default: `GRID_ROOT_GROUP_ID`).
+ * @param {Partial} params Request parameters override.
+ */
+ fetchRows: (parentId?: GridRowId, params?: Partial) => void;
+}
+
+export interface GridDataSourceApiPremium {
+ /**
+ * The data source API.
+ */
+ unstable_dataSource: GridDataSourceApiBasePremium;
+}
+
+export interface GridDataSourcePremiumPrivateApi extends GridDataSourcePrivateApi {
+ resolveGroupAggregation: (groupId: GridRowId, field: string) => any;
+}
diff --git a/packages/x-data-grid-premium/src/hooks/features/dataSource/useGridDataSourcePremium.tsx b/packages/x-data-grid-premium/src/hooks/features/dataSource/useGridDataSourcePremium.tsx
new file mode 100644
index 0000000000000..36c58e2c8df90
--- /dev/null
+++ b/packages/x-data-grid-premium/src/hooks/features/dataSource/useGridDataSourcePremium.tsx
@@ -0,0 +1,102 @@
+import * as React from 'react';
+import {
+ useGridApiEventHandler as addEventHandler,
+ useGridApiMethod,
+ GridEventLookup,
+ GRID_ROOT_GROUP_ID,
+ GridValidRowModel,
+} from '@mui/x-data-grid-pro';
+import {
+ useGridDataSourceBase,
+ useGridRegisterStrategyProcessor,
+ GridPipeProcessor,
+ useGridRegisterPipeProcessor,
+} from '@mui/x-data-grid-pro/internals';
+import { GridPrivateApiPremium } from '../../../models/gridApiPremium';
+import { DataGridPremiumProcessedProps } from '../../../models/dataGridPremiumProps';
+import {
+ GridDataSourcePremiumPrivateApi,
+ GridGetRowsParamsPremium,
+ GridGetRowsResponsePremium,
+} from './models';
+import { getKeyPremium } from './cache';
+
+const options = {
+ cacheOptions: {
+ getKey: getKeyPremium,
+ },
+};
+
+export const useGridDataSourcePremium = (
+ apiRef: React.RefObject,
+ props: DataGridPremiumProcessedProps,
+) => {
+ const { api, strategyProcessor, events } = useGridDataSourceBase(
+ apiRef,
+ props,
+ options,
+ );
+ const aggregateRowRef = React.useRef({});
+
+ const processDataSourceRows = React.useCallback>(
+ (
+ {
+ params,
+ response,
+ }: {
+ params: GridGetRowsParamsPremium;
+ response: GridGetRowsResponsePremium;
+ },
+ applyRowHydration: boolean,
+ ) => {
+ if (response.aggregateRow) {
+ aggregateRowRef.current = response.aggregateRow;
+ }
+ if (Object.keys(params.aggregationModel || {}).length > 0) {
+ if (applyRowHydration) {
+ apiRef.current.requestPipeProcessorsApplication('hydrateRows');
+ }
+ apiRef.current.applyAggregation();
+ }
+
+ return {
+ params,
+ response,
+ };
+ },
+ [apiRef],
+ );
+
+ const resolveGroupAggregation = React.useCallback<
+ GridDataSourcePremiumPrivateApi['resolveGroupAggregation']
+ >(
+ (groupId, field) => {
+ if (groupId === GRID_ROOT_GROUP_ID) {
+ return props.unstable_dataSource?.getAggregatedValue?.(aggregateRowRef.current, field);
+ }
+ const row = apiRef.current.getRow(groupId);
+ return props.unstable_dataSource?.getAggregatedValue?.(row, field);
+ },
+ [apiRef, props.unstable_dataSource],
+ );
+
+ const privateApi: GridDataSourcePremiumPrivateApi = {
+ ...api.private,
+ resolveGroupAggregation,
+ };
+
+ useGridApiMethod(apiRef, api.public, 'public');
+ useGridApiMethod(apiRef, privateApi, 'private');
+
+ useGridRegisterStrategyProcessor(
+ apiRef,
+ strategyProcessor.strategyName,
+ strategyProcessor.group,
+ strategyProcessor.processor,
+ );
+ useGridRegisterPipeProcessor(apiRef, 'processDataSourceRows', processDataSourceRows);
+
+ Object.entries(events).forEach(([event, handler]) => {
+ addEventHandler(apiRef, event as keyof GridEventLookup, handler);
+ });
+};
diff --git a/packages/x-data-grid-premium/src/hooks/features/export/serializer/excelSerializer.ts b/packages/x-data-grid-premium/src/hooks/features/export/serializer/excelSerializer.ts
index 61dba42a4ab75..62d0660ab63d5 100644
--- a/packages/x-data-grid-premium/src/hooks/features/export/serializer/excelSerializer.ts
+++ b/packages/x-data-grid-premium/src/hooks/features/export/serializer/excelSerializer.ts
@@ -66,7 +66,7 @@ interface SerializedRow {
export const serializeRowUnsafe = (
id: GridRowId,
columns: GridStateColDef[],
- apiRef: React.MutableRefObject,
+ apiRef: React.RefObject,
defaultValueOptionsFormulae: { [field: string]: { address: string } },
options: Pick,
): SerializedRow => {
@@ -403,7 +403,7 @@ interface BuildExcelOptions
export async function buildExcel(
options: BuildExcelOptions,
- apiRef: React.MutableRefObject,
+ apiRef: React.RefObject,
): Promise {
const {
columns,
diff --git a/packages/x-data-grid-premium/src/hooks/features/export/useGridExcelExport.tsx b/packages/x-data-grid-premium/src/hooks/features/export/useGridExcelExport.tsx
index 93286e45fc4fe..639d53f941666 100644
--- a/packages/x-data-grid-premium/src/hooks/features/export/useGridExcelExport.tsx
+++ b/packages/x-data-grid-premium/src/hooks/features/export/useGridExcelExport.tsx
@@ -36,7 +36,7 @@ import { GridExcelExportMenuItem } from '../../../components';
* @requires useGridParamsApi (method)
*/
export const useGridExcelExport = (
- apiRef: React.MutableRefObject,
+ apiRef: React.RefObject,
props: DataGridPremiumProps,
): void => {
const logger = useGridLogger(apiRef, 'useGridExcelExport');
diff --git a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/createGroupingColDef.tsx b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/createGroupingColDef.tsx
index 172821242fdeb..926668d9da258 100644
--- a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/createGroupingColDef.tsx
+++ b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/createGroupingColDef.tsx
@@ -264,7 +264,7 @@ export const createGroupingColDefForOneGroupingCriteria = ({
};
interface CreateGroupingColDefSeveralCriteriaParams {
- apiRef: React.MutableRefObject;
+ apiRef: React.RefObject;
columnsLookup: GridColumnRawLookup;
/**
* The fields from which we are grouping the rows.
diff --git a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/gridRowGroupingUtils.ts b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/gridRowGroupingUtils.ts
index 353b51936bd4a..08f01b8a59d78 100644
--- a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/gridRowGroupingUtils.ts
+++ b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/gridRowGroupingUtils.ts
@@ -54,7 +54,7 @@ interface FilterRowTreeFromTreeDataParams {
rowTree: GridRowTreeConfig;
isRowMatchingFilters: GridAggregatedFilterItemApplier | null;
filterModel: GridFilterModel;
- apiRef: React.MutableRefObject;
+ apiRef: React.RefObject;
}
/**
@@ -196,7 +196,7 @@ export const mergeStateWithRowGroupingModel =
});
export const setStrategyAvailability = (
- privateApiRef: React.MutableRefObject,
+ privateApiRef: React.RefObject,
disableRowGrouping: boolean,
dataSource?: GridDataSource,
) => {
@@ -224,7 +224,7 @@ export const getCellGroupingCriteria = ({
row: GridRowModel;
colDef: GridColDef;
groupingRule: GridGroupingRule;
- apiRef: React.MutableRefObject;
+ apiRef: React.RefObject;
}) => {
let key: GridKeyValue | null | undefined;
if (groupingRule.groupingValueGetter) {
diff --git a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridDataSourceRowGroupingPreProcessors.ts b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridDataSourceRowGroupingPreProcessors.ts
index 07ad4690d0594..0a088c6549b4b 100644
--- a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridDataSourceRowGroupingPreProcessors.ts
+++ b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridDataSourceRowGroupingPreProcessors.ts
@@ -16,7 +16,7 @@ import { GridPrivateApiPremium } from '../../../models/gridApiPremium';
import { gridRowGroupingSanitizedModelSelector } from './gridRowGroupingSelector';
export const useGridDataSourceRowGroupingPreProcessors = (
- apiRef: React.MutableRefObject,
+ apiRef: React.RefObject,
props: Pick<
DataGridPremiumProcessedProps,
| 'disableRowGrouping'
diff --git a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGrouping.tsx b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGrouping.tsx
index b8b6779f14016..2dcbed058bb09 100644
--- a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGrouping.tsx
+++ b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGrouping.tsx
@@ -51,7 +51,7 @@ export const rowGroupingStateInitializer: GridStateInitializer<
* @requires useGridParamsApi (method) - can be after, async only
*/
export const useGridRowGrouping = (
- apiRef: React.MutableRefObject,
+ apiRef: React.RefObject,
props: Pick<
DataGridPremiumProcessedProps,
| 'initialState'
diff --git a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGroupingPreProcessors.ts b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGroupingPreProcessors.ts
index 435ac1ca1a7eb..6d7efbb141039 100644
--- a/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGroupingPreProcessors.ts
+++ b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridRowGroupingPreProcessors.ts
@@ -40,7 +40,7 @@ import {
import { GridPrivateApiPremium } from '../../../models/gridApiPremium';
export const useGridRowGroupingPreProcessors = (
- apiRef: React.MutableRefObject,
+ apiRef: React.RefObject,
props: Pick<
DataGridPremiumProcessedProps,
| 'disableRowGrouping'
diff --git a/packages/x-data-grid-premium/src/hooks/utils/useKeepGroupedColumnsHidden.ts b/packages/x-data-grid-premium/src/hooks/utils/useKeepGroupedColumnsHidden.ts
index f0781bba037fd..3c8514b8f1cbb 100644
--- a/packages/x-data-grid-premium/src/hooks/utils/useKeepGroupedColumnsHidden.ts
+++ b/packages/x-data-grid-premium/src/hooks/utils/useKeepGroupedColumnsHidden.ts
@@ -36,7 +36,7 @@ const updateColumnVisibilityModel = (
*/
export const useKeepGroupedColumnsHidden = (
props: {
- apiRef: React.MutableRefObject;
+ apiRef: React.RefObject;
} & Pick,
) => {
const initialProps = React.useRef(props);
diff --git a/packages/x-data-grid-premium/src/index.ts b/packages/x-data-grid-premium/src/index.ts
index 0def4c628a755..36842cea3ac1a 100644
--- a/packages/x-data-grid-premium/src/index.ts
+++ b/packages/x-data-grid-premium/src/index.ts
@@ -40,3 +40,14 @@ export {
GRID_COLUMN_MENU_SLOTS,
GRID_COLUMN_MENU_SLOT_PROPS,
} from './components/reexports';
+
+export type { GridDataSourceCache } from '@mui/x-data-grid-pro';
+
+export type {
+ GridGetRowsParamsPremium as GridGetRowsParams,
+ GridGetRowsResponsePremium as GridGetRowsResponse,
+ GridDataSourcePremium as GridDataSource,
+ GridDataSourceApiPremium as GridDataSourceApi,
+ GridDataSourceApiBasePremium as GridDataSourceApiBase,
+ GridDataSourcePremiumPrivateApi as GridDataSourcePrivateApi,
+} from './hooks/features/dataSource/models';
diff --git a/packages/x-data-grid-premium/src/models/dataGridPremiumProps.ts b/packages/x-data-grid-premium/src/models/dataGridPremiumProps.ts
index 8ed5498364dab..f67b06907710a 100644
--- a/packages/x-data-grid-premium/src/models/dataGridPremiumProps.ts
+++ b/packages/x-data-grid-premium/src/models/dataGridPremiumProps.ts
@@ -17,12 +17,14 @@ import type { GridRowGroupingModel } from '../hooks/features/rowGrouping';
import type {
GridAggregationModel,
GridAggregationFunction,
+ GridAggregationFunctionDataSource,
GridAggregationPosition,
} from '../hooks/features/aggregation';
import { GridPremiumSlotsComponent } from './gridPremiumSlotsComponent';
import { GridInitialStatePremium } from './gridStatePremium';
import { GridApiPremium } from './gridApiPremium';
import { GridCellSelectionModel } from '../hooks/features/cellSelection';
+import { GridDataSourcePremium as GridDataSource } from '../hooks/features/dataSource/models';
export interface GridExperimentalPremiumFeatures extends GridExperimentalProFeatures {}
@@ -86,9 +88,11 @@ export interface DataGridPremiumPropsWithDefaultValue;
+ aggregationFunctions:
+ | Record
+ | Record;
/**
* Rows used to generate the aggregated value.
* If `filtered`, the aggregated values are generated using only the rows currently passing the filtering process.
@@ -118,11 +122,14 @@ export interface DataGridPremiumPropsWithDefaultValue
- extends Omit, 'initialState' | 'apiRef'> {
+ extends Omit<
+ DataGridProPropsWithoutDefaultValue,
+ 'initialState' | 'apiRef' | 'unstable_dataSource'
+ > {
/**
* The ref object that allows grid manipulation. Can be instantiated with `useGridApiRef()`.
*/
- apiRef?: React.MutableRefObject;
+ apiRef?: React.RefObject;
/**
* The initial state of the DataGridPremium.
* The data in it is set in the state on initialization but isn't controlled.
@@ -188,4 +195,5 @@ export interface DataGridPremiumPropsWithoutDefaultValue;
+ unstable_dataSource?: GridDataSource;
}
diff --git a/packages/x-data-grid-premium/src/models/gridApiPremium.ts b/packages/x-data-grid-premium/src/models/gridApiPremium.ts
index 98ec11cdd1dce..1ea6dd05158ce 100644
--- a/packages/x-data-grid-premium/src/models/gridApiPremium.ts
+++ b/packages/x-data-grid-premium/src/models/gridApiPremium.ts
@@ -8,13 +8,16 @@ import {
GridRowMultiSelectionApi,
GridColumnReorderApi,
GridRowProApi,
- GridDataSourceApi,
- GridDataSourcePrivateApi,
} from '@mui/x-data-grid-pro';
import { GridInitialStatePremium, GridStatePremium } from './gridStatePremium';
import type { GridRowGroupingApi, GridExcelExportApi, GridAggregationApi } from '../hooks';
import { GridCellSelectionApi } from '../hooks/features/cellSelection/gridCellSelectionInterfaces';
import type { DataGridPremiumProcessedProps } from './dataGridPremiumProps';
+import type {
+ GridDataSourcePremiumPrivateApi,
+ GridDataSourceApiPremium,
+} from '../hooks/features/dataSource/models';
+import type { GridAggregationPrivateApi } from '../hooks/features/aggregation/gridAggregationInterfaces';
/**
* The api of Data Grid Premium.
@@ -29,7 +32,7 @@ export interface GridApiPremium
GridExcelExportApi,
GridAggregationApi,
GridRowPinningApi,
- GridDataSourceApi,
+ GridDataSourceApiPremium,
GridCellSelectionApi,
// APIs that are private in Community plan, but public in Pro and Premium plans
GridRowMultiSelectionApi,
@@ -38,5 +41,6 @@ export interface GridApiPremium
export interface GridPrivateApiPremium
extends GridApiPremium,
GridPrivateOnlyApiCommon,
- GridDataSourcePrivateApi,
+ GridDataSourcePremiumPrivateApi,
+ GridAggregationPrivateApi,
GridDetailPanelPrivateApi {}
diff --git a/packages/x-data-grid-premium/src/models/gridGroupingValueGetter.ts b/packages/x-data-grid-premium/src/models/gridGroupingValueGetter.ts
index aaa20b7f73a13..bd23ac1de2ef4 100644
--- a/packages/x-data-grid-premium/src/models/gridGroupingValueGetter.ts
+++ b/packages/x-data-grid-premium/src/models/gridGroupingValueGetter.ts
@@ -8,5 +8,5 @@ export type GridGroupingValueGetter<
value: TValue,
row: R,
column: GridColDef,
- apiRef: React.MutableRefObject,
+ apiRef: React.RefObject,
) => GridKeyValue | null | undefined;
diff --git a/packages/x-data-grid-premium/src/models/gridPastedValueParser.ts b/packages/x-data-grid-premium/src/models/gridPastedValueParser.ts
index e20efa8a0c530..34d5f5c03655e 100644
--- a/packages/x-data-grid-premium/src/models/gridPastedValueParser.ts
+++ b/packages/x-data-grid-premium/src/models/gridPastedValueParser.ts
@@ -9,5 +9,5 @@ export type GridPastedValueParser<
value: string,
row: R,
column: GridColDef,
- apiRef: React.MutableRefObject,
+ apiRef: React.RefObject,
) => V | undefined;
diff --git a/packages/x-data-grid-premium/src/tests/DataGridPremium.test.tsx b/packages/x-data-grid-premium/src/tests/DataGridPremium.test.tsx
index 70df5ae9c3d59..3678e4c1ca3d7 100644
--- a/packages/x-data-grid-premium/src/tests/DataGridPremium.test.tsx
+++ b/packages/x-data-grid-premium/src/tests/DataGridPremium.test.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { createRenderer, act } from '@mui/internal-test-utils';
+import { createRenderer, act, waitFor } from '@mui/internal-test-utils';
import { expect } from 'chai';
import {
DataGridPremium as DataGrid,
@@ -35,7 +35,7 @@ describe(' - Quick filter', () => {
columns: [{ field: 'brand' }],
};
- let apiRef: React.MutableRefObject;
+ let apiRef: React.RefObject;
function TestCase(props: Partial) {
apiRef = useGridApiRef();
@@ -63,7 +63,7 @@ describe(' - Quick filter', () => {
}
// https://github.com/mui/mui-x/issues/9677
- it('should not fail when adding a grouping criterion', () => {
+ it('should not fail when adding a grouping criterion', async () => {
const { setProps } = render(
- Quick filter', () => {
/>,
);
- act(() => apiRef.current.addRowGroupingCriteria('year'));
+ await act(() => apiRef.current.addRowGroupingCriteria('year'));
setProps({
filterModel: {
@@ -103,6 +103,6 @@ describe(' - Quick filter', () => {
},
});
- expect(getColumnValues(0)).to.deep.equal(['20th Century Fox (1)', '']);
+ await waitFor(() => expect(getColumnValues(0)).to.deep.equal(['20th Century Fox (1)', '']));
});
});
diff --git a/packages/x-data-grid-premium/src/tests/aggregation.DataGridPremium.test.tsx b/packages/x-data-grid-premium/src/tests/aggregation.DataGridPremium.test.tsx
index a397d549e0377..3384b2c47a916 100644
--- a/packages/x-data-grid-premium/src/tests/aggregation.DataGridPremium.test.tsx
+++ b/packages/x-data-grid-premium/src/tests/aggregation.DataGridPremium.test.tsx
@@ -46,7 +46,7 @@ const baselineProps: DataGridPremiumProps = {
describe(' - Aggregation', () => {
const { render, clock } = createRenderer({ clock: 'fake' });
- let apiRef: React.MutableRefObject;
+ let apiRef: React.RefObject;
function Test(props: Partial) {
apiRef = useGridApiRef();
diff --git a/packages/x-data-grid-premium/src/tests/cellSelection.DataGridPremium.test.tsx b/packages/x-data-grid-premium/src/tests/cellSelection.DataGridPremium.test.tsx
index 1918719266a19..4949c71649394 100644
--- a/packages/x-data-grid-premium/src/tests/cellSelection.DataGridPremium.test.tsx
+++ b/packages/x-data-grid-premium/src/tests/cellSelection.DataGridPremium.test.tsx
@@ -12,11 +12,12 @@ import {
} from '@mui/x-data-grid-premium';
import { getBasicGridData } from '@mui/x-data-grid-generator';
import { fireUserEvent } from 'test/utils/fireUserEvent';
+import { isJSDOM, describeSkipIf } from 'test/utils/skipIf';
describe(' - Cell selection', () => {
const { render } = createRenderer();
- let apiRef: React.MutableRefObject;
+ let apiRef: React.RefObject;
function TestDataGridSelection({
rowLength = 4,
@@ -88,14 +89,16 @@ describe(' - Cell selection', () => {
});
describe('Ctrl + click', () => {
- it('should add the clicked cells to the selection', () => {
- render( );
+ it('should add the clicked cells to the selection', async () => {
+ const { user } = render( );
expect(document.querySelector('.Mui-selected')).to.equal(null);
const cell11 = getCell(1, 1);
- fireEvent.click(cell11);
+ await user.click(cell11);
expect(cell11).to.have.class('Mui-selected');
const cell21 = getCell(2, 1);
- fireEvent.click(cell21, { ctrlKey: true });
+ await user.keyboard('{Control>}');
+ await user.click(cell21);
+ await user.keyboard('{/Control}');
expect(cell21).to.have.class('Mui-selected');
expect(cell11).to.have.class('Mui-selected');
});
@@ -187,14 +190,15 @@ describe(' - Cell selection', () => {
expect(spiedSelectCellsBetweenRange.lastCall.args[1]).to.deep.equal({ id: 1, field: 'id' });
});
- it('should call selectCellRange when ArrowUp is pressed', () => {
- render( );
+ it('should call selectCellRange when ArrowUp is pressed', async () => {
+ const { user } = render( );
const spiedSelectCellsBetweenRange = spyApi(apiRef.current, 'selectCellRange');
const cell = getCell(1, 0);
- cell.focus();
- fireUserEvent.mousePress(cell);
- fireEvent.keyDown(cell, { key: 'Shift' });
- fireEvent.keyDown(cell, { key: 'ArrowUp', shiftKey: true });
+ await act(() => {
+ cell.focus();
+ });
+ await user.click(cell);
+ await user.keyboard('{Shift>}{ArrowUp}{/Shift}');
expect(spiedSelectCellsBetweenRange.lastCall.args[0]).to.deep.equal({ id: 1, field: 'id' });
expect(spiedSelectCellsBetweenRange.lastCall.args[1]).to.deep.equal({ id: 0, field: 'id' });
});
@@ -298,9 +302,9 @@ describe(' - Cell selection', () => {
expect(getCell(2, 2)).to.have.class('Mui-selected');
});
- it('should select all cells within the given arguments if start > end', () => {
+ it('should select all cells within the given arguments if start > end', async () => {
render( );
- act(() =>
+ await act(() =>
apiRef.current.selectCellRange({ id: 0, field: 'id' }, { id: 2, field: 'price1M' }),
);
@@ -361,14 +365,7 @@ describe(' - Cell selection', () => {
});
});
- describe('Auto-scroll', () => {
- before(function beforeHook() {
- if (/jsdom/.test(window.navigator.userAgent)) {
- // Need layouting
- this.skip();
- }
- });
-
+ describeSkipIf(isJSDOM)('Auto-scroll', () => {
it('should auto-scroll when the mouse approaches the bottom edge', () => {
stub(window, 'requestAnimationFrame').callsFake(() => 0);
diff --git a/packages/x-data-grid-premium/src/tests/clipboard.DataGridPremium.test.tsx b/packages/x-data-grid-premium/src/tests/clipboard.DataGridPremium.test.tsx
index 3b4c67048c675..40d6ba2736c8b 100644
--- a/packages/x-data-grid-premium/src/tests/clipboard.DataGridPremium.test.tsx
+++ b/packages/x-data-grid-premium/src/tests/clipboard.DataGridPremium.test.tsx
@@ -12,11 +12,12 @@ import { SinonSpy, spy, stub, SinonStub } from 'sinon';
import { getCell, getColumnValues, sleep } from 'test/utils/helperFn';
import { fireUserEvent } from 'test/utils/fireUserEvent';
import { getBasicGridData } from '@mui/x-data-grid-generator';
+import { isJSDOM, describeSkipIf } from 'test/utils/skipIf';
describe(' - Clipboard', () => {
const { render } = createRenderer();
- let apiRef: React.MutableRefObject;
+ let apiRef: React.RefObject;
function Test({
rowLength = 4,
@@ -168,14 +169,8 @@ describe(' - Clipboard', () => {
});
});
- describe('paste', () => {
- before(function beforeHook() {
- if (/jsdom/.test(window.navigator.userAgent)) {
- // These test are flaky in JSDOM
- this.skip();
- }
- });
-
+ // These test are flaky in JSDOM
+ describeSkipIf(isJSDOM)('paste', () => {
function paste(cell: HTMLElement, pasteText: string) {
const pasteEvent = new Event('paste');
diff --git a/packages/x-data-grid-premium/src/tests/columns.DataGridPremium.test.tsx b/packages/x-data-grid-premium/src/tests/columns.DataGridPremium.test.tsx
index 979136dc5e7e4..b380acbe3ce27 100644
--- a/packages/x-data-grid-premium/src/tests/columns.DataGridPremium.test.tsx
+++ b/packages/x-data-grid-premium/src/tests/columns.DataGridPremium.test.tsx
@@ -3,20 +3,15 @@ import { act, createRenderer, fireEvent } from '@mui/internal-test-utils';
import { expect } from 'chai';
import { DataGridPremium, gridClasses } from '@mui/x-data-grid-premium';
import { getCell, getColumnHeaderCell } from 'test/utils/helperFn';
-
-const isJSDOM = /jsdom/.test(window.navigator.userAgent);
+import { testSkipIf, isJSDOM } from 'test/utils/skipIf';
describe(' - Columns', () => {
const { render } = createRenderer();
describe('resizing', () => {
// https://github.com/mui/mui-x/issues/10078
- it('should properly resize aggregated column', function test() {
- if (isJSDOM) {
- // Need layouting
- this.skip();
- }
-
+ // Needs layout
+ testSkipIf(isJSDOM)('should properly resize aggregated column', () => {
render(
- Data source aggregation', () => {
+ const { render } = createRenderer();
+
+ let apiRef: React.RefObject
;
+ let getRowsSpy: SinonSpy;
+ let mockServer: ReturnType;
+
+ function TestDataSourceAggregation(
+ props: Partial & {
+ getAggregatedValue?: GridDataSource['getAggregatedValue'];
+ },
+ ) {
+ apiRef = useGridApiRef();
+ const { getAggregatedValue: getAggregatedValueProp, ...rest } = props;
+ mockServer = useMockServer(
+ { rowLength: 10, maxColumns: 1 },
+ { useCursorPagination: false, minDelay: 0, maxDelay: 0, verbose: false },
+ );
+
+ const { fetchRows } = mockServer;
+
+ const dataSource: GridDataSource = React.useMemo(
+ () => ({
+ getRows: async (params: GridGetRowsParams) => {
+ const urlParams = new URLSearchParams({
+ filterModel: JSON.stringify(params.filterModel),
+ sortModel: JSON.stringify(params.sortModel),
+ paginationModel: JSON.stringify(params.paginationModel),
+ aggregationModel: JSON.stringify(params.aggregationModel),
+ });
+
+ const getRowsResponse = await fetchRows(
+ `https://mui.com/x/api/data-grid?${urlParams.toString()}`,
+ );
+
+ return {
+ rows: getRowsResponse.rows,
+ rowCount: getRowsResponse.rowCount,
+ aggregateRow: getRowsResponse.aggregateRow,
+ };
+ },
+ getAggregatedValue:
+ getAggregatedValueProp ??
+ ((row, field) => {
+ return row[`${field}Aggregate`];
+ }),
+ }),
+ [fetchRows, getAggregatedValueProp],
+ );
+
+ getRowsSpy?.restore();
+ getRowsSpy = spy(dataSource, 'getRows');
+
+ const baselineProps = {
+ unstable_dataSource: dataSource,
+ columns: mockServer.columns,
+ disableVirtualization: true,
+ aggregationFunctions: {
+ sum: { columnTypes: ['number'] },
+ avg: { columnTypes: ['number'] },
+ min: { columnTypes: ['number', 'date', 'dateTime'] },
+ max: { columnTypes: ['number', 'date', 'dateTime'] },
+ size: {},
+ },
+ };
+
+ return (
+
+
+
+ );
+ }
+
+ beforeEach(function beforeTest() {
+ if (isJSDOM) {
+ this.skip(); // Needs layout
+ }
+ });
+
+ it('should show aggregation option in the column menu', async () => {
+ const { user } = render( );
+ await user.click(within(getColumnHeaderCell(0)).getByLabelText('Menu'));
+ expect(screen.queryByLabelText('Aggregation')).not.to.equal(null);
+ });
+
+ it('should not show aggregation option in the column menu when no aggregation function is defined', async () => {
+ const { user } = render( );
+ await user.click(within(getColumnHeaderCell(0)).getByLabelText('Menu'));
+ expect(screen.queryByLabelText('Aggregation')).to.equal(null);
+ });
+
+ it('should provide the `aggregationModel` in the `getRows` params', async () => {
+ render(
+ ,
+ );
+ await waitFor(() => {
+ expect(getRowsSpy.callCount).to.be.greaterThan(0);
+ });
+ expect(getRowsSpy.args[0][0].aggregationModel).to.deep.equal({ id: 'size' });
+ });
+
+ it('should show the aggregation footer row when aggregation is enabled', async () => {
+ render(
+ ,
+ );
+ await waitFor(() => {
+ expect(Object.keys(apiRef.current.state.aggregation.lookup).length).to.be.greaterThan(0);
+ });
+ expect(apiRef.current.state.rows.tree[GRID_AGGREGATION_ROOT_FOOTER_ROW_ID]).not.to.equal(null);
+ const footerRow = apiRef.current.state.aggregation.lookup[GRID_ROOT_GROUP_ID];
+ expect(footerRow.id).to.deep.equal({ position: 'footer', value: 10 });
+ });
+
+ it('should derive the aggregation values using `dataSource.getAggregatedValue`', async () => {
+ render(
+ 'Agg value'}
+ />,
+ );
+ await waitFor(() => {
+ expect(Object.keys(apiRef.current.state.aggregation.lookup).length).to.be.greaterThan(0);
+ });
+ expect(apiRef.current.state.aggregation.lookup[GRID_ROOT_GROUP_ID].id.value).to.equal(
+ 'Agg value',
+ );
+ });
+});
diff --git a/packages/x-data-grid-premium/src/tests/exportExcel.DataGridPremium.test.tsx b/packages/x-data-grid-premium/src/tests/exportExcel.DataGridPremium.test.tsx
index b7e6c72fe40f0..28c072060e207 100644
--- a/packages/x-data-grid-premium/src/tests/exportExcel.DataGridPremium.test.tsx
+++ b/packages/x-data-grid-premium/src/tests/exportExcel.DataGridPremium.test.tsx
@@ -19,7 +19,7 @@ const isJSDOM = /jsdom/.test(window.navigator.userAgent);
describe(' - Export Excel', () => {
const { render } = createRenderer();
- let apiRef: React.MutableRefObject;
+ let apiRef: React.RefObject;
const columns: GridColDef[] = [{ field: 'id' }, { field: 'brand', headerName: 'Brand' }];
const rows = [
@@ -221,7 +221,10 @@ describe(' - Export Excel', () => {
}
render( );
- const workbook = await apiRef.current.getDataAsExcel();
+ let workbook: Excel.Workbook | null = null;
+ await act(async () => {
+ workbook = await apiRef.current.getDataAsExcel();
+ });
const worksheet = workbook!.worksheets[0];
// 1-based index + 1 for column header row
diff --git a/packages/x-data-grid-premium/src/tests/license.DataGridPremium.test.tsx b/packages/x-data-grid-premium/src/tests/license.DataGridPremium.test.tsx
index d3e861a8e3535..9a230483cda99 100644
--- a/packages/x-data-grid-premium/src/tests/license.DataGridPremium.test.tsx
+++ b/packages/x-data-grid-premium/src/tests/license.DataGridPremium.test.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import addYears from 'date-fns/addYears';
+import { addYears } from 'date-fns/addYears';
import { expect } from 'chai';
import { createRenderer, screen, waitFor } from '@mui/internal-test-utils';
import { DataGridPremium } from '@mui/x-data-grid-premium';
diff --git a/packages/x-data-grid-premium/src/tests/rowGrouping.DataGridPremium.test.tsx b/packages/x-data-grid-premium/src/tests/rowGrouping.DataGridPremium.test.tsx
index 6a0979cff342c..c25c327b78456 100644
--- a/packages/x-data-grid-premium/src/tests/rowGrouping.DataGridPremium.test.tsx
+++ b/packages/x-data-grid-premium/src/tests/rowGrouping.DataGridPremium.test.tsx
@@ -78,7 +78,7 @@ const baselineProps: BaselineProps = {
describe(' - Row grouping', () => {
const { render, clock } = createRenderer();
- let apiRef: React.MutableRefObject;
+ let apiRef: React.RefObject;
function Test(props: Partial) {
apiRef = useGridApiRef();
diff --git a/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx b/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx
index da9e7161db238..65513481901c3 100644
--- a/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx
+++ b/packages/x-data-grid-premium/src/tests/rowSelection.DataGridPremium.test.tsx
@@ -47,7 +47,7 @@ describe(' - Row selection', () => {
const { render } = createRenderer();
describe('props: rowSelectionPropagation = { descendants: true, parents: true }', () => {
- let apiRef: React.MutableRefObject;
+ let apiRef: React.RefObject;
function Test(props: Partial) {
apiRef = useGridApiRef();
@@ -120,19 +120,19 @@ describe(' - Row selection', () => {
]);
});
- it('should deselect auto selected parent if one of the children is deselected', () => {
- render( );
+ it('should deselect auto selected parent if one of the children is deselected', async () => {
+ const { user } = render( );
- fireEvent.click(getCell(1, 0).querySelector('input')!);
- fireEvent.click(getCell(2, 0).querySelector('input')!);
- fireEvent.click(getCell(3, 0).querySelector('input')!);
+ await user.click(getCell(1, 0).querySelector('input')!);
+ await user.click(getCell(2, 0).querySelector('input')!);
+ await user.click(getCell(3, 0).querySelector('input')!);
expect(apiRef.current.getSelectedRows()).to.have.keys([
0,
1,
2,
'auto-generated-row-category1/Cat A',
]);
- fireEvent.click(getCell(2, 0).querySelector('input')!);
+ await user.click(getCell(2, 0).querySelector('input')!);
expect(apiRef.current.getSelectedRows()).to.have.keys([0, 2]);
});
diff --git a/packages/x-data-grid-premium/src/tests/rowSpanning.DataGridPremium.test.tsx b/packages/x-data-grid-premium/src/tests/rowSpanning.DataGridPremium.test.tsx
index 61d9685b72c17..f69405f42ddfc 100644
--- a/packages/x-data-grid-premium/src/tests/rowSpanning.DataGridPremium.test.tsx
+++ b/packages/x-data-grid-premium/src/tests/rowSpanning.DataGridPremium.test.tsx
@@ -2,8 +2,7 @@ import * as React from 'react';
import { createRenderer } from '@mui/internal-test-utils';
import { expect } from 'chai';
import { DataGridPremium, DataGridPremiumProps } from '@mui/x-data-grid-premium';
-
-const isJSDOM = /jsdom/.test(window.navigator.userAgent);
+import { testSkipIf, isJSDOM } from 'test/utils/skipIf';
describe(' - Row spanning', () => {
const { render } = createRenderer();
@@ -109,10 +108,7 @@ describe(' - Row spanning', () => {
}
// See https://github.com/mui/mui-x/issues/14691
- it('should not throw when initializing an aggregation model', function test() {
- if (isJSDOM) {
- this.skip();
- }
+ testSkipIf(isJSDOM)('should not throw when initializing an aggregation model', () => {
expect(() =>
render(
- State persistence', () => {
const { render } = createRenderer();
- let apiRef: React.MutableRefObject;
+ let apiRef: React.RefObject;
function TestCase(props: Omit) {
apiRef = useGridApiRef();
@@ -90,28 +90,32 @@ describe(' - State persistence', () => {
});
});
- it('should export the current version of the exportable state', () => {
+ it('should export the current version of the exportable state', async () => {
render( );
- act(() => apiRef.current.setRowGroupingModel(['category']));
- act(() =>
+ await act(() => {
+ apiRef.current.setRowGroupingModel(['category']);
+ });
+ await act(() => {
apiRef.current.setAggregationModel({
id: 'size',
- }),
- );
+ });
+ });
const exportedState = apiRef.current.exportState();
expect(exportedState.rowGrouping).to.deep.equal(FULL_INITIAL_STATE.rowGrouping);
expect(exportedState.aggregation).to.deep.equal(FULL_INITIAL_STATE.aggregation);
});
- it('should export the current version of the exportable state when using exportOnlyDirtyModels', () => {
+ it('should export the current version of the exportable state when using exportOnlyDirtyModels', async () => {
render( );
- act(() => apiRef.current.setRowGroupingModel(['category']));
- act(() =>
+ await act(() => {
+ apiRef.current.setRowGroupingModel(['category']);
+ });
+ await act(() => {
apiRef.current.setAggregationModel({
id: 'size',
- }),
- );
+ });
+ });
const exportedState = apiRef.current.exportState({ exportOnlyDirtyModels: true });
expect(exportedState.rowGrouping).to.deep.equal(FULL_INITIAL_STATE.rowGrouping);
diff --git a/packages/x-data-grid-pro/package.json b/packages/x-data-grid-pro/package.json
index 0b9b9de947644..842b6eebd3b04 100644
--- a/packages/x-data-grid-pro/package.json
+++ b/packages/x-data-grid-pro/package.json
@@ -1,6 +1,6 @@
{
"name": "@mui/x-data-grid-pro",
- "version": "8.0.0-alpha.5",
+ "version": "8.0.0-alpha.7",
"description": "The Pro plan edition of the Data Grid components (MUI X).",
"author": "MUI Team",
"main": "src/index.ts",
@@ -70,9 +70,9 @@
}
},
"devDependencies": {
- "@mui/internal-test-utils": "^1.0.23",
- "@mui/material": "^5.16.11",
- "@mui/system": "^5.16.8",
+ "@mui/internal-test-utils": "^1.0.26",
+ "@mui/material": "^5.16.14",
+ "@mui/system": "^5.16.14",
"@types/prop-types": "^15.7.14",
"rimraf": "^6.0.1"
},
diff --git a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx
index 495f6b9a1db71..206136b43bc68 100644
--- a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx
+++ b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import { useLicenseVerifier, Watermark } from '@mui/x-license';
import { GridRoot, GridContextProvider, GridValidRowModel } from '@mui/x-data-grid';
import { validateProps } from '@mui/x-data-grid/internals';
+import { forwardRef } from '@mui/x-internals/forwardRef';
import { useDataGridProComponent } from './useDataGridProComponent';
import { DataGridProProps } from '../models/dataGridProProps';
import { useDataGridProProps } from './useDataGridProProps';
@@ -22,7 +23,7 @@ const configuration = {
};
const releaseInfo = getReleaseInfo();
-const DataGridProRaw = React.forwardRef(function DataGridPro(
+const DataGridProRaw = forwardRef(function DataGridPro(
inProps: DataGridProProps,
ref: React.Ref,
) {
@@ -39,8 +40,8 @@ const DataGridProRaw = React.forwardRef(function DataGridPro
diff --git a/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx b/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx
index c2adbe7761bf5..ac954d606fa22 100644
--- a/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx
+++ b/packages/x-data-grid-pro/src/DataGridPro/useDataGridProComponent.tsx
@@ -82,14 +82,12 @@ import {
rowPinningStateInitializer,
} from '../hooks/features/rowPinning/useGridRowPinning';
import { useGridRowPinningPreProcessors } from '../hooks/features/rowPinning/useGridRowPinningPreProcessors';
-import {
- useGridDataSource,
- dataSourceStateInitializer,
-} from '../hooks/features/dataSource/useGridDataSource';
+import { useGridDataSourcePro as useGridDataSource } from '../hooks/features/dataSource/useGridDataSourcePro';
+import { dataSourceStateInitializer } from '../hooks/features/dataSource/useGridDataSourceBase';
import { useGridDataSourceLazyLoader } from '../hooks/features/serverSideLazyLoader/useGridDataSourceLazyLoader';
export const useDataGridProComponent = (
- inputApiRef: React.MutableRefObject | undefined,
+ inputApiRef: React.RefObject | undefined,
props: DataGridProProcessedProps,
) => {
const apiRef = useGridInitialization(inputApiRef, props);
diff --git a/packages/x-data-grid-pro/src/components/GridColumnHeaders.tsx b/packages/x-data-grid-pro/src/components/GridColumnHeaders.tsx
index ea0327ece85e1..c057691a60adb 100644
--- a/packages/x-data-grid-pro/src/components/GridColumnHeaders.tsx
+++ b/packages/x-data-grid-pro/src/components/GridColumnHeaders.tsx
@@ -1,19 +1,20 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { styled } from '@mui/material/styles';
-import { GridBaseColumnHeaders, UseGridColumnHeadersProps } from '@mui/x-data-grid/internals';
+import { forwardRef } from '@mui/x-internals/forwardRef';
+import { vars, GridBaseColumnHeaders, UseGridColumnHeadersProps } from '@mui/x-data-grid/internals';
import { useGridColumnHeaders } from '../hooks/features/columnHeaders/useGridColumnHeaders';
const Filler = styled('div')({
flex: 1,
- backgroundColor: 'var(--DataGrid-containerBackground)',
+ backgroundColor: vars.colors.background.base,
});
export interface GridColumnHeadersProps
extends React.HTMLAttributes,
UseGridColumnHeadersProps {}
-const GridColumnHeaders = React.forwardRef(
+const GridColumnHeaders = forwardRef(
function GridColumnHeaders(props, ref) {
const {
style,
@@ -50,7 +51,7 @@ const GridColumnHeaders = React.forwardRef
+
{getColumnGroupHeadersRows()}
{getColumnHeadersRow()}
{getColumnFiltersRow()}
diff --git a/packages/x-data-grid-pro/src/components/GridDataSourceTreeDataGroupingCell.tsx b/packages/x-data-grid-pro/src/components/GridDataSourceTreeDataGroupingCell.tsx
index 483958e66591d..40359742c5601 100644
--- a/packages/x-data-grid-pro/src/components/GridDataSourceTreeDataGroupingCell.tsx
+++ b/packages/x-data-grid-pro/src/components/GridDataSourceTreeDataGroupingCell.tsx
@@ -7,7 +7,6 @@ import {
GridDataSourceGroupNode,
useGridSelector,
} from '@mui/x-data-grid';
-import CircularProgress from '@mui/material/CircularProgress';
import { useGridRootProps } from '../hooks/utils/useGridRootProps';
import { useGridPrivateApiContext } from '../hooks/utils/useGridPrivateApiContext';
import { DataGridProProcessedProps } from '../models/dataGridProProps';
@@ -48,7 +47,7 @@ interface GridTreeDataGroupingCellIconProps
}
function GridTreeDataGroupingCellIcon(props: GridTreeDataGroupingCellIconProps) {
- const apiRef = useGridPrivateApiContext() as React.MutableRefObject;
+ const apiRef = useGridPrivateApiContext() as React.RefObject;
const rootProps = useGridRootProps();
const classes = useUtilityClasses(rootProps);
const { rowNode, id, field, descendantCount } = props;
@@ -74,7 +73,7 @@ function GridTreeDataGroupingCellIcon(props: GridTreeDataGroupingCellIconProps)
if (isDataLoading) {
return (
-
+
);
}
diff --git a/packages/x-data-grid-pro/src/components/GridDetailPanel.tsx b/packages/x-data-grid-pro/src/components/GridDetailPanel.tsx
index a37b3e4bc8071..bc401f963fc54 100644
--- a/packages/x-data-grid-pro/src/components/GridDetailPanel.tsx
+++ b/packages/x-data-grid-pro/src/components/GridDetailPanel.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import { styled } from '@mui/material/styles';
import { GridRowId } from '@mui/x-data-grid';
+import { vars } from '@mui/x-data-grid/internals';
import { useResizeObserver } from '@mui/x-internals/useResizeObserver';
import { useGridRootProps } from '../hooks/utils/useGridRootProps';
import { useGridPrivateApiContext } from '../hooks/utils/useGridPrivateApiContext';
@@ -12,12 +13,12 @@ const DetailPanel = styled('div', {
name: 'MuiDataGrid',
slot: 'DetailPanel',
overridesResolver: (props, styles) => styles.detailPanel,
-})<{ ownerState: OwnerState }>(({ theme }) => ({
+})<{ ownerState: OwnerState }>({
width:
'calc(var(--DataGrid-rowWidth) - var(--DataGrid-hasScrollY) * var(--DataGrid-scrollbarSize))',
- backgroundColor: (theme.vars || theme).palette.background.default,
+ backgroundColor: vars.colors.background.base,
overflow: 'auto',
-}));
+});
interface GridDetailPanelProps
extends Pick, 'className' | 'children'> {
diff --git a/packages/x-data-grid-pro/src/components/GridProColumnMenu.tsx b/packages/x-data-grid-pro/src/components/GridProColumnMenu.tsx
index c35f78686ea3c..3b065c0fd5749 100644
--- a/packages/x-data-grid-pro/src/components/GridProColumnMenu.tsx
+++ b/packages/x-data-grid-pro/src/components/GridProColumnMenu.tsx
@@ -5,6 +5,7 @@ import {
GRID_COLUMN_MENU_SLOTS,
GRID_COLUMN_MENU_SLOT_PROPS,
} from '@mui/x-data-grid';
+import { forwardRef } from '@mui/x-internals/forwardRef';
import { GridColumnMenuPinningItem } from './GridColumnMenuPinningItem';
export const GRID_COLUMN_MENU_SLOTS_PRO = {
@@ -19,14 +20,14 @@ export const GRID_COLUMN_MENU_SLOT_PROPS_PRO = {
},
};
-export const GridProColumnMenu = React.forwardRef(
+export const GridProColumnMenu = forwardRef(
function GridProColumnMenu(props, ref) {
return (
);
},
diff --git a/packages/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterCell.tsx b/packages/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterCell.tsx
index 0c28cfed8e91e..9e1867fcb84ee 100644
--- a/packages/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterCell.tsx
+++ b/packages/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterCell.tsx
@@ -34,6 +34,7 @@ import {
attachPinnedStyle,
} from '@mui/x-data-grid/internals';
import { useRtl } from '@mui/system/RtlProvider';
+import { forwardRef } from '@mui/x-internals/forwardRef';
import { useGridRootProps } from '../../hooks/utils/useGridRootProps';
import { DataGridProProcessedProps } from '../../models/dataGridProProps';
import { GridHeaderFilterMenuContainer } from './GridHeaderFilterMenuContainer';
@@ -51,7 +52,7 @@ export interface GridHeaderFilterCellProps extends Pick;
+ headerFilterMenuRef: React.RefObject;
item: GridFilterItem;
showClearIcon?: boolean;
InputComponentProps: GridFilterOperator['InputComponentProps'];
@@ -89,7 +90,7 @@ const useUtilityClasses = (ownerState: OwnerState) => {
return composeClasses(slots, getDataGridUtilityClass, classes);
};
-const dateSx = {
+const emptyFieldSx = {
[`& input[value=""]:not(:focus)`]: { color: 'transparent' },
};
const defaultInputComponents: { [key in GridColType]: React.JSXElementConstructor | null } = {
@@ -102,299 +103,290 @@ const defaultInputComponents: { [key in GridColType]: React.JSXElementConstructo
actions: null,
custom: null,
};
-const GridHeaderFilterCell = React.forwardRef(
- (props, ref) => {
- const {
- colIndex,
- height,
- hasFocus,
- width,
- headerClassName,
- colDef,
- item,
- headerFilterMenuRef,
- InputComponentProps,
- showClearIcon = true,
- pinnedPosition,
- pinnedOffset,
- style: styleProp,
- showLeftBorder,
- showRightBorder,
- ...other
- } = props;
-
- const apiRef = useGridPrivateApiContext();
- const isRtl = useRtl();
- const columnFields = useGridSelector(apiRef, gridVisibleColumnFieldsSelector);
- const rootProps = useGridRootProps();
- const cellRef = React.useRef(null);
- const handleRef = useForkRef(ref, cellRef);
- const inputRef = React.useRef(null);
- const buttonRef = React.useRef(null);
-
- const editingField = useGridSelector(apiRef, gridHeaderFilteringEditFieldSelector);
- const isEditing = editingField === colDef.field;
-
- const menuOpenField = useGridSelector(apiRef, gridHeaderFilteringMenuSelector);
- const isMenuOpen = menuOpenField === colDef.field;
-
- // TODO: Support for `isAnyOf` operator
- const filterOperators = React.useMemo(() => {
- if (!colDef.filterOperators) {
- return [];
+const GridHeaderFilterCell = forwardRef((props, ref) => {
+ const {
+ colIndex,
+ height,
+ hasFocus,
+ width,
+ headerClassName,
+ colDef,
+ item,
+ headerFilterMenuRef,
+ InputComponentProps,
+ showClearIcon = true,
+ pinnedPosition,
+ pinnedOffset,
+ style: styleProp,
+ showLeftBorder,
+ showRightBorder,
+ ...other
+ } = props;
+
+ const apiRef = useGridPrivateApiContext();
+ const isRtl = useRtl();
+ const columnFields = useGridSelector(apiRef, gridVisibleColumnFieldsSelector);
+ const rootProps = useGridRootProps();
+ const cellRef = React.useRef(null);
+ const handleRef = useForkRef(ref, cellRef);
+ const inputRef = React.useRef(null);
+ const buttonRef = React.useRef(null);
+
+ const editingField = useGridSelector(apiRef, gridHeaderFilteringEditFieldSelector);
+ const isEditing = editingField === colDef.field;
+
+ const menuOpenField = useGridSelector(apiRef, gridHeaderFilteringMenuSelector);
+ const isMenuOpen = menuOpenField === colDef.field;
+
+ // TODO: Support for `isAnyOf` operator
+ const filterOperators = React.useMemo(() => {
+ if (!colDef.filterOperators) {
+ return [];
+ }
+ return colDef.filterOperators.filter((operator) => operator.value !== 'isAnyOf');
+ }, [colDef.filterOperators]);
+ const filterModel = useGridSelector(apiRef, gridFilterModelSelector);
+ const filterableColumnsLookup = useGridSelector(apiRef, gridFilterableColumnLookupSelector);
+
+ const isFilterReadOnly = React.useMemo(() => {
+ if (!filterModel?.items.length) {
+ return false;
+ }
+ const filterModelItem = filterModel.items.find((it) => it.field === colDef.field);
+ return filterModelItem ? !filterableColumnsLookup[filterModelItem.field] : false;
+ }, [colDef.field, filterModel, filterableColumnsLookup]);
+
+ const currentOperator = React.useMemo(
+ () =>
+ filterOperators.find((operator) => operator.value === item.operator) ?? filterOperators![0],
+ [item.operator, filterOperators],
+ );
+
+ const InputComponent =
+ colDef.filterable || isFilterReadOnly
+ ? (currentOperator.InputComponent ?? defaultInputComponents[colDef.type as GridColType])
+ : null;
+
+ const clearFilterItem = React.useCallback(() => {
+ apiRef.current.deleteFilterItem(item);
+ }, [apiRef, item]);
+
+ let headerFilterComponent: React.ReactNode;
+ if (colDef.renderHeaderFilter) {
+ headerFilterComponent = colDef.renderHeaderFilter({ ...props, inputRef });
+ }
+
+ React.useLayoutEffect(() => {
+ if (hasFocus && !isMenuOpen) {
+ let focusableElement = cellRef.current!.querySelector('[tabindex="0"]');
+ if (isEditing && InputComponent) {
+ focusableElement = inputRef.current;
}
- return colDef.filterOperators.filter((operator) => operator.value !== 'isAnyOf');
- }, [colDef.filterOperators]);
- const filterModel = useGridSelector(apiRef, gridFilterModelSelector);
- const filterableColumnsLookup = useGridSelector(apiRef, gridFilterableColumnLookupSelector);
-
- const isFilterReadOnly = React.useMemo(() => {
- if (!filterModel?.items.length) {
- return false;
+ const elementToFocus = focusableElement || cellRef.current;
+ elementToFocus?.focus();
+ if (apiRef.current.columnHeadersContainerRef.current) {
+ apiRef.current.columnHeadersContainerRef.current.scrollLeft = 0;
}
- const filterModelItem = filterModel.items.find((it) => it.field === colDef.field);
- return filterModelItem ? !filterableColumnsLookup[filterModelItem.field] : false;
- }, [colDef.field, filterModel, filterableColumnsLookup]);
-
- const currentOperator = React.useMemo(
- () =>
- filterOperators.find((operator) => operator.value === item.operator) ?? filterOperators![0],
- [item.operator, filterOperators],
- );
-
- const InputComponent =
- colDef.filterable || isFilterReadOnly
- ? (currentOperator.InputComponent ?? defaultInputComponents[colDef.type as GridColType])
- : null;
-
- const applyFilterChanges = React.useCallback(
- (updatedItem: GridFilterItem) => {
- if (item.value && updatedItem.value === undefined) {
- apiRef.current.deleteFilterItem(updatedItem);
- return;
- }
- apiRef.current.upsertFilterItem(updatedItem);
- },
- [apiRef, item],
- );
-
- const clearFilterItem = React.useCallback(() => {
- apiRef.current.deleteFilterItem(item);
- }, [apiRef, item]);
-
- let headerFilterComponent: React.ReactNode;
- if (colDef.renderHeaderFilter) {
- headerFilterComponent = colDef.renderHeaderFilter({ ...props, inputRef });
}
+ }, [InputComponent, apiRef, hasFocus, isEditing, isMenuOpen]);
- React.useLayoutEffect(() => {
- if (hasFocus && !isMenuOpen) {
- let focusableElement = cellRef.current!.querySelector('[tabindex="0"]');
- if (isEditing && InputComponent) {
- focusableElement = inputRef.current;
- }
- const elementToFocus = focusableElement || cellRef.current;
- elementToFocus?.focus();
- if (apiRef.current.columnHeadersContainerRef.current) {
- apiRef.current.columnHeadersContainerRef.current.scrollLeft = 0;
- }
+ const onKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (isMenuOpen || isNavigationKey(event.key) || isFilterReadOnly) {
+ return;
}
- }, [InputComponent, apiRef, hasFocus, isEditing, isMenuOpen]);
-
- const onKeyDown = React.useCallback(
- (event: React.KeyboardEvent) => {
- if (isMenuOpen || isNavigationKey(event.key) || isFilterReadOnly) {
- return;
- }
- switch (event.key) {
- case 'Escape':
- if (isEditing) {
+ switch (event.key) {
+ case 'Escape':
+ if (isEditing) {
+ apiRef.current.stopHeaderFilterEditMode();
+ }
+ break;
+ case 'Enter':
+ if (isEditing) {
+ if (!event.defaultPrevented) {
apiRef.current.stopHeaderFilterEditMode();
- }
- break;
- case 'Enter':
- if (isEditing) {
- if (!event.defaultPrevented) {
- apiRef.current.stopHeaderFilterEditMode();
- break;
- }
- }
- if (event.metaKey || event.ctrlKey) {
- headerFilterMenuRef.current = buttonRef.current;
- apiRef.current.showHeaderFilterMenu(colDef.field);
break;
}
- apiRef.current.startHeaderFilterEditMode(colDef.field);
- break;
- case 'Tab': {
- if (isEditing) {
- const fieldToFocus = columnFields[colIndex + (event.shiftKey ? -1 : 1)] ?? null;
-
- if (fieldToFocus) {
- apiRef.current.startHeaderFilterEditMode(fieldToFocus);
- apiRef.current.setColumnHeaderFilterFocus(fieldToFocus, event);
- }
- }
+ }
+ if (event.metaKey || event.ctrlKey) {
+ headerFilterMenuRef.current = buttonRef.current;
+ apiRef.current.showHeaderFilterMenu(colDef.field);
break;
}
- default:
- if (isEditing || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) {
- break;
+ apiRef.current.startHeaderFilterEditMode(colDef.field);
+ break;
+ case 'Tab': {
+ if (isEditing) {
+ const fieldToFocus = columnFields[colIndex + (event.shiftKey ? -1 : 1)] ?? null;
+
+ if (fieldToFocus) {
+ apiRef.current.startHeaderFilterEditMode(fieldToFocus);
+ apiRef.current.setColumnHeaderFilterFocus(fieldToFocus, event);
}
- apiRef.current.startHeaderFilterEditMode(colDef.field);
+ }
+ break;
+ }
+ default:
+ if (isEditing || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) {
break;
+ }
+ apiRef.current.startHeaderFilterEditMode(colDef.field);
+ break;
+ }
+ },
+ [
+ apiRef,
+ colDef.field,
+ colIndex,
+ columnFields,
+ headerFilterMenuRef,
+ isEditing,
+ isFilterReadOnly,
+ isMenuOpen,
+ ],
+ );
+
+ const publish = React.useCallback(
+ (eventName: keyof GridHeaderFilterEventLookup, propHandler?: React.EventHandler) =>
+ (event: React.SyntheticEvent) => {
+ apiRef.current.publishEvent(
+ eventName,
+ apiRef.current.getColumnHeaderParams(colDef.field),
+ event as any,
+ );
+
+ if (propHandler) {
+ propHandler(event);
}
},
- [
- apiRef,
- colDef.field,
- colIndex,
- columnFields,
- headerFilterMenuRef,
- isEditing,
- isFilterReadOnly,
- isMenuOpen,
- ],
- );
+ [apiRef, colDef.field],
+ );
+
+ const onMouseDown = React.useCallback(
+ (event: React.MouseEvent) => {
+ if (!hasFocus) {
+ if (inputRef.current?.contains?.(event.target as HTMLElement)) {
+ inputRef.current.focus();
+ }
+ apiRef.current.setColumnHeaderFilterFocus(colDef.field, event);
+ }
+ },
+ [apiRef, colDef.field, hasFocus],
+ );
+
+ const mouseEventsHandlers = React.useMemo(
+ () => ({
+ onKeyDown: publish('headerFilterKeyDown', onKeyDown),
+ onClick: publish('headerFilterClick'),
+ onMouseDown: publish('headerFilterMouseDown', onMouseDown),
+ onBlur: publish('headerFilterBlur'),
+ }),
+ [onMouseDown, onKeyDown, publish],
+ );
+
+ const ownerState: OwnerState = {
+ ...rootProps,
+ pinnedPosition,
+ colDef,
+ showLeftBorder,
+ showRightBorder,
+ };
- const publish = React.useCallback(
- (eventName: keyof GridHeaderFilterEventLookup, propHandler?: React.EventHandler) =>
- (event: React.SyntheticEvent) => {
- apiRef.current.publishEvent(
- eventName,
- apiRef.current.getColumnHeaderParams(colDef.field),
- event as any,
- );
-
- if (propHandler) {
- propHandler(event);
- }
- },
- [apiRef, colDef.field],
- );
+ const classes = useUtilityClasses(ownerState as OwnerState);
- const onMouseDown = React.useCallback(
- (event: React.MouseEvent) => {
- if (!hasFocus) {
- if (inputRef.current?.contains?.(event.target as HTMLElement)) {
- inputRef.current.focus();
- }
- apiRef.current.setColumnHeaderFilterFocus(colDef.field, event);
- }
- },
- [apiRef, colDef.field, hasFocus],
- );
+ const isNoInputOperator = currentOperator.requiresFilterValue === false;
+
+ const isApplied = item?.value !== undefined || isNoInputOperator;
- const mouseEventsHandlers = React.useMemo(
- () => ({
- onKeyDown: publish('headerFilterKeyDown', onKeyDown),
- onClick: publish('headerFilterClick'),
- onMouseDown: publish('headerFilterMouseDown', onMouseDown),
- onBlur: publish('headerFilterBlur'),
- }),
- [onMouseDown, onKeyDown, publish],
+ const label =
+ currentOperator.headerLabel ??
+ apiRef.current.getLocaleText(
+ `headerFilterOperator${capitalize(item.operator)}` as 'headerFilterOperatorContains',
);
- const ownerState: OwnerState = {
- ...rootProps,
- pinnedPosition,
- colDef,
- showLeftBorder,
- showRightBorder,
- };
-
- const classes = useUtilityClasses(ownerState as OwnerState);
-
- const isNoInputOperator = currentOperator.requiresFilterValue === false;
-
- const isApplied = item?.value !== undefined || isNoInputOperator;
-
- const label =
- currentOperator.headerLabel ??
- apiRef.current.getLocaleText(
- `headerFilterOperator${capitalize(item.operator)}` as 'headerFilterOperatorContains',
- );
-
- const isFilterActive = isApplied || hasFocus;
-
- const style = {
- height,
- width,
- ...styleProp,
- };
-
- const pinnedSide = rtlFlipSide(pinnedPosition, isRtl);
- attachPinnedStyle(style, pinnedSide, pinnedOffset);
-
- return (
-
- {headerFilterComponent}
- {InputComponent && headerFilterComponent === undefined ? (
-
- apiRef.current.startHeaderFilterEditMode(colDef.field)}
- onBlur={(event: React.FocusEvent) => {
- apiRef.current.stopHeaderFilterEditMode();
- // Blurring an input element should reset focus state only if `relatedTarget` is not the header filter cell
- if (!event.relatedTarget?.className.includes('columnHeader')) {
- apiRef.current.setState((state) => ({
- ...state,
- focus: {
- cell: null,
- columnHeader: null,
- columnHeaderFilter: null,
- columnGroupHeader: null,
- },
- }));
- }
- }}
- label={capitalize(label)}
- placeholder=""
- isFilterActive={isFilterActive}
- clearButton={
- showClearIcon && isApplied ? (
-
- ) : null
+ const isFilterActive = isApplied || hasFocus;
+
+ const style = {
+ height,
+ width,
+ ...styleProp,
+ };
+
+ const pinnedSide = rtlFlipSide(pinnedPosition, isRtl);
+ attachPinnedStyle(style, pinnedSide, pinnedOffset);
+
+ return (
+
+ {headerFilterComponent}
+ {InputComponent && headerFilterComponent === undefined ? (
+
+ apiRef.current.startHeaderFilterEditMode(colDef.field)}
+ onBlur={(event: React.FocusEvent) => {
+ apiRef.current.stopHeaderFilterEditMode();
+ // Blurring an input element should reset focus state only if `relatedTarget` is not the header filter cell
+ if (!event.relatedTarget?.className.includes('columnHeader')) {
+ apiRef.current.setState((state) => ({
+ ...state,
+ focus: {
+ cell: null,
+ columnHeader: null,
+ columnHeaderFilter: null,
+ columnGroupHeader: null,
+ },
+ }));
}
- disabled={isFilterReadOnly || isNoInputOperator}
- tabIndex={-1}
- InputLabelProps={null}
- sx={colDef.type === 'date' || colDef.type === 'dateTime' ? dateSx : undefined}
- {...(isNoInputOperator ? { value: '' } : {})}
- {...currentOperator?.InputComponentProps}
- {...InputComponentProps}
- />
-
-
- ) : null}
-
- );
- },
-);
+ }}
+ label={capitalize(label)}
+ placeholder=""
+ isFilterActive={isFilterActive}
+ clearButton={
+ showClearIcon && isApplied ? (
+
+ ) : null
+ }
+ disabled={isFilterReadOnly || isNoInputOperator}
+ tabIndex={-1}
+ InputLabelProps={null}
+ sx={
+ colDef.type === 'date' || colDef.type === 'dateTime' || colDef.type === 'number'
+ ? emptyFieldSx
+ : undefined
+ }
+ {...(isNoInputOperator ? { value: '' } : {})}
+ {...currentOperator?.InputComponentProps}
+ {...InputComponentProps}
+ />
+
+
+ ) : null}
+
+ );
+});
GridHeaderFilterCell.propTypes = {
// ----------------------------- Warning --------------------------------
diff --git a/packages/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterClearButton.tsx b/packages/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterClearButton.tsx
index 1c4ad36c7c588..bb22108ee9025 100644
--- a/packages/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterClearButton.tsx
+++ b/packages/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterClearButton.tsx
@@ -1,10 +1,13 @@
import * as React from 'react';
-import { IconButtonProps } from '@mui/material/IconButton';
+import { GridSlotProps } from '@mui/x-data-grid';
import { useGridRootProps } from '../../hooks/utils/useGridRootProps';
-interface GridHeaderFilterClearIconProps extends IconButtonProps {}
+type BaseIconButtonProps = GridSlotProps['baseIconButton'];
-const sx = { padding: '2px' };
+// FIXME(v8:romgrk): Make parametric
+interface GridHeaderFilterClearIconProps extends BaseIconButtonProps {}
+
+const style = { padding: '2px' };
function GridHeaderFilterClearButton(props: GridHeaderFilterClearIconProps) {
const rootProps = useGridRootProps();
@@ -13,7 +16,7 @@ function GridHeaderFilterClearButton(props: GridHeaderFilterClearIconProps) {
tabIndex={-1}
aria-label="Clear filter"
size="small"
- sx={sx}
+ style={style}
{...props}
{...rootProps.slotProps?.baseIconButton}
>
diff --git a/packages/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterMenuContainer.tsx b/packages/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterMenuContainer.tsx
index a09f307355e91..c50440d8aac0f 100644
--- a/packages/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterMenuContainer.tsx
+++ b/packages/x-data-grid-pro/src/components/headerFiltering/GridHeaderFilterMenuContainer.tsx
@@ -11,7 +11,7 @@ import { refType, unstable_useId as useId } from '@mui/utils';
import { gridHeaderFilteringMenuSelector } from '@mui/x-data-grid/internals';
import { useGridRootProps } from '../../hooks/utils/useGridRootProps';
-const sx = {
+const style = {
width: 22,
height: 22,
margin: 'auto 0 10px 5px',
@@ -22,7 +22,7 @@ function GridHeaderFilterMenuContainer(props: {
field: GridColDef['field'];
item: GridFilterItem;
applyFilterChanges: (item: GridFilterItem) => void;
- headerFilterMenuRef: React.MutableRefObject;
+ headerFilterMenuRef: React.RefObject;
buttonRef: React.Ref;
disabled?: boolean;
}) {
@@ -69,7 +69,7 @@ function GridHeaderFilterMenuContainer(props: {
tabIndex={-1}
size="small"
onClick={handleClick}
- sx={sx}
+ style={style}
disabled={disabled}
{...rootProps.slotProps?.baseIconButton}
>
diff --git a/packages/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinning.tsx b/packages/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinning.tsx
index d64f7916d68fd..e341324360005 100644
--- a/packages/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinning.tsx
+++ b/packages/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinning.tsx
@@ -47,7 +47,7 @@ export const columnPinningStateInitializer: GridStateInitializer<
};
export const useGridColumnPinning = (
- apiRef: React.MutableRefObject,
+ apiRef: React.RefObject,
props: Pick<
DataGridProProcessedProps,
| 'disableColumnPinning'
@@ -336,10 +336,7 @@ export const useGridColumnPinning = (
}, [apiRef, props.pinnedColumns]);
};
-function setState(
- apiRef: React.MutableRefObject,
- model: GridPinnedColumnFields,
-) {
+function setState(apiRef: React.RefObject, model: GridPinnedColumnFields) {
apiRef.current.setState((state) => ({
...state,
pinnedColumns: model,
diff --git a/packages/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinningPreProcessors.ts b/packages/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinningPreProcessors.ts
index 310644dcd678f..4763340502ab5 100644
--- a/packages/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinningPreProcessors.ts
+++ b/packages/x-data-grid-pro/src/hooks/features/columnPinning/useGridColumnPinningPreProcessors.ts
@@ -11,7 +11,7 @@ import { DataGridProProcessedProps } from '../../../models/dataGridProProps';
import { GridPrivateApiPro } from '../../../models/gridApiPro';
export const useGridColumnPinningPreProcessors = (
- apiRef: React.MutableRefObject