diff --git a/.github/workflows/cherry-pick-next-to-master.yml b/.github/workflows/cherry-pick-master-to-v7.yml similarity index 88% rename from .github/workflows/cherry-pick-next-to-master.yml rename to .github/workflows/cherry-pick-master-to-v7.yml index 614c58330d3a..6f980ac23c16 100644 --- a/.github/workflows/cherry-pick-next-to-master.yml +++ b/.github/workflows/cherry-pick-master-to-v7.yml @@ -1,17 +1,17 @@ -name: Cherry pick next to master +name: Cherry pick master to v7 on: pull_request_target: branches: - - next + - master types: ['closed'] permissions: {} jobs: - cherry_pick_to_master: + cherry_pick_to_v7: runs-on: ubuntu-latest - name: Cherry pick into master + name: Cherry pick into v7 permissions: pull-requests: write contents: write @@ -26,7 +26,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} uses: carloscastrojumo/github-cherry-pick-action@503773289f4a459069c832dc628826685b75b4b3 # v1.0.10 with: - branch: master + branch: v7.x body: 'Cherry-pick of #{old_pull_request_id}' cherry-pick-branch: ${{ format('cherry-pick-{0}', github.event.number) }} title: '{old_title} (@${{ github.event.pull_request.user.login }})' diff --git a/.github/workflows/cherry-pick-master-to-v6.yml b/.github/workflows/cherry-pick-v7-to-v6.yml similarity index 91% rename from .github/workflows/cherry-pick-master-to-v6.yml rename to .github/workflows/cherry-pick-v7-to-v6.yml index 1b594e5b8200..f7e48ffe9fbe 100644 --- a/.github/workflows/cherry-pick-master-to-v6.yml +++ b/.github/workflows/cherry-pick-v7-to-v6.yml @@ -1,17 +1,17 @@ -name: Cherry pick master to v6 +name: Cherry pick v7 to v6 on: pull_request_target: branches: - - master + - v7.x types: ['closed'] permissions: {} jobs: - cherry_pick_to_v6: + cherry_pick_v7_to_v6: runs-on: ubuntu-latest - name: Cherry pick into v6 + name: Cherry pick v7 into v6 permissions: pull-requests: write contents: write diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ab1ee76e25..2dac6eb4e0c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,7 +69,7 @@ Same changes as in `@mui/x-charts@7.21.0`. #### `@mui/x-tree-view@7.21.0` -- [TreeView] Fix `alpha` usage with CSS variables (#14969) @wangkailang +- [TreeView] Fix `alpha()` usage with CSS variables (#14969) @wangkailang - [TreeView] Fix usage of the `aria-selected` attribute (#14991) @flaviendelangle - [TreeView] Fix hydration error (#15002) @flaviendelangle @@ -405,7 +405,7 @@ We'd like to offer a big thanks to the 12 contributors who made this release pos - [DataGrid] Add "does not equal" and "does not contain" filter operators (#14489) @KenanYusuf - [DataGrid] Add demo to the "Custom columns" page that does not use generator (#13695) @arminmeh -- [DataGrid] Fix Voice Over reading the column name twice (#14482) @arminmeh +- [DataGrid] Fix VoiceOver reading the column name twice (#14482) @arminmeh - [DataGrid] Fix bug in CRUD example (#14513) @michelengelen - [DataGrid] Fix failing jsdom tests caused by `:has()` selectors (#14559) @KenanYusuf - [DataGrid] Refactor string operator filter functions (#14564) @KenanYusuf @@ -1225,7 +1225,7 @@ _Jul 5, 2024_ We'd like to offer a big thanks to the 7 contributors who made this release possible. Here are some highlights ✨: - 🔄 Add loading overlay variants, including a skeleton loader option to the Data Grid component. See [Loading overlay docs](https://mui.com/x/react-data-grid/overlays/#loading-overlay) for more details. -- 🌳 Add `selectItem` and `getItemDOMElement` methods to the TreeView component public API +- 🌳 Add `selectItem()` and `getItemDOMElement()` methods to the TreeView component public API - ⛏️ Make the `usePickersTranslations` hook public in the pickers component - 🐞 Bugfixes @@ -1270,7 +1270,7 @@ Same changes as in `@mui/x-date-pickers@7.9.0`. #### `@mui/x-tree-view@7.9.0` -- [TreeView] Add `selectItem` and `getItemDOMElement` methods to the public API (#13485) @flaviendelangle +- [TreeView] Add `selectItem()` and `getItemDOMElement()` methods to the public API (#13485) @flaviendelangle ### Docs diff --git a/codecov.yml b/codecov.yml index 4a05503c5b9e..b4af65812615 100644 --- a/codecov.yml +++ b/codecov.yml @@ -10,15 +10,15 @@ coverage: adapters: target: 100% paths: - - 'packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.ts' - - 'packages/x-date-pickers/src/AdapterDateFnsV3/AdapterDateFnsV3.ts' - - 'packages/x-date-pickers/src/AdapterDateFnsJalali/AdapterDateFnsJalali.ts' - - 'packages/x-date-pickers/src/AdapterDateFnsJalaliV3/AdapterDateFnsJalaliV3.ts' - - 'packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.ts' - - 'packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts' - - 'packages/x-date-pickers/src/AdapterMoment/AdapterMoment.ts' - - 'packages/x-date-pickers/src/AdapterMomentHijri/AdapterMomentHijri.ts' - - 'packages/x-date-pickers/src/AdapterMomentJalaali/AdapterMomentJalaali.ts' + - packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.ts + - packages/x-date-pickers/src/AdapterDateFnsV3/AdapterDateFnsV3.ts + - packages/x-date-pickers/src/AdapterDateFnsJalali/AdapterDateFnsJalali.ts + - packages/x-date-pickers/src/AdapterDateFnsJalaliV3/AdapterDateFnsJalaliV3.ts + - packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.ts + - packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts + - packages/x-date-pickers/src/AdapterMoment/AdapterMoment.ts + - packages/x-date-pickers/src/AdapterMomentHijri/AdapterMomentHijri.ts + - packages/x-date-pickers/src/AdapterMomentJalaali/AdapterMomentJalaali.ts patch: off comment: false diff --git a/docs/data/charts/areas-demo/areas-demo.md b/docs/data/charts/areas-demo/areas-demo.md index 92432f02ad72..d784d0996c93 100644 --- a/docs/data/charts/areas-demo/areas-demo.md +++ b/docs/data/charts/areas-demo/areas-demo.md @@ -36,7 +36,7 @@ You can pass this gradient definition as a children of the `` and u To do so you will need to use the [``](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/linearGradient) and [``](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/stop) SVG elements. The first part is to get the SVG total height. -Which can be done with the `useDrawingArea` hook. +Which can be done with the `useDrawingArea()` hook. It's useful to define the `` as a vector that goes from the top to the bottom of our SVG container. Then to define where the gradient should switch from one color to another, you can use the `useYScale` hook to get the y coordinate of value 0. diff --git a/docs/data/charts/axis/axis.md b/docs/data/charts/axis/axis.md index e415596c2583..e8edd2836409 100644 --- a/docs/data/charts/axis/axis.md +++ b/docs/data/charts/axis/axis.md @@ -56,7 +56,7 @@ Which expects an array of value coherent with the `scaleType`: Some series types also require specific axis attributes: - line plots require an `xAxis` to have `data` provided -- bar plots require an `xAxis` with `scaleType='band'` and some `data` provided. +- bar plots require an `xAxis` with `scaleType="band"` and some `data` provided. ### Axis formatter diff --git a/docs/data/charts/bars/bars.md b/docs/data/charts/bars/bars.md index 11c42dd85e93..606f2895de13 100644 --- a/docs/data/charts/bars/bars.md +++ b/docs/data/charts/bars/bars.md @@ -100,7 +100,7 @@ Learn more about the `colorMap` properties in the [Styling docs](/x/react-charts {{"demo": "ColorScale.js"}} -### Border Radius +### Border radius To give your bar chart rounded corners, you can change the value of the `borderRadius` property on the [BarChart](/x/api/charts/bar-chart/#bar-chart-prop-slots). @@ -117,7 +117,7 @@ Or you can pass `'value'` to display the raw value of the bar. {{"demo": "BarLabel.js"}} -### Custom Labels +### Custom labels You can display, change, or hide labels based on conditional logic. To do so, provide a function to the `barLabel`. @@ -174,7 +174,7 @@ import ChartsOnAxisClickHandler from '@mui/x-charts/ChartsOnAxisClickHandler'; To skip animation at the creation and update of your chart, you can use the `skipAnimation` prop. When set to `true` it skips animation powered by `@react-spring/web`. -Charts containers already use the `useReducedMotion` from `@react-spring/web` to skip animation [according to user preferences](https://react-spring.dev/docs/utilities/use-reduced-motion#why-is-it-important). +Charts containers already use the `useReducedMotion()` from `@react-spring/web` to skip animation [according to user preferences](https://react-spring.dev/docs/utilities/use-reduced-motion#why-is-it-important). ```jsx // For a single component chart diff --git a/docs/data/charts/components/components.md b/docs/data/charts/components/components.md index 5cfff2f9f6d9..be2f67d72870 100644 --- a/docs/data/charts/components/components.md +++ b/docs/data/charts/components/components.md @@ -20,7 +20,7 @@ Charts dimensions are defined by a few props: The term **drawing area** refers to the space available to plot data (scatter points, lines, or pie arcs). The `margin` is used to leave some space for extra elements, such as the axes, the legend, or the title. -You can use the `useDrawingArea` hook in the charts subcomponents to get the coordinates of the **drawing area**. +You can use the `useDrawingArea()` hook in the charts subcomponents to get the coordinates of the **drawing area**. ```jsx import { useDrawingArea } from '@mui/x-charts'; diff --git a/docs/data/charts/gauge/gauge.md b/docs/data/charts/gauge/gauge.md index 30c5df560756..465560c8527d 100644 --- a/docs/data/charts/gauge/gauge.md +++ b/docs/data/charts/gauge/gauge.md @@ -101,7 +101,7 @@ import { ### Creating your components -To create your own components, use the `useGaugeState` hook which provides all you need about the gauge configuration: +To create your own components, use the `useGaugeState()` hook which provides all you need about the gauge configuration: - information about the value: `value`, `valueMin`, `valueMax` - information to plot the arc: `startAngle`, `endAngle`, `outerRadius`, `innerRadius`, `cornerRadius`, `cx`, and `cy` diff --git a/docs/data/charts/lines/lines.md b/docs/data/charts/lines/lines.md index be651d4dd57a..de9a55dfb928 100644 --- a/docs/data/charts/lines/lines.md +++ b/docs/data/charts/lines/lines.md @@ -235,7 +235,7 @@ sx={{ To skip animation at the creation and update of your chart, you can use the `skipAnimation` prop. When set to `true` it skips animation powered by `@react-spring/web`. -Charts containers already use the `useReducedMotion` from `@react-spring/web` to skip animation [according to user preferences](https://react-spring.dev/docs/utilities/use-reduced-motion#why-is-it-important). +Charts containers already use the `useReducedMotion()` from `@react-spring/web` to skip animation [according to user preferences](https://react-spring.dev/docs/utilities/use-reduced-motion#why-is-it-important). :::warning If you support interactive ways to add or remove series from your chart, you have to provide the series' id. diff --git a/docs/data/charts/pie/pie.md b/docs/data/charts/pie/pie.md index 047a4cc576e2..eda5601ffc78 100644 --- a/docs/data/charts/pie/pie.md +++ b/docs/data/charts/pie/pie.md @@ -110,7 +110,7 @@ const onItemClick = ( To skip animation at the creation and update of your chart you can use the `skipAnimation` prop. When set to `true` it skips animation powered by `@react-spring/web`. -Charts containers already use the `useReducedMotion` from `@react-spring/web` to skip animation [according to user preferences](https://react-spring.dev/docs/utilities/use-reduced-motion#why-is-it-important). +Charts containers already use the `useReducedMotion()` from `@react-spring/web` to skip animation [according to user preferences](https://react-spring.dev/docs/utilities/use-reduced-motion#why-is-it-important). ```jsx // For a single component chart diff --git a/docs/data/charts/tooltip/CustomAxisTooltip.js b/docs/data/charts/tooltip/CustomAxisTooltip.js index e3e36c4e65b5..c8b63c7f6cec 100644 --- a/docs/data/charts/tooltip/CustomAxisTooltip.js +++ b/docs/data/charts/tooltip/CustomAxisTooltip.js @@ -3,22 +3,80 @@ import NoSsr from '@mui/material/NoSsr'; import Popper from '@mui/material/Popper'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; -import { useAxisTooltip, useMouseTracker } from '@mui/x-charts/ChartsTooltip'; +import { useAxisTooltip } from '@mui/x-charts/ChartsTooltip'; +import { useSvgRef } from '@mui/x-charts/hooks'; import { generateVirtualElement } from './generateVirtualElement'; +function usePointer() { + const svgRef = useSvgRef(); + const popperRef = React.useRef(null); + const virtualElement = React.useRef(generateVirtualElement({ x: 0, y: 0 })); + + // Use a ref to avoid rerendering on every mousemove event. + const [pointer, setPointer] = React.useState({ + isActive: false, + isMousePointer: false, + pointerHeight: 0, + }); + + React.useEffect(() => { + const element = svgRef.current; + if (element === null) { + return () => {}; + } + + const handleOut = (event) => { + if (event.pointerType !== 'mouse') { + setPointer((prev) => ({ + ...prev, + isActive: false, + })); + } + }; + + const handleEnter = (event) => { + setPointer({ + isActive: true, + isMousePointer: event.pointerType === 'mouse', + pointerHeight: event.height, + }); + }; + + const handleMove = (event) => { + virtualElement.current = generateVirtualElement({ + x: event.clientX, + y: event.clientY, + }); + popperRef.current?.update(); + }; + + element.addEventListener('pointerenter', handleEnter); + element.addEventListener('pointerup', handleOut); + element.addEventListener('pointermove', handleMove); + + return () => { + element.removeEventListener('pointerenter', handleEnter); + element.removeEventListener('pointerup', handleOut); + element.removeEventListener('pointermove', handleMove); + }; + }, [svgRef]); + + return { ...pointer, popperRef, anchorEl: virtualElement.current }; +} + export function CustomAxisTooltip() { const tooltipData = useAxisTooltip(); - const mousePosition = useMouseTracker(); // Track the mouse position on chart. + const { isActive, isMousePointer, pointerHeight, popperRef, anchorEl } = + usePointer(); - if (!tooltipData || !mousePosition) { + if (!tooltipData || !isActive) { // No data to display return null; } // The pointer type can be used to have different behavior based on pointer type. - const isMousePointer = mousePosition?.pointerType === 'mouse'; // Adapt the tooltip offset to the size of the pointer. - const yOffset = isMousePointer ? 0 : 40 - mousePosition.height; + const yOffset = isMousePointer ? 0 : 40 - pointerHeight; return ( @@ -29,7 +87,8 @@ export function CustomAxisTooltip() { }} open placement={isMousePointer ? 'top-end' : 'top'} - anchorEl={generateVirtualElement(mousePosition)} + anchorEl={anchorEl} + popperRef={popperRef} modifiers={[ { name: 'offset', diff --git a/docs/data/charts/tooltip/CustomAxisTooltip.tsx b/docs/data/charts/tooltip/CustomAxisTooltip.tsx index e3e36c4e65b5..841d740c49ba 100644 --- a/docs/data/charts/tooltip/CustomAxisTooltip.tsx +++ b/docs/data/charts/tooltip/CustomAxisTooltip.tsx @@ -1,24 +1,88 @@ import * as React from 'react'; import NoSsr from '@mui/material/NoSsr'; -import Popper from '@mui/material/Popper'; +import Popper, { PopperProps } from '@mui/material/Popper'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; -import { useAxisTooltip, useMouseTracker } from '@mui/x-charts/ChartsTooltip'; +import { useAxisTooltip } from '@mui/x-charts/ChartsTooltip'; +import { useSvgRef } from '@mui/x-charts/hooks'; import { generateVirtualElement } from './generateVirtualElement'; +type PointerState = { + isActive: boolean; + isMousePointer: boolean; + pointerHeight: number; +}; + +function usePointer(): PointerState & Pick { + const svgRef = useSvgRef(); + const popperRef: PopperProps['popperRef'] = React.useRef(null); + const virtualElement = React.useRef(generateVirtualElement({ x: 0, y: 0 })); + + // Use a ref to avoid rerendering on every mousemove event. + const [pointer, setPointer] = React.useState({ + isActive: false, + isMousePointer: false, + pointerHeight: 0, + }); + + React.useEffect(() => { + const element = svgRef.current; + if (element === null) { + return () => {}; + } + + const handleOut = (event: PointerEvent) => { + if (event.pointerType !== 'mouse') { + setPointer((prev) => ({ + ...prev, + isActive: false, + })); + } + }; + + const handleEnter = (event: PointerEvent) => { + setPointer({ + isActive: true, + isMousePointer: event.pointerType === 'mouse', + pointerHeight: event.height, + }); + }; + + const handleMove = (event: PointerEvent) => { + virtualElement.current = generateVirtualElement({ + x: event.clientX, + y: event.clientY, + }); + popperRef.current?.update(); + }; + + element.addEventListener('pointerenter', handleEnter); + element.addEventListener('pointerup', handleOut); + element.addEventListener('pointermove', handleMove); + + return () => { + element.removeEventListener('pointerenter', handleEnter); + element.removeEventListener('pointerup', handleOut); + element.removeEventListener('pointermove', handleMove); + }; + }, [svgRef]); + + return { ...pointer, popperRef, anchorEl: virtualElement.current }; +} + export function CustomAxisTooltip() { const tooltipData = useAxisTooltip(); - const mousePosition = useMouseTracker(); // Track the mouse position on chart. + const { isActive, isMousePointer, pointerHeight, popperRef, anchorEl } = + usePointer(); - if (!tooltipData || !mousePosition) { + if (!tooltipData || !isActive) { // No data to display return null; } // The pointer type can be used to have different behavior based on pointer type. - const isMousePointer = mousePosition?.pointerType === 'mouse'; // Adapt the tooltip offset to the size of the pointer. - const yOffset = isMousePointer ? 0 : 40 - mousePosition.height; + const yOffset = isMousePointer ? 0 : 40 - pointerHeight; return ( @@ -29,7 +93,8 @@ export function CustomAxisTooltip() { }} open placement={isMousePointer ? 'top-end' : 'top'} - anchorEl={generateVirtualElement(mousePosition)} + anchorEl={anchorEl} + popperRef={popperRef} modifiers={[ { name: 'offset', diff --git a/docs/data/charts/tooltip/CustomItemTooltip.js b/docs/data/charts/tooltip/CustomItemTooltip.js index fcaf28469db1..c41324f0d3df 100644 --- a/docs/data/charts/tooltip/CustomItemTooltip.js +++ b/docs/data/charts/tooltip/CustomItemTooltip.js @@ -4,22 +4,79 @@ import Popper from '@mui/material/Popper'; import Paper from '@mui/material/Paper'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; -import { useItemTooltip, useMouseTracker } from '@mui/x-charts/ChartsTooltip'; +import { useItemTooltip } from '@mui/x-charts/ChartsTooltip'; +import { useSvgRef } from '@mui/x-charts/hooks'; import { generateVirtualElement } from './generateVirtualElement'; +function usePointer() { + const svgRef = useSvgRef(); + const popperRef = React.useRef(null); + const virtualElement = React.useRef(generateVirtualElement({ x: 0, y: 0 })); + + // Use a ref to avoid rerendering on every mousemove event. + const [pointer, setPointer] = React.useState({ + isActive: false, + isMousePointer: false, + pointerHeight: 0, + }); + + React.useEffect(() => { + const element = svgRef.current; + if (element === null) { + return () => {}; + } + + const handleOut = (event) => { + if (event.pointerType !== 'mouse') { + setPointer((prev) => ({ + ...prev, + isActive: false, + })); + } + }; + + const handleEnter = (event) => { + setPointer({ + isActive: true, + isMousePointer: event.pointerType === 'mouse', + pointerHeight: event.height, + }); + }; + + const handleMove = (event) => { + virtualElement.current = generateVirtualElement({ + x: event.clientX, + y: event.clientY, + }); + popperRef.current?.update(); + }; + + element.addEventListener('pointerenter', handleEnter); + element.addEventListener('pointerup', handleOut); + element.addEventListener('pointermove', handleMove); + + return () => { + element.removeEventListener('pointerenter', handleEnter); + element.removeEventListener('pointerup', handleOut); + element.removeEventListener('pointermove', handleMove); + }; + }, [svgRef]); + + return { ...pointer, popperRef, anchorEl: virtualElement.current }; +} + export function CustomItemTooltip() { const tooltipData = useItemTooltip(); - const mousePosition = useMouseTracker(); // Track the mouse position on chart. + const { isActive, isMousePointer, pointerHeight, popperRef, anchorEl } = + usePointer(); - if (!tooltipData || !mousePosition) { + if (!tooltipData || !isActive) { // No data to display return null; } - // The pointer type can be used to have different behavior based on pointer type. - const isMousePointer = mousePosition?.pointerType === 'mouse'; // Adapt the tooltip offset to the size of the pointer. - const yOffset = isMousePointer ? 0 : 40 - mousePosition.height; + const yOffset = isMousePointer ? 0 : 40 - pointerHeight; return ( @@ -30,7 +87,8 @@ export function CustomItemTooltip() { }} open placement={isMousePointer ? 'top-end' : 'top'} - anchorEl={generateVirtualElement(mousePosition)} + anchorEl={anchorEl} + popperRef={popperRef} modifiers={[ { name: 'offset', diff --git a/docs/data/charts/tooltip/CustomItemTooltip.tsx b/docs/data/charts/tooltip/CustomItemTooltip.tsx index fcaf28469db1..926b4fd3179d 100644 --- a/docs/data/charts/tooltip/CustomItemTooltip.tsx +++ b/docs/data/charts/tooltip/CustomItemTooltip.tsx @@ -1,25 +1,88 @@ import * as React from 'react'; import NoSsr from '@mui/material/NoSsr'; -import Popper from '@mui/material/Popper'; +import Popper, { PopperProps } from '@mui/material/Popper'; import Paper from '@mui/material/Paper'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; -import { useItemTooltip, useMouseTracker } from '@mui/x-charts/ChartsTooltip'; +import { useItemTooltip } from '@mui/x-charts/ChartsTooltip'; +import { useSvgRef } from '@mui/x-charts/hooks'; import { generateVirtualElement } from './generateVirtualElement'; +type PointerState = { + isActive: boolean; + isMousePointer: boolean; + pointerHeight: number; +}; + +function usePointer(): PointerState & Pick { + const svgRef = useSvgRef(); + const popperRef: PopperProps['popperRef'] = React.useRef(null); + const virtualElement = React.useRef(generateVirtualElement({ x: 0, y: 0 })); + + // Use a ref to avoid rerendering on every mousemove event. + const [pointer, setPointer] = React.useState({ + isActive: false, + isMousePointer: false, + pointerHeight: 0, + }); + + React.useEffect(() => { + const element = svgRef.current; + if (element === null) { + return () => {}; + } + + const handleOut = (event: PointerEvent) => { + if (event.pointerType !== 'mouse') { + setPointer((prev) => ({ + ...prev, + isActive: false, + })); + } + }; + + const handleEnter = (event: PointerEvent) => { + setPointer({ + isActive: true, + isMousePointer: event.pointerType === 'mouse', + pointerHeight: event.height, + }); + }; + + const handleMove = (event: PointerEvent) => { + virtualElement.current = generateVirtualElement({ + x: event.clientX, + y: event.clientY, + }); + popperRef.current?.update(); + }; + + element.addEventListener('pointerenter', handleEnter); + element.addEventListener('pointerup', handleOut); + element.addEventListener('pointermove', handleMove); + + return () => { + element.removeEventListener('pointerenter', handleEnter); + element.removeEventListener('pointerup', handleOut); + element.removeEventListener('pointermove', handleMove); + }; + }, [svgRef]); + + return { ...pointer, popperRef, anchorEl: virtualElement.current }; +} + export function CustomItemTooltip() { const tooltipData = useItemTooltip(); - const mousePosition = useMouseTracker(); // Track the mouse position on chart. + const { isActive, isMousePointer, pointerHeight, popperRef, anchorEl } = + usePointer(); - if (!tooltipData || !mousePosition) { + if (!tooltipData || !isActive) { // No data to display return null; } - // The pointer type can be used to have different behavior based on pointer type. - const isMousePointer = mousePosition?.pointerType === 'mouse'; // Adapt the tooltip offset to the size of the pointer. - const yOffset = isMousePointer ? 0 : 40 - mousePosition.height; + const yOffset = isMousePointer ? 0 : 40 - pointerHeight; return ( @@ -30,7 +93,8 @@ export function CustomItemTooltip() { }} open placement={isMousePointer ? 'top-end' : 'top'} - anchorEl={generateVirtualElement(mousePosition)} + anchorEl={anchorEl} + popperRef={popperRef} modifiers={[ { name: 'offset', diff --git a/docs/data/charts/tooltip/ItemTooltip.js b/docs/data/charts/tooltip/ItemTooltip.js index 68fbab1d77ae..cb2a90370e5e 100644 --- a/docs/data/charts/tooltip/ItemTooltip.js +++ b/docs/data/charts/tooltip/ItemTooltip.js @@ -1,23 +1,80 @@ import * as React from 'react'; import NoSsr from '@mui/material/NoSsr'; import Popper from '@mui/material/Popper'; -import { useItemTooltip, useMouseTracker } from '@mui/x-charts/ChartsTooltip'; +import { useItemTooltip } from '@mui/x-charts/ChartsTooltip'; +import { useSvgRef } from '@mui/x-charts/hooks'; import { CustomItemTooltipContent } from './CustomItemTooltipContent'; import { generateVirtualElement } from './generateVirtualElement'; +function usePointer() { + const svgRef = useSvgRef(); + const popperRef = React.useRef(null); + const virtualElement = React.useRef(generateVirtualElement({ x: 0, y: 0 })); + + // Use a ref to avoid rerendering on every mousemove event. + const [pointer, setPointer] = React.useState({ + isActive: false, + isMousePointer: false, + pointerHeight: 0, + }); + + React.useEffect(() => { + const element = svgRef.current; + if (element === null) { + return () => {}; + } + + const handleOut = (event) => { + if (event.pointerType !== 'mouse') { + setPointer((prev) => ({ + ...prev, + isActive: false, + })); + } + }; + + const handleEnter = (event) => { + setPointer({ + isActive: true, + isMousePointer: event.pointerType === 'mouse', + pointerHeight: event.height, + }); + }; + + const handleMove = (event) => { + virtualElement.current = generateVirtualElement({ + x: event.clientX, + y: event.clientY, + }); + popperRef.current?.update(); + }; + + element.addEventListener('pointerenter', handleEnter); + element.addEventListener('pointerup', handleOut); + element.addEventListener('pointermove', handleMove); + + return () => { + element.removeEventListener('pointerenter', handleEnter); + element.removeEventListener('pointerup', handleOut); + element.removeEventListener('pointermove', handleMove); + }; + }, [svgRef]); + + return { ...pointer, popperRef, anchorEl: virtualElement.current }; +} + export function ItemTooltip() { const tooltipData = useItemTooltip(); - const mousePosition = useMouseTracker(); // Track the mouse position on chart. + const { isActive, isMousePointer, pointerHeight, popperRef, anchorEl } = + usePointer(); - if (!tooltipData || !mousePosition) { + if (!tooltipData || !isActive) { // No data to display return null; } - // The pointer type can be used to have different behavior based on pointer type. - const isMousePointer = mousePosition?.pointerType === 'mouse'; // Adapt the tooltip offset to the size of the pointer. - const yOffset = isMousePointer ? 0 : 40 - mousePosition.height; + const yOffset = isMousePointer ? 0 : 40 - pointerHeight; return ( @@ -28,7 +85,8 @@ export function ItemTooltip() { }} open placement={isMousePointer ? 'top-end' : 'top'} - anchorEl={generateVirtualElement(mousePosition)} + anchorEl={anchorEl} + popperRef={popperRef} modifiers={[ { name: 'offset', diff --git a/docs/data/charts/tooltip/ItemTooltip.tsx b/docs/data/charts/tooltip/ItemTooltip.tsx index 68fbab1d77ae..5069ad32df3d 100644 --- a/docs/data/charts/tooltip/ItemTooltip.tsx +++ b/docs/data/charts/tooltip/ItemTooltip.tsx @@ -1,23 +1,86 @@ import * as React from 'react'; import NoSsr from '@mui/material/NoSsr'; -import Popper from '@mui/material/Popper'; -import { useItemTooltip, useMouseTracker } from '@mui/x-charts/ChartsTooltip'; +import Popper, { PopperProps } from '@mui/material/Popper'; +import { useItemTooltip } from '@mui/x-charts/ChartsTooltip'; +import { useSvgRef } from '@mui/x-charts/hooks'; import { CustomItemTooltipContent } from './CustomItemTooltipContent'; import { generateVirtualElement } from './generateVirtualElement'; +type PointerState = { + isActive: boolean; + isMousePointer: boolean; + pointerHeight: number; +}; + +function usePointer(): PointerState & Pick { + const svgRef = useSvgRef(); + const popperRef: PopperProps['popperRef'] = React.useRef(null); + const virtualElement = React.useRef(generateVirtualElement({ x: 0, y: 0 })); + + // Use a ref to avoid rerendering on every mousemove event. + const [pointer, setPointer] = React.useState({ + isActive: false, + isMousePointer: false, + pointerHeight: 0, + }); + + React.useEffect(() => { + const element = svgRef.current; + if (element === null) { + return () => {}; + } + + const handleOut = (event: PointerEvent) => { + if (event.pointerType !== 'mouse') { + setPointer((prev) => ({ + ...prev, + isActive: false, + })); + } + }; + + const handleEnter = (event: PointerEvent) => { + setPointer({ + isActive: true, + isMousePointer: event.pointerType === 'mouse', + pointerHeight: event.height, + }); + }; + + const handleMove = (event: PointerEvent) => { + virtualElement.current = generateVirtualElement({ + x: event.clientX, + y: event.clientY, + }); + popperRef.current?.update(); + }; + + element.addEventListener('pointerenter', handleEnter); + element.addEventListener('pointerup', handleOut); + element.addEventListener('pointermove', handleMove); + + return () => { + element.removeEventListener('pointerenter', handleEnter); + element.removeEventListener('pointerup', handleOut); + element.removeEventListener('pointermove', handleMove); + }; + }, [svgRef]); + + return { ...pointer, popperRef, anchorEl: virtualElement.current }; +} + export function ItemTooltip() { const tooltipData = useItemTooltip(); - const mousePosition = useMouseTracker(); // Track the mouse position on chart. + const { isActive, isMousePointer, pointerHeight, popperRef, anchorEl } = + usePointer(); - if (!tooltipData || !mousePosition) { + if (!tooltipData || !isActive) { // No data to display return null; } - // The pointer type can be used to have different behavior based on pointer type. - const isMousePointer = mousePosition?.pointerType === 'mouse'; // Adapt the tooltip offset to the size of the pointer. - const yOffset = isMousePointer ? 0 : 40 - mousePosition.height; + const yOffset = isMousePointer ? 0 : 40 - pointerHeight; return ( @@ -28,7 +91,8 @@ export function ItemTooltip() { }} open placement={isMousePointer ? 'top-end' : 'top'} - anchorEl={generateVirtualElement(mousePosition)} + anchorEl={anchorEl} + popperRef={popperRef} modifiers={[ { name: 'offset', diff --git a/docs/data/charts/tooltip/ItemTooltipFixedY.js b/docs/data/charts/tooltip/ItemTooltipFixedY.js index 4e19cd1bc9f6..11bfb8fe8668 100644 --- a/docs/data/charts/tooltip/ItemTooltipFixedY.js +++ b/docs/data/charts/tooltip/ItemTooltipFixedY.js @@ -1,28 +1,92 @@ import * as React from 'react'; import NoSsr from '@mui/material/NoSsr'; import Popper from '@mui/material/Popper'; -import { useItemTooltip, useMouseTracker } from '@mui/x-charts/ChartsTooltip'; +import { useItemTooltip } from '@mui/x-charts/ChartsTooltip'; import { useDrawingArea, useSvgRef } from '@mui/x-charts/hooks'; import { CustomItemTooltipContent } from './CustomItemTooltipContent'; import { generateVirtualElement } from './generateVirtualElement'; +function usePointer() { + const svgRef = useSvgRef(); + + // Use a ref to avoid rerendering on every mousemove event. + const [pointer, setPointer] = React.useState({ + isActive: false, + isMousePointer: false, + pointerHeight: 0, + }); + + React.useEffect(() => { + const element = svgRef.current; + if (element === null) { + return () => {}; + } + + const handleOut = (event) => { + if (event.pointerType !== 'mouse') { + setPointer((prev) => ({ + ...prev, + isActive: false, + })); + } + }; + + const handleEnter = (event) => { + setPointer({ + isActive: true, + isMousePointer: event.pointerType === 'mouse', + pointerHeight: event.height, + }); + }; + + element.addEventListener('pointerenter', handleEnter); + element.addEventListener('pointerup', handleOut); + + return () => { + element.removeEventListener('pointerenter', handleEnter); + element.removeEventListener('pointerup', handleOut); + }; + }, [svgRef]); + + return pointer; +} + export function ItemTooltipFixedY() { const tooltipData = useItemTooltip(); - const mousePosition = useMouseTracker(); + const { isActive } = usePointer(); + + const popperRef = React.useRef(null); + const virtualElement = React.useRef(generateVirtualElement({ x: 0, y: 0 })); const svgRef = useSvgRef(); // Get the ref of the component. const drawingArea = useDrawingArea(); // Get the dimensions of the chart inside the . - if (!tooltipData || !mousePosition) { + React.useEffect(() => { + const element = svgRef.current; + if (element === null) { + return () => {}; + } + + const handleMove = (event) => { + virtualElement.current = generateVirtualElement({ + x: event.clientX, + // Add the y-coordinate of the to the to margin between the and the drawing area + y: svgRef.current.getBoundingClientRect().top + drawingArea.top, + }); + popperRef.current?.update(); + }; + + element.addEventListener('pointermove', handleMove); + + return () => { + element.removeEventListener('pointermove', handleMove); + }; + }, [svgRef, drawingArea.top]); + + if (!tooltipData || !isActive) { // No data to display return null; } - const tooltipPosition = { - ...mousePosition, - // Add the y-coordinate of the to the to margin between the and the drawing area - y: svgRef.current.getBoundingClientRect().top + drawingArea.top, - }; - return ( diff --git a/docs/data/charts/tooltip/ItemTooltipFixedY.tsx b/docs/data/charts/tooltip/ItemTooltipFixedY.tsx index 3059c260afca..b6feefe896f4 100644 --- a/docs/data/charts/tooltip/ItemTooltipFixedY.tsx +++ b/docs/data/charts/tooltip/ItemTooltipFixedY.tsx @@ -1,28 +1,98 @@ import * as React from 'react'; import NoSsr from '@mui/material/NoSsr'; -import Popper from '@mui/material/Popper'; -import { useItemTooltip, useMouseTracker } from '@mui/x-charts/ChartsTooltip'; +import Popper, { PopperProps } from '@mui/material/Popper'; +import { useItemTooltip } from '@mui/x-charts/ChartsTooltip'; import { useDrawingArea, useSvgRef } from '@mui/x-charts/hooks'; import { CustomItemTooltipContent } from './CustomItemTooltipContent'; -import { generateVirtualElement, MousePosition } from './generateVirtualElement'; +import { generateVirtualElement } from './generateVirtualElement'; + +type PointerState = { + isActive: boolean; + isMousePointer: boolean; + pointerHeight: number; +}; + +function usePointer(): PointerState { + const svgRef = useSvgRef(); + + // Use a ref to avoid rerendering on every mousemove event. + const [pointer, setPointer] = React.useState({ + isActive: false, + isMousePointer: false, + pointerHeight: 0, + }); + + React.useEffect(() => { + const element = svgRef.current; + if (element === null) { + return () => {}; + } + + const handleOut = (event: PointerEvent) => { + if (event.pointerType !== 'mouse') { + setPointer((prev) => ({ + ...prev, + isActive: false, + })); + } + }; + + const handleEnter = (event: PointerEvent) => { + setPointer({ + isActive: true, + isMousePointer: event.pointerType === 'mouse', + pointerHeight: event.height, + }); + }; + + element.addEventListener('pointerenter', handleEnter); + element.addEventListener('pointerup', handleOut); + + return () => { + element.removeEventListener('pointerenter', handleEnter); + element.removeEventListener('pointerup', handleOut); + }; + }, [svgRef]); + + return pointer; +} export function ItemTooltipFixedY() { const tooltipData = useItemTooltip(); - const mousePosition = useMouseTracker(); + const { isActive } = usePointer(); + + const popperRef: PopperProps['popperRef'] = React.useRef(null); + const virtualElement = React.useRef(generateVirtualElement({ x: 0, y: 0 })); const svgRef = useSvgRef(); // Get the ref of the component. const drawingArea = useDrawingArea(); // Get the dimensions of the chart inside the . - if (!tooltipData || !mousePosition) { + React.useEffect(() => { + const element = svgRef.current; + if (element === null) { + return () => {}; + } + + const handleMove = (event: PointerEvent) => { + virtualElement.current = generateVirtualElement({ + x: event.clientX, + // Add the y-coordinate of the to the to margin between the and the drawing area + y: svgRef.current.getBoundingClientRect().top + drawingArea.top, + }); + popperRef.current?.update(); + }; + + element.addEventListener('pointermove', handleMove); + + return () => { + element.removeEventListener('pointermove', handleMove); + }; + }, [svgRef, drawingArea.top]); + + if (!tooltipData || !isActive) { // No data to display return null; } - const tooltipPosition: MousePosition = { - ...mousePosition, - // Add the y-coordinate of the to the to margin between the and the drawing area - y: svgRef.current.getBoundingClientRect().top + drawingArea.top, - }; - return ( diff --git a/docs/data/charts/tooltip/ItemTooltipTopElement.js b/docs/data/charts/tooltip/ItemTooltipTopElement.js index 7d86e69b81e4..a3b52483984e 100644 --- a/docs/data/charts/tooltip/ItemTooltipTopElement.js +++ b/docs/data/charts/tooltip/ItemTooltipTopElement.js @@ -2,14 +2,59 @@ import * as React from 'react'; import NoSsr from '@mui/material/NoSsr'; import Popper from '@mui/material/Popper'; -import { useItemTooltip, useMouseTracker } from '@mui/x-charts/ChartsTooltip'; +import { useItemTooltip } from '@mui/x-charts/ChartsTooltip'; import { useSvgRef, useXAxis, useXScale, useYScale } from '@mui/x-charts/hooks'; import { CustomItemTooltipContent } from './CustomItemTooltipContent'; import { generateVirtualElement } from './generateVirtualElement'; +function usePointer() { + const svgRef = useSvgRef(); + + // Use a ref to avoid rerendering on every mousemove event. + const [pointer, setPointer] = React.useState({ + isActive: false, + isMousePointer: false, + pointerHeight: 0, + }); + + React.useEffect(() => { + const element = svgRef.current; + if (element === null) { + return () => {}; + } + + const handleOut = (event) => { + if (event.pointerType !== 'mouse') { + setPointer((prev) => ({ + ...prev, + isActive: false, + })); + } + }; + + const handleEnter = (event) => { + setPointer({ + isActive: true, + isMousePointer: event.pointerType === 'mouse', + pointerHeight: event.height, + }); + }; + + element.addEventListener('pointerenter', handleEnter); + element.addEventListener('pointerup', handleOut); + + return () => { + element.removeEventListener('pointerenter', handleEnter); + element.removeEventListener('pointerup', handleOut); + }; + }, [svgRef]); + + return pointer; +} + export function ItemTooltipTopElement() { const tooltipData = useItemTooltip(); - const mousePosition = useMouseTracker(); + const { isActive } = usePointer(); // Get xAxis config to access its data array. const xAxis = useXAxis(); // Get the scale which map values to SVG coordinates. @@ -21,7 +66,7 @@ export function ItemTooltipTopElement() { // Get the ref of the component. const svgRef = useSvgRef(); - if (!tooltipData || !mousePosition || !xAxis.data) { + if (!tooltipData || !isActive || !xAxis.data) { // No data to display return null; } @@ -41,7 +86,6 @@ export function ItemTooltipTopElement() { const svgXPosition = xScale(xValue) ?? 0; const tooltipPosition = { - ...mousePosition, // Add half of `yScale.step()` to be in the middle of the band. x: svgRef.current.getBoundingClientRect().left + svgXPosition + xScale.step() / 2, diff --git a/docs/data/charts/tooltip/ItemTooltipTopElement.tsx b/docs/data/charts/tooltip/ItemTooltipTopElement.tsx index d69ab33e96da..a84a45cdc39b 100644 --- a/docs/data/charts/tooltip/ItemTooltipTopElement.tsx +++ b/docs/data/charts/tooltip/ItemTooltipTopElement.tsx @@ -2,14 +2,65 @@ import * as React from 'react'; import { ScaleBand } from '@mui/x-charts-vendor/d3-scale'; import NoSsr from '@mui/material/NoSsr'; import Popper from '@mui/material/Popper'; -import { useItemTooltip, useMouseTracker } from '@mui/x-charts/ChartsTooltip'; +import { useItemTooltip } from '@mui/x-charts/ChartsTooltip'; import { useSvgRef, useXAxis, useXScale, useYScale } from '@mui/x-charts/hooks'; import { CustomItemTooltipContent } from './CustomItemTooltipContent'; -import { generateVirtualElement, MousePosition } from './generateVirtualElement'; +import { generateVirtualElement } from './generateVirtualElement'; + +type PointerState = { + isActive: boolean; + isMousePointer: boolean; + pointerHeight: number; +}; + +function usePointer(): PointerState { + const svgRef = useSvgRef(); + + // Use a ref to avoid rerendering on every mousemove event. + const [pointer, setPointer] = React.useState({ + isActive: false, + isMousePointer: false, + pointerHeight: 0, + }); + + React.useEffect(() => { + const element = svgRef.current; + if (element === null) { + return () => {}; + } + + const handleOut = (event: PointerEvent) => { + if (event.pointerType !== 'mouse') { + setPointer((prev) => ({ + ...prev, + isActive: false, + })); + } + }; + + const handleEnter = (event: PointerEvent) => { + setPointer({ + isActive: true, + isMousePointer: event.pointerType === 'mouse', + pointerHeight: event.height, + }); + }; + + element.addEventListener('pointerenter', handleEnter); + element.addEventListener('pointerup', handleOut); + + return () => { + element.removeEventListener('pointerenter', handleEnter); + element.removeEventListener('pointerup', handleOut); + }; + }, [svgRef]); + + return pointer; +} export function ItemTooltipTopElement() { const tooltipData = useItemTooltip<'bar'>(); - const mousePosition = useMouseTracker(); + const { isActive } = usePointer(); // Get xAxis config to access its data array. const xAxis = useXAxis(); // Get the scale which map values to SVG coordinates. @@ -21,7 +72,7 @@ export function ItemTooltipTopElement() { // Get the ref of the component. const svgRef = useSvgRef(); - if (!tooltipData || !mousePosition || !xAxis.data) { + if (!tooltipData || !isActive || !xAxis.data) { // No data to display return null; } @@ -40,8 +91,7 @@ export function ItemTooltipTopElement() { const svgYPosition = yScale(tooltipData.value) ?? 0; const svgXPosition = xScale(xValue) ?? 0; - const tooltipPosition: MousePosition = { - ...mousePosition, + const tooltipPosition = { // Add half of `yScale.step()` to be in the middle of the band. x: svgRef.current.getBoundingClientRect().left + diff --git a/docs/data/charts/tooltip/tooltip.md b/docs/data/charts/tooltip/tooltip.md index 655886065223..f53171fca6e5 100644 --- a/docs/data/charts/tooltip/tooltip.md +++ b/docs/data/charts/tooltip/tooltip.md @@ -15,8 +15,8 @@ If you are using composition, you can add the `` component and The tooltip can be triggered by two kinds of events: -- `'item'`—when the user's mouse hovers over an item on the chart, the tooltip will display data about this specific item. -- `'axis'`—the user's mouse position is associated with a value of the x-axis. The tooltip will display data about all series at this specific x value. +- `'item'`—when the user's mouse hovers over an item on the chart, the tooltip displays data about this specific item. +- `'axis'`—the user's mouse position is associated with a value of the x-axis. The tooltip displays data about all series at this specific x value. - `'none'`—disable the tooltip. {{"demo": "Interaction.js"}} @@ -79,7 +79,7 @@ See [Label—Conditional formatting](/x/react-charts/label/#conditional-formatti ### Hiding values You can hide the axis value with `hideTooltip` in the `xAxis` props. -It will remove the header showing the x-axis value from the tooltip. +It removes the header showing the x-axis value from the tooltip. ```jsx ({ - color: ownerState.open ? 'secondary' : 'primary', + color: ownerState.isPickerOpen ? 'secondary' : 'primary', }), }} /> diff --git a/docs/data/common-concepts/custom-components/CustomSlotPropsCallback.tsx b/docs/data/common-concepts/custom-components/CustomSlotPropsCallback.tsx index e1272f2776ba..ea3b72c58ed1 100644 --- a/docs/data/common-concepts/custom-components/CustomSlotPropsCallback.tsx +++ b/docs/data/common-concepts/custom-components/CustomSlotPropsCallback.tsx @@ -9,7 +9,7 @@ export default function CustomSlotPropsCallback() { ({ - color: ownerState.open ? 'secondary' : 'primary', + color: ownerState.isPickerOpen ? 'secondary' : 'primary', }), }} /> diff --git a/docs/data/common-concepts/custom-components/CustomSlotPropsCallback.tsx.preview b/docs/data/common-concepts/custom-components/CustomSlotPropsCallback.tsx.preview index 100188718fae..70bc3e35f25e 100644 --- a/docs/data/common-concepts/custom-components/CustomSlotPropsCallback.tsx.preview +++ b/docs/data/common-concepts/custom-components/CustomSlotPropsCallback.tsx.preview @@ -1,7 +1,7 @@ ({ - color: ownerState.open ? 'secondary' : 'primary', + color: ownerState.isPickerOpen ? 'secondary' : 'primary', }), }} /> \ No newline at end of file diff --git a/docs/data/data-grid/column-definition/AutogeneratedRows.js b/docs/data/data-grid/column-definition/AutogeneratedRows.js index d5689bc6ecbd..b9059be151f0 100644 --- a/docs/data/data-grid/column-definition/AutogeneratedRows.js +++ b/docs/data/data-grid/column-definition/AutogeneratedRows.js @@ -1,6 +1,7 @@ import * as React from 'react'; import { DataGridPremium, + GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, isAutogeneratedRow, useGridApiRef, useKeepGroupedColumnsHidden, @@ -8,7 +9,7 @@ import { import { useMovieData } from '@mui/x-data-grid-generator'; const columns = [ - { field: '__row_group_by_columns_group__', width: 200 }, + { field: GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, width: 200 }, { field: 'company', width: 200 }, { field: 'title', diff --git a/docs/data/data-grid/column-definition/AutogeneratedRows.tsx b/docs/data/data-grid/column-definition/AutogeneratedRows.tsx index c58c4bf79e2d..cb127be3cb88 100644 --- a/docs/data/data-grid/column-definition/AutogeneratedRows.tsx +++ b/docs/data/data-grid/column-definition/AutogeneratedRows.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { DataGridPremium, + GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, GridColDef, isAutogeneratedRow, useGridApiRef, @@ -9,7 +10,7 @@ import { import { useMovieData } from '@mui/x-data-grid-generator'; const columns: GridColDef[] = [ - { field: '__row_group_by_columns_group__', width: 200 }, + { field: GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, width: 200 }, { field: 'company', width: 200 }, { field: 'title', diff --git a/docs/data/data-grid/column-definition/column-definition.md b/docs/data/data-grid/column-definition/column-definition.md index 75364b0c7c81..89c245b09175 100644 --- a/docs/data/data-grid/column-definition/column-definition.md +++ b/docs/data/data-grid/column-definition/column-definition.md @@ -102,8 +102,8 @@ Read more in the [handling autogenerated rows](/x/react-data-grid/column-definit ::: :::warning -[Row grouping](/x/react-data-grid/row-grouping/) uses the [`groupingValueGetter`](/x/react-data-grid/row-grouping/#using-groupingvaluegetter-for-complex-grouping-value) instead of `valueGetter` to get the value for the grouping. -The value passed to the `groupingValueGetter` is the raw row value (`row[field]`) even if the column definition has a `valueGetter` defined. +[Row grouping](/x/react-data-grid/row-grouping/) uses the [`groupingValueGetter()`](/x/react-data-grid/row-grouping/#using-groupingvaluegetter-for-complex-grouping-value) instead of `valueGetter` to get the value for the grouping. +The value passed to the `groupingValueGetter()` is the raw row value (`row[field]`) even if the column definition has a `valueGetter` defined. ::: ### Value formatter diff --git a/docs/data/data-grid/column-visibility/ColumnSelectorGridCustomizeColumns.js b/docs/data/data-grid/column-visibility/ColumnSelectorGridCustomizeColumns.js index bfa80b2c2523..96ed3a295aa2 100644 --- a/docs/data/data-grid/column-visibility/ColumnSelectorGridCustomizeColumns.js +++ b/docs/data/data-grid/column-visibility/ColumnSelectorGridCustomizeColumns.js @@ -4,10 +4,11 @@ import { GridToolbar, useKeepGroupedColumnsHidden, useGridApiRef, + GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, } from '@mui/x-data-grid-premium'; import { useDemoData } from '@mui/x-data-grid-generator'; -const hiddenFields = ['id', '__row_group_by_columns_group__', 'status']; +const hiddenFields = ['id', GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, 'status']; const getTogglableColumns = (columns) => { return columns diff --git a/docs/data/data-grid/column-visibility/ColumnSelectorGridCustomizeColumns.tsx b/docs/data/data-grid/column-visibility/ColumnSelectorGridCustomizeColumns.tsx index 9c667a5f013e..83eb196585b7 100644 --- a/docs/data/data-grid/column-visibility/ColumnSelectorGridCustomizeColumns.tsx +++ b/docs/data/data-grid/column-visibility/ColumnSelectorGridCustomizeColumns.tsx @@ -5,10 +5,11 @@ import { GridColDef, useKeepGroupedColumnsHidden, useGridApiRef, + GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, } from '@mui/x-data-grid-premium'; import { useDemoData } from '@mui/x-data-grid-generator'; -const hiddenFields = ['id', '__row_group_by_columns_group__', 'status']; +const hiddenFields = ['id', GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, 'status']; const getTogglableColumns = (columns: GridColDef[]) => { return columns diff --git a/docs/data/data-grid/column-visibility/column-visibility.md b/docs/data/data-grid/column-visibility/column-visibility.md index 08d912a07057..3b7a149dd701 100644 --- a/docs/data/data-grid/column-visibility/column-visibility.md +++ b/docs/data/data-grid/column-visibility/column-visibility.md @@ -86,8 +86,13 @@ In the following demo, the columns panel is disabled, and access to columns `id` To show or hide specific columns in the column visibility panel, use the `slotProps.columnsManagement.getTogglableColumns` prop. It should return an array of column field names. ```tsx -// stop `id`, `__row_group_by_columns_group__`, and `status` columns to be togglable -const hiddenFields = ['id', '__row_group_by_columns_group__', 'status']; +import { + DataGridPremium, + GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, +} from '@mui/x-data-grid-premium'; + +// stop `id`, GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, and `status` columns to be togglable +const hiddenFields = ['id', GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, 'status']; const getTogglableColumns = (columns: GridColDef[]) => { return columns @@ -95,7 +100,7 @@ const getTogglableColumns = (columns: GridColDef[]) => { .map((column) => column.field); }; -; ``` -Note that `createTheme` accepts any number of arguments. +Note that `createTheme()` accepts any number of arguments. If you are already using the [translations of the core components](/material-ui/guides/localization/#locale-text), you can add `bgBG` as a new argument. The same import works for Data Grid Pro as it's an extension of Data Grid. @@ -86,7 +86,7 @@ const theme = createTheme( ; ``` -If you want to pass language translations directly to the Data Grid without using `createTheme` and `ThemeProvider`, you can directly load the language translations from `@mui/x-data-grid/locales`. +If you want to pass language translations directly to the Data Grid without using `createTheme()` and `ThemeProvider`, you can directly load the language translations from `@mui/x-data-grid/locales`. ```jsx import { DataGrid } from '@mui/x-data-grid'; diff --git a/docs/data/data-grid/overview/DataGridPremiumDemo.js b/docs/data/data-grid/overview/DataGridPremiumDemo.js index 98a6959cff62..3e99541b76cb 100644 --- a/docs/data/data-grid/overview/DataGridPremiumDemo.js +++ b/docs/data/data-grid/overview/DataGridPremiumDemo.js @@ -2,6 +2,7 @@ import * as React from 'react'; import Box from '@mui/material/Box'; import { DataGridPremium, + GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, GridToolbar, useGridApiRef, useKeepGroupedColumnsHidden, @@ -40,7 +41,7 @@ export default function DataGridPremiumDemo() { model: ['commodity'], }, sorting: { - sortModel: [{ field: '__row_group_by_columns_group__', sort: 'asc' }], + sortModel: [{ field: GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, sort: 'asc' }], }, aggregation: { model: { diff --git a/docs/data/data-grid/overview/DataGridPremiumDemo.tsx b/docs/data/data-grid/overview/DataGridPremiumDemo.tsx index 98a6959cff62..3e99541b76cb 100644 --- a/docs/data/data-grid/overview/DataGridPremiumDemo.tsx +++ b/docs/data/data-grid/overview/DataGridPremiumDemo.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import Box from '@mui/material/Box'; import { DataGridPremium, + GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, GridToolbar, useGridApiRef, useKeepGroupedColumnsHidden, @@ -40,7 +41,7 @@ export default function DataGridPremiumDemo() { model: ['commodity'], }, sorting: { - sortModel: [{ field: '__row_group_by_columns_group__', sort: 'asc' }], + sortModel: [{ field: GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, sort: 'asc' }], }, aggregation: { model: { diff --git a/docs/data/data-grid/row-grouping/RowGroupingFullExample.js b/docs/data/data-grid/row-grouping/RowGroupingFullExample.js index d1a08e3ff062..52040b1639af 100644 --- a/docs/data/data-grid/row-grouping/RowGroupingFullExample.js +++ b/docs/data/data-grid/row-grouping/RowGroupingFullExample.js @@ -1,6 +1,7 @@ import * as React from 'react'; import { DataGridPremium, + GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, useGridApiRef, useKeepGroupedColumnsHidden, } from '@mui/x-data-grid-premium'; @@ -23,7 +24,7 @@ export default function RowGroupingFullExample() { model: ['commodity'], }, sorting: { - sortModel: [{ field: '__row_group_by_columns_group__', sort: 'asc' }], + sortModel: [{ field: GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, sort: 'asc' }], }, }, }); diff --git a/docs/data/data-grid/row-grouping/RowGroupingFullExample.tsx b/docs/data/data-grid/row-grouping/RowGroupingFullExample.tsx index d1a08e3ff062..52040b1639af 100644 --- a/docs/data/data-grid/row-grouping/RowGroupingFullExample.tsx +++ b/docs/data/data-grid/row-grouping/RowGroupingFullExample.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { DataGridPremium, + GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, useGridApiRef, useKeepGroupedColumnsHidden, } from '@mui/x-data-grid-premium'; @@ -23,7 +24,7 @@ export default function RowGroupingFullExample() { model: ['commodity'], }, sorting: { - sortModel: [{ field: '__row_group_by_columns_group__', sort: 'asc' }], + sortModel: [{ field: GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, sort: 'asc' }], }, }, }); diff --git a/docs/data/data-grid/row-grouping/row-grouping.md b/docs/data/data-grid/row-grouping/row-grouping.md index b70899fb38b4..13f56a2ebac8 100644 --- a/docs/data/data-grid/row-grouping/row-grouping.md +++ b/docs/data/data-grid/row-grouping/row-grouping.md @@ -11,6 +11,10 @@ In the following example, movies are grouped based on their production `company` {{"demo": "RowGroupingBasicExample.js", "bg": "inline", "defaultCodeOpen": false}} +:::info +If you are looking for row grouping on the server-side, see [server-side row grouping](/x/react-data-grid/server-side-data/row-grouping/). +::: + ## Grouping criteria ### Initialize the row grouping @@ -252,6 +256,10 @@ Use the `setRowChildrenExpansion` method on `apiRef` to programmatically set the {{"demo": "RowGroupingSetChildrenExpansion.js", "bg": "inline", "defaultCodeOpen": false}} +:::warning +The `apiRef.current.setRowChildrenExpansion` method is not compatible with the [server-side tree data](/x/react-data-grid/server-side-data/tree-data/) and [server-side row grouping](/x/react-data-grid/server-side-data/row-grouping/). Use `apiRef.current.unstable_dataSource.fetchRows` instead. +::: + ### Customize grouping cell indent To change the default cell indent, you can use the `--DataGrid-cellOffsetMultiplier` CSS variable: @@ -280,10 +288,6 @@ If you are rendering leaves with the `leafField` property of `groupingColDef`, t You can force the filtering to be applied on another grouping criteria with the `mainGroupingCriteria` property of `groupingColDef` -:::warning -This feature is not yet compatible with `sortingMode = "server"` and `filteringMode = "server"`. -::: - {{"demo": "RowGroupingFilteringSingleGroupingColDef.js", "bg": "inline", "defaultCodeOpen": false}} ### Multiple grouping columns @@ -376,6 +380,10 @@ const rows = apiRef.current.getRowGroupChildren({ {{"demo": "RowGroupingGetRowGroupChildren.js", "bg": "inline", "defaultCodeOpen": false}} +:::warning +The `apiRef.current.getRowGroupChildren` method is not compatible with the [server-side row grouping](/x/react-data-grid/server-side-data/row-grouping/) since all the rows might not be available to get at a given instance. +::: + ## Row group panel 🚧 :::warning diff --git a/docs/data/data-grid/server-side-data/ServerSideRowGroupingDataGrid.js b/docs/data/data-grid/server-side-data/ServerSideRowGroupingDataGrid.js new file mode 100644 index 000000000000..fc75932d136b --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideRowGroupingDataGrid.js @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { + DataGridPremium, + useGridApiRef, + useKeepGroupedColumnsHidden, +} from '@mui/x-data-grid-premium'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Button from '@mui/material/Button'; + +export default function ServerSideRowGroupingDataGrid() { + const apiRef = useGridApiRef(); + + const { fetchRows, columns } = useMockServer({ + rowGrouping: true, + }); + + const dataSource = React.useMemo(() => { + return { + 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), + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + getGroupKey: (row) => row.group, + getChildrenCount: (row) => row.descendantCount, + }; + }, [fetchRows]); + + const initialState = useKeepGroupedColumnsHidden({ + apiRef, + initialState: { + rowGrouping: { + model: ['company', 'director'], + }, + }, + }); + + return ( +
+ + +
+ +
+
+ ); +} diff --git a/docs/data/data-grid/server-side-data/ServerSideRowGroupingDataGrid.tsx b/docs/data/data-grid/server-side-data/ServerSideRowGroupingDataGrid.tsx new file mode 100644 index 000000000000..91c7f66a6d99 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideRowGroupingDataGrid.tsx @@ -0,0 +1,70 @@ +import * as React from 'react'; +import { + DataGridPremium, + GridDataSource, + useGridApiRef, + useKeepGroupedColumnsHidden, +} from '@mui/x-data-grid-premium'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Button from '@mui/material/Button'; + +export default function ServerSideRowGroupingDataGrid() { + const apiRef = useGridApiRef(); + + const { fetchRows, columns } = useMockServer({ + rowGrouping: true, + }); + + const dataSource: GridDataSource = React.useMemo(() => { + return { + 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), + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + getGroupKey: (row) => row.group, + getChildrenCount: (row) => row.descendantCount, + }; + }, [fetchRows]); + + const initialState = useKeepGroupedColumnsHidden({ + apiRef, + initialState: { + rowGrouping: { + model: ['company', 'director'], + }, + }, + }); + + return ( +
+ + +
+ +
+
+ ); +} diff --git a/docs/data/data-grid/server-side-data/ServerSideRowGroupingDataGrid.tsx.preview b/docs/data/data-grid/server-side-data/ServerSideRowGroupingDataGrid.tsx.preview new file mode 100644 index 000000000000..920c80a41342 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideRowGroupingDataGrid.tsx.preview @@ -0,0 +1,16 @@ + + +
+ +
\ No newline at end of file diff --git a/docs/data/data-grid/server-side-data/ServerSideRowGroupingErrorHandling.js b/docs/data/data-grid/server-side-data/ServerSideRowGroupingErrorHandling.js new file mode 100644 index 000000000000..793347a26301 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideRowGroupingErrorHandling.js @@ -0,0 +1,137 @@ +import * as React from 'react'; +import { + DataGridPremium, + useGridApiRef, + useKeepGroupedColumnsHidden, +} from '@mui/x-data-grid-premium'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Snackbar from '@mui/material/Snackbar'; +import Button from '@mui/material/Button'; +import Checkbox from '@mui/material/Checkbox'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import { alpha, styled, darken, lighten } from '@mui/material/styles'; + +export default function ServerSideRowGroupingErrorHandling() { + const apiRef = useGridApiRef(); + const [rootError, setRootError] = React.useState(); + const [childrenError, setChildrenError] = React.useState(); + const [shouldRequestsFail, setShouldRequestsFail] = React.useState(false); + + const { fetchRows, columns } = useMockServer( + { + rowGrouping: true, + }, + {}, + shouldRequestsFail, + ); + + const dataSource = React.useMemo(() => { + return { + 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), + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + getGroupKey: (row) => row.group, + getChildrenCount: (row) => row.descendantCount, + }; + }, [fetchRows]); + + const initialState = useKeepGroupedColumnsHidden({ + apiRef, + initialState: { + rowGrouping: { + model: ['company', 'director'], + }, + }, + }); + + return ( +
+
+ + setShouldRequestsFail(event.target.checked)} + /> + } + label="Make the requests fail" + /> +
+
+ { + if (!params.groupKeys || params.groupKeys.length === 0) { + setRootError(error.message); + } else { + setChildrenError( + `${error.message} (Requested level: ${params.groupKeys.join(' > ')})`, + ); + } + }} + unstable_dataSourceCache={null} + apiRef={apiRef} + initialState={initialState} + /> + {rootError && } + setChildrenError('')} + message={childrenError} + /> +
+
+ ); +} + +function getBorderColor(theme) { + if (theme.palette.mode === 'light') { + return lighten(alpha(theme.palette.divider, 1), 0.88); + } + return darken(alpha(theme.palette.divider, 1), 0.68); +} + +const StyledDiv = styled('div')(({ theme: t }) => ({ + position: 'absolute', + zIndex: 10, + fontSize: '0.875em', + top: 0, + height: '100%', + width: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + borderRadius: '4px', + border: `1px solid ${getBorderColor(t)}`, + backgroundColor: t.palette.background.default, +})); + +function ErrorOverlay({ error }) { + if (!error) { + return null; + } + return {error}; +} diff --git a/docs/data/data-grid/server-side-data/ServerSideRowGroupingErrorHandling.tsx b/docs/data/data-grid/server-side-data/ServerSideRowGroupingErrorHandling.tsx new file mode 100644 index 000000000000..621b74b052f4 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideRowGroupingErrorHandling.tsx @@ -0,0 +1,138 @@ +import * as React from 'react'; +import { + DataGridPremium, + GridDataSource, + useGridApiRef, + useKeepGroupedColumnsHidden, +} from '@mui/x-data-grid-premium'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Snackbar from '@mui/material/Snackbar'; +import Button from '@mui/material/Button'; +import Checkbox from '@mui/material/Checkbox'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import { alpha, styled, darken, lighten, Theme } from '@mui/material/styles'; + +export default function ServerSideRowGroupingErrorHandling() { + const apiRef = useGridApiRef(); + const [rootError, setRootError] = React.useState(); + const [childrenError, setChildrenError] = React.useState(); + const [shouldRequestsFail, setShouldRequestsFail] = React.useState(false); + + const { fetchRows, columns } = useMockServer( + { + rowGrouping: true, + }, + {}, + shouldRequestsFail, + ); + + const dataSource: GridDataSource = React.useMemo(() => { + return { + 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), + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + getGroupKey: (row) => row.group, + getChildrenCount: (row) => row.descendantCount, + }; + }, [fetchRows]); + + const initialState = useKeepGroupedColumnsHidden({ + apiRef, + initialState: { + rowGrouping: { + model: ['company', 'director'], + }, + }, + }); + + return ( +
+
+ + setShouldRequestsFail(event.target.checked)} + /> + } + label="Make the requests fail" + /> +
+
+ { + if (!params.groupKeys || params.groupKeys.length === 0) { + setRootError(error.message); + } else { + setChildrenError( + `${error.message} (Requested level: ${params.groupKeys.join(' > ')})`, + ); + } + }} + unstable_dataSourceCache={null} + apiRef={apiRef} + initialState={initialState} + /> + {rootError && } + setChildrenError('')} + message={childrenError} + /> +
+
+ ); +} + +function getBorderColor(theme: Theme) { + if (theme.palette.mode === 'light') { + return lighten(alpha(theme.palette.divider, 1), 0.88); + } + return darken(alpha(theme.palette.divider, 1), 0.68); +} + +const StyledDiv = styled('div')(({ theme: t }) => ({ + position: 'absolute', + zIndex: 10, + fontSize: '0.875em', + top: 0, + height: '100%', + width: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + borderRadius: '4px', + border: `1px solid ${getBorderColor(t)}`, + backgroundColor: t.palette.background.default, +})); + +function ErrorOverlay({ error }: { error: string }) { + if (!error) { + return null; + } + return {error}; +} diff --git a/docs/data/data-grid/server-side-data/ServerSideRowGroupingFullDataGrid.js b/docs/data/data-grid/server-side-data/ServerSideRowGroupingFullDataGrid.js new file mode 100644 index 000000000000..7db1ea09c9c3 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideRowGroupingFullDataGrid.js @@ -0,0 +1,83 @@ +import * as React from 'react'; +import { + DataGridPremium, + useGridApiRef, + useKeepGroupedColumnsHidden, + GridToolbar, +} from '@mui/x-data-grid-premium'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Button from '@mui/material/Button'; + +export default function ServerSideRowGroupingFullDataGrid() { + const apiRef = useGridApiRef(); + + const { fetchRows, columns, loadNewData } = useMockServer({ + rowGrouping: true, + rowLength: 1000, + dataSet: 'Commodity', + maxColumns: 20, + }); + + const dataSource = React.useMemo(() => { + return { + 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), + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + getGroupKey: (row) => row.group, + getChildrenCount: (row) => row.descendantCount, + }; + }, [fetchRows]); + + const initialState = useKeepGroupedColumnsHidden({ + apiRef, + initialState: { + rowGrouping: { + model: ['commodity', 'status'], + }, + columns: { + columnVisibilityModel: { + id: false, + }, + }, + }, + }); + + return ( +
+ + +
+ +
+
+ ); +} diff --git a/docs/data/data-grid/server-side-data/ServerSideRowGroupingFullDataGrid.tsx b/docs/data/data-grid/server-side-data/ServerSideRowGroupingFullDataGrid.tsx new file mode 100644 index 000000000000..4545cf49f9e7 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideRowGroupingFullDataGrid.tsx @@ -0,0 +1,84 @@ +import * as React from 'react'; +import { + DataGridPremium, + GridDataSource, + useGridApiRef, + useKeepGroupedColumnsHidden, + GridToolbar, +} from '@mui/x-data-grid-premium'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Button from '@mui/material/Button'; + +export default function ServerSideRowGroupingFullDataGrid() { + const apiRef = useGridApiRef(); + + const { fetchRows, columns, loadNewData } = useMockServer({ + rowGrouping: true, + rowLength: 1000, + dataSet: 'Commodity', + maxColumns: 20, + }); + + const dataSource: GridDataSource = React.useMemo(() => { + return { + 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), + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + getGroupKey: (row) => row.group, + getChildrenCount: (row) => row.descendantCount, + }; + }, [fetchRows]); + + const initialState = useKeepGroupedColumnsHidden({ + apiRef, + initialState: { + rowGrouping: { + model: ['commodity', 'status'], + }, + columns: { + columnVisibilityModel: { + id: false, + }, + }, + }, + }); + + return ( +
+ + +
+ +
+
+ ); +} diff --git a/docs/data/data-grid/server-side-data/ServerSideRowGroupingGroupExpansion.js b/docs/data/data-grid/server-side-data/ServerSideRowGroupingGroupExpansion.js new file mode 100644 index 000000000000..bdb42747fc07 --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideRowGroupingGroupExpansion.js @@ -0,0 +1,70 @@ +import * as React from 'react'; +import { + DataGridPremium, + useGridApiRef, + useKeepGroupedColumnsHidden, +} from '@mui/x-data-grid-premium'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Button from '@mui/material/Button'; + +export default function ServerSideRowGroupingGroupExpansion() { + const apiRef = useGridApiRef(); + + const { fetchRows, columns } = useMockServer({ + rowGrouping: true, + }); + + const dataSource = React.useMemo(() => { + return { + 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), + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + getGroupKey: (row) => row.group, + getChildrenCount: (row) => row.descendantCount, + }; + }, [fetchRows]); + + const initialState = useKeepGroupedColumnsHidden({ + apiRef, + initialState: { + rowGrouping: { + model: ['company'], + }, + }, + }); + + return ( +
+ + +
+ +
+
+ ); +} diff --git a/docs/data/data-grid/server-side-data/ServerSideRowGroupingGroupExpansion.tsx b/docs/data/data-grid/server-side-data/ServerSideRowGroupingGroupExpansion.tsx new file mode 100644 index 000000000000..64aa58a45d8e --- /dev/null +++ b/docs/data/data-grid/server-side-data/ServerSideRowGroupingGroupExpansion.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import { + DataGridPremium, + GridDataSource, + useGridApiRef, + useKeepGroupedColumnsHidden, +} from '@mui/x-data-grid-premium'; +import { useMockServer } from '@mui/x-data-grid-generator'; +import Button from '@mui/material/Button'; + +export default function ServerSideRowGroupingGroupExpansion() { + const apiRef = useGridApiRef(); + + const { fetchRows, columns } = useMockServer({ + rowGrouping: true, + }); + + const dataSource: GridDataSource = React.useMemo(() => { + return { + 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), + }); + const getRowsResponse = await fetchRows( + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; + }, + getGroupKey: (row) => row.group, + getChildrenCount: (row) => row.descendantCount, + }; + }, [fetchRows]); + + const initialState = useKeepGroupedColumnsHidden({ + apiRef, + initialState: { + rowGrouping: { + model: ['company'], + }, + }, + }); + + return ( +
+ + +
+ +
+
+ ); +} diff --git a/docs/data/data-grid/server-side-data/row-grouping.md b/docs/data/data-grid/server-side-data/row-grouping.md index 537ef8ad0d54..25b6044f3f43 100644 --- a/docs/data/data-grid/server-side-data/row-grouping.md +++ b/docs/data/data-grid/server-side-data/row-grouping.md @@ -2,14 +2,90 @@ title: React Server-side row grouping --- -# Data Grid - Server-side row grouping [](/x/introduction/licensing/#pro-plan 'Pro plan')🚧 +# Data Grid - Server-side row grouping [](/x/introduction/licensing/#pro-plan 'Pro plan')

Lazy-loaded row grouping with server-side data source.

-:::warning -This feature isn't implemented yet. It's coming. +To dynamically load row grouping data from the server, including lazy-loading of children, create a data source and pass the `unstable_dataSource` prop to the Data Grid, as mentioned in the [overview](/x/react-data-grid/server-side-data/) section. + +:::info +If you are looking for row grouping on the client-side, see [client-side row grouping](/x/react-data-grid/row-grouping/). +::: + +Similar to the [tree data](/x/react-data-grid/server-side-data/tree-data/), you need to pass some additional properties to enable the data source row grouping feature: + +- `getGroupKey()`: Returns the group key for the row. +- `getChildrenCount()`: Returns the number of children for the row. If the children count is not available for some reason, but there are some children, returns `-1`. + +```tsx +const customDataSource: GridDataSource = { + getRows: async (params) => { + // Fetch the data from the server + }, + getGroupKey: (row) => { + // Return the group key for the row, e.g. `name` + return row.name; + }, + getChildrenCount: (row) => { + // Return the number of children for the row + return row.childrenCount; + }, +}; +``` -👍 Upvote [issue #10859](https://github.com/mui/mui-x/issues/10859) if you want to see it land faster. +In addition to `groupKeys`, the `getRows()` callback receives a `groupFields` parameter. This corresponds to the current `rowGroupingModel`. Use `groupFields` on the server to group the data for each `getRows()` call. -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. +```tsx +const getRows: async (params) => { + const urlParams = new URLSearchParams({ + // Example: JSON.stringify(['20th Century Fox', 'James Cameron']) + groupKeys: JSON.stringify(params.groupKeys), + // Example: JSON.stringify(['company', 'director']) + groupFields: JSON.stringify(params.groupFields), + }); + const getRowsResponse = await fetchRows( + // Server should group the data based on `groupFields` and + // extract the rows for the nested level based on `groupKeys` + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; +} +``` + +{{"demo": "ServerSideRowGroupingDataGrid.js", "bg": "inline"}} + +:::warning +For complex data, consider using `colDef.groupingValueGetter` to extract the grouping value. This value is passed in the `groupKeys` parameter when `getRows` is called. + +Ensure your backend can interpret the `groupKeys` parameter generated by `colDef.groupingValueGetter` to retrieve grouping values for child rows. ::: + +## Error handling + +If an error occurs during a `getRows` call, the Data Grid displays an error message in the row group cell. `unstable_onDataSourceError` is also triggered with the error and the fetch params. + +This example shows error handling with toast notifications and default error messages in grouping cells. Caching is disabled for simplicity. + +{{"demo": "ServerSideRowGroupingErrorHandling.js", "bg": "inline"}} + +## Group expansion + +The group expansion works similar to the [data source tree data](/x/react-data-grid/server-side-data/tree-data/#group-expansion). +The following demo uses `defaultGroupingExpansionDepth='-1'` to expand all the groups. + +{{"demo": "ServerSideRowGroupingGroupExpansion.js", "bg": "inline"}} + +## Demo + +In the following demo, use the auto generated data based on the `Commodities` dataset to simulate the server-side row grouping. + +{{"demo": "ServerSideRowGroupingFullDataGrid.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/) diff --git a/docs/data/data-grid/server-side-data/tree-data.md b/docs/data/data-grid/server-side-data/tree-data.md index 8e77fe0542bf..d5c725ac456e 100644 --- a/docs/data/data-grid/server-side-data/tree-data.md +++ b/docs/data/data-grid/server-side-data/tree-data.md @@ -8,8 +8,14 @@ title: React Server-side tree data To dynamically load tree data from the server, including lazy-loading of children, you must create a data source and pass the `unstable_dataSource` prop to the Data Grid, as detailed in the [overview section](/x/react-data-grid/server-side-data/). -The data source also requires some additional props to handle tree data, namely `getGroupKey` and `getChildrenCount`. -If the children count is not available for some reason, but there are some children, `getChildrenCount` should return `-1`. +:::info +If you are looking for tree data on the client-side, see [client-side tree data](/x/react-data-grid/tree-data/). +::: + +The data source also requires some additional props to handle tree data: + +- `getGroupKey()`: Returns the group key for the row. +- `getChildrenCount()`: Returns the number of children for the row. If the children count is not available for some reason, but there are some children, returns `-1`. ```tsx const customDataSource: GridDataSource = { @@ -27,6 +33,26 @@ const customDataSource: GridDataSource = { }; ``` +Like the other parameters such as `filterModel`, `sortModel`, and `paginationModel`, the `getRows()` callback receives a `groupKeys` parameter that corresponds to the keys provided for each nested level in `getGroupKey()`. +Use `groupKeys` on the server to extract the rows for a given nested level. + +```tsx +const getRows: async (params) => { + const urlParams = new URLSearchParams({ + // Example: JSON.stringify(['Billy Houston', 'Lora Dean']) + groupKeys: JSON.stringify(params.groupKeys), + }); + const getRowsResponse = await fetchRows( + // Server should extract the rows for the nested level based on `groupKeys` + `https://mui.com/x/api/data-grid?${urlParams.toString()}`, + ); + return { + rows: getRowsResponse.rows, + rowCount: getRowsResponse.rowCount, + }; +} +``` + The following tree data example supports filtering, sorting, and pagination on the server. It also caches the data by default. diff --git a/docs/data/date-pickers/custom-field/DateRangePickerWithButtonField.js b/docs/data/date-pickers/custom-field/DateRangePickerWithButtonField.js deleted file mode 100644 index 7180636a94dd..000000000000 --- a/docs/data/date-pickers/custom-field/DateRangePickerWithButtonField.js +++ /dev/null @@ -1,72 +0,0 @@ -import * as React from 'react'; - -import Button from '@mui/material/Button'; -import useForkRef from '@mui/utils/useForkRef'; - -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { DateRangePicker } from '@mui/x-date-pickers-pro/DateRangePicker'; - -const DateRangeButtonField = React.forwardRef((props, ref) => { - const { - setOpen, - label, - id, - disabled, - InputProps: { ref: containerRef } = {}, - inputProps: { 'aria-label': ariaLabel } = {}, - } = props; - - const handleRef = useForkRef(ref, containerRef); - - return ( - - ); -}); - -DateRangeButtonField.fieldType = 'single-input'; - -const ButtonDateRangePicker = React.forwardRef((props, ref) => { - const [open, setOpen] = React.useState(false); - - return ( - setOpen(false)} - onOpen={() => setOpen(true)} - /> - ); -}); - -export default function DateRangePickerWithButtonField() { - const [value, setValue] = React.useState([null, null]); - - return ( - - (date ? date.format('MM/DD/YYYY') : 'null')) - .join(' - ') - } - value={value} - onChange={(newValue) => setValue(newValue)} - /> - - ); -} diff --git a/docs/data/date-pickers/custom-field/DateRangePickerWithButtonField.tsx b/docs/data/date-pickers/custom-field/DateRangePickerWithButtonField.tsx deleted file mode 100644 index 1b894f55e6a1..000000000000 --- a/docs/data/date-pickers/custom-field/DateRangePickerWithButtonField.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import * as React from 'react'; -import { Dayjs } from 'dayjs'; -import Button from '@mui/material/Button'; -import useForkRef from '@mui/utils/useForkRef'; -import { DateRange, FieldType } from '@mui/x-date-pickers-pro/models'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { - DateRangePicker, - DateRangePickerProps, -} from '@mui/x-date-pickers-pro/DateRangePicker'; -import { SingleInputDateRangeFieldProps } from '@mui/x-date-pickers-pro/SingleInputDateRangeField'; - -interface DateRangeButtonFieldProps extends SingleInputDateRangeFieldProps { - setOpen?: React.Dispatch>; -} - -type DateRangeButtonFieldComponent = (( - props: DateRangeButtonFieldProps & React.RefAttributes, -) => React.JSX.Element) & { fieldType?: FieldType }; - -const DateRangeButtonField = React.forwardRef( - (props: DateRangeButtonFieldProps, ref: React.Ref) => { - const { - setOpen, - label, - id, - disabled, - InputProps: { ref: containerRef } = {}, - inputProps: { 'aria-label': ariaLabel } = {}, - } = props; - - const handleRef = useForkRef(ref, containerRef); - - return ( - - ); - }, -) as DateRangeButtonFieldComponent; - -DateRangeButtonField.fieldType = 'single-input'; - -const ButtonDateRangePicker = React.forwardRef( - ( - props: Omit, 'open' | 'onOpen' | 'onClose'>, - ref: React.Ref, - ) => { - const [open, setOpen] = React.useState(false); - - return ( - setOpen(false)} - onOpen={() => setOpen(true)} - /> - ); - }, -); - -export default function DateRangePickerWithButtonField() { - const [value, setValue] = React.useState>([null, null]); - - return ( - - (date ? date.format('MM/DD/YYYY') : 'null')) - .join(' - ') - } - value={value} - onChange={(newValue) => setValue(newValue)} - /> - - ); -} diff --git a/docs/data/date-pickers/custom-field/DateRangePickerWithButtonField.tsx.preview b/docs/data/date-pickers/custom-field/DateRangePickerWithButtonField.tsx.preview deleted file mode 100644 index 3a8d6daacd70..000000000000 --- a/docs/data/date-pickers/custom-field/DateRangePickerWithButtonField.tsx.preview +++ /dev/null @@ -1,11 +0,0 @@ - (date ? date.format('MM/DD/YYYY') : 'null')) - .join(' - ') - } - value={value} - onChange={(newValue) => setValue(newValue)} -/> \ No newline at end of file diff --git a/docs/data/date-pickers/custom-field/PickerWithButtonField.js b/docs/data/date-pickers/custom-field/PickerWithButtonField.js deleted file mode 100644 index f13ca0315b2b..000000000000 --- a/docs/data/date-pickers/custom-field/PickerWithButtonField.js +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from 'react'; - -import Button from '@mui/material/Button'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { DatePicker } from '@mui/x-date-pickers/DatePicker'; - -function ButtonField(props) { - const { - setOpen, - label, - id, - disabled, - InputProps: { ref } = {}, - inputProps: { 'aria-label': ariaLabel } = {}, - } = props; - - return ( - - ); -} - -function ButtonDatePicker(props) { - const [open, setOpen] = React.useState(false); - - return ( - setOpen(false)} - onOpen={() => setOpen(true)} - /> - ); -} - -export default function PickerWithButtonField() { - const [value, setValue] = React.useState(null); - - return ( - - setValue(newValue)} - /> - - ); -} diff --git a/docs/data/date-pickers/custom-field/PickerWithButtonField.tsx b/docs/data/date-pickers/custom-field/PickerWithButtonField.tsx deleted file mode 100644 index 8acbab77d7c4..000000000000 --- a/docs/data/date-pickers/custom-field/PickerWithButtonField.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import * as React from 'react'; -import { Dayjs } from 'dayjs'; -import Button from '@mui/material/Button'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { DatePicker, DatePickerProps } from '@mui/x-date-pickers/DatePicker'; -import { UseDateFieldProps } from '@mui/x-date-pickers/DateField'; -import { - BaseSingleInputFieldProps, - DateValidationError, - FieldSection, -} from '@mui/x-date-pickers/models'; - -interface ButtonFieldProps - extends UseDateFieldProps, - BaseSingleInputFieldProps< - Dayjs | null, - Dayjs, - FieldSection, - true, - DateValidationError - > { - setOpen?: React.Dispatch>; -} - -function ButtonField(props: ButtonFieldProps) { - const { - setOpen, - label, - id, - disabled, - InputProps: { ref } = {}, - inputProps: { 'aria-label': ariaLabel } = {}, - } = props; - - return ( - - ); -} - -function ButtonDatePicker( - props: Omit, 'open' | 'onOpen' | 'onClose'>, -) { - const [open, setOpen] = React.useState(false); - - return ( - setOpen(false)} - onOpen={() => setOpen(true)} - /> - ); -} - -export default function PickerWithButtonField() { - const [value, setValue] = React.useState(null); - - return ( - - setValue(newValue)} - /> - - ); -} diff --git a/docs/data/date-pickers/custom-field/PickerWithButtonField.tsx.preview b/docs/data/date-pickers/custom-field/PickerWithButtonField.tsx.preview deleted file mode 100644 index 173a0ba16962..000000000000 --- a/docs/data/date-pickers/custom-field/PickerWithButtonField.tsx.preview +++ /dev/null @@ -1,5 +0,0 @@ - setValue(newValue)} -/> \ No newline at end of file diff --git a/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.js b/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.js new file mode 100644 index 000000000000..11da4bc2ba23 --- /dev/null +++ b/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.js @@ -0,0 +1,73 @@ +import * as React from 'react'; + +import Button from '@mui/material/Button'; +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, + usePickersContext, +} from '@mui/x-date-pickers/hooks'; + +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 pickersContext = usePickersContext(); + + const parsedFormat = useParsedFormat(internalProps); + const { hasValidationError } = useValidation({ + validator: validateDate, + value, + timezone, + props: internalProps, + }); + + const handleTogglePicker = (event) => { + if (pickersContext.open) { + pickersContext.onClose(event); + } else { + pickersContext.onOpen(event); + } + }; + + const valueStr = value == null ? parsedFormat : value.format(format); + + return ( + + ); +} + +function ButtonFieldDatePicker(props) { + return ( + + ); +} + +export default function MaterialDatePicker() { + return ( + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.tsx b/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.tsx new file mode 100644 index 000000000000..1f31f5d63e6a --- /dev/null +++ b/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.tsx @@ -0,0 +1,77 @@ +import * as React from 'react'; +import { Dayjs } from 'dayjs'; +import Button from '@mui/material/Button'; +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, + usePickersContext, +} from '@mui/x-date-pickers/hooks'; + +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 pickersContext = usePickersContext(); + + const parsedFormat = useParsedFormat(internalProps); + const { hasValidationError } = useValidation({ + validator: validateDate, + value, + timezone, + props: internalProps, + }); + + const handleTogglePicker = (event: React.UIEvent) => { + if (pickersContext.open) { + pickersContext.onClose(event); + } else { + pickersContext.onOpen(event); + } + }; + + const valueStr = value == null ? parsedFormat : value.format(format); + + return ( + + ); +} + +function ButtonFieldDatePicker(props: DatePickerProps) { + return ( + + ); +} + +export default function MaterialDatePicker() { + return ( + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.tsx.preview b/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.tsx.preview new file mode 100644 index 000000000000..5f578e21a1d6 --- /dev/null +++ b/docs/data/date-pickers/custom-field/behavior-button/MaterialDatePicker.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.js b/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.js new file mode 100644 index 000000000000..f9fc03f45d57 --- /dev/null +++ b/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.js @@ -0,0 +1,82 @@ +import * as React from 'react'; + +import Button from '@mui/material/Button'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateRangePicker } from '@mui/x-date-pickers-pro/DateRangePicker'; +import { useValidation } from '@mui/x-date-pickers/validation'; +import { validateDateRange } from '@mui/x-date-pickers-pro/validation'; +import { + useSplitFieldProps, + useParsedFormat, + usePickersContext, +} from '@mui/x-date-pickers/hooks'; + +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 pickersContext = usePickersContext(); + + const parsedFormat = useParsedFormat(internalProps); + const { hasValidationError } = useValidation({ + validator: validateDateRange, + value, + timezone, + props: internalProps, + }); + + const handleTogglePicker = (event) => { + if (pickersContext.open) { + pickersContext.onClose(event); + } else { + pickersContext.onOpen(event); + } + }; + + const formattedValue = (value ?? [null, null]) + .map((date) => (date == null ? parsedFormat : date.format(format))) + .join(' – '); + + return ( + + ); +} + +// TODO v8: Will be removed before the end of the alpha since single input will become the default field. +ButtonDateRangeField.fieldType = 'single-input'; + +function ButtonFieldDateRangePicker(props) { + return ( + + ); +} + +export default function MaterialDateRangePicker() { + return ( + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.tsx b/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.tsx new file mode 100644 index 000000000000..bc154d9a2b9f --- /dev/null +++ b/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.tsx @@ -0,0 +1,86 @@ +import * as React from 'react'; +import { Dayjs } from 'dayjs'; +import Button from '@mui/material/Button'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { + DateRangePicker, + DateRangePickerProps, + DateRangePickerFieldProps, +} from '@mui/x-date-pickers-pro/DateRangePicker'; +import { useValidation } from '@mui/x-date-pickers/validation'; +import { validateDateRange } from '@mui/x-date-pickers-pro/validation'; +import { + useSplitFieldProps, + useParsedFormat, + usePickersContext, +} from '@mui/x-date-pickers/hooks'; + +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 pickersContext = usePickersContext(); + + const parsedFormat = useParsedFormat(internalProps); + const { hasValidationError } = useValidation({ + validator: validateDateRange, + value, + timezone, + props: internalProps, + }); + + const handleTogglePicker = (event: React.UIEvent) => { + if (pickersContext.open) { + pickersContext.onClose(event); + } else { + pickersContext.onOpen(event); + } + }; + + const formattedValue = (value ?? [null, null]) + .map((date) => (date == null ? parsedFormat : date.format(format))) + .join(' – '); + + return ( + + ); +} + +// TODO v8: Will be removed before the end of the alpha since single input will become the default field. +ButtonDateRangeField.fieldType = 'single-input'; + +function ButtonFieldDateRangePicker(props: DateRangePickerProps) { + return ( + + ); +} + +export default function MaterialDateRangePicker() { + return ( + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.tsx.preview b/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.tsx.preview new file mode 100644 index 000000000000..6512674c0bb6 --- /dev/null +++ b/docs/data/date-pickers/custom-field/behavior-button/MaterialDateRangePicker.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file 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 new file mode 100644 index 000000000000..0723d4565c51 --- /dev/null +++ b/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.js @@ -0,0 +1,164 @@ +import * as React from 'react'; +import dayjs from 'dayjs'; +import { useRifm } from 'rifm'; +import TextField from '@mui/material/TextField'; +import useControlled from '@mui/utils/useControlled'; +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 { useValidation, validateDate } from '@mui/x-date-pickers/validation'; + +const MASK_USER_INPUT_SYMBOL = '_'; +const ACCEPT_REGEX = /[\d]/gi; + +const staticDateWith2DigitTokens = dayjs('2019-11-21T11:30:00.000'); +const staticDateWith1DigitTokens = dayjs('2019-01-01T09:00:00.000'); + +function getValueStrFromValue(value, format) { + if (value == null) { + return ''; + } + + return value.isValid() ? value.format(format) : ''; +} + +function MaskedField(props) { + const { slots, slotProps, ...other } = props; + + const { forwardedProps, internalProps } = useSplitFieldProps(other, 'date'); + + const { + format, + value: valueProp, + defaultValue, + onChange, + timezone, + onError, + } = internalProps; + + const [value, setValue] = useControlled({ + controlled: valueProp, + default: defaultValue ?? null, + name: 'MaskedField', + state: 'value', + }); + + // Control the input text + const [inputValue, setInputValue] = React.useState(() => + getValueStrFromValue(value, format), + ); + + React.useEffect(() => { + if (value && value.isValid()) { + const newDisplayDate = getValueStrFromValue(value, format); + setInputValue(newDisplayDate); + } + }, [format, value]); + + const parsedFormat = useParsedFormat(internalProps); + + const { hasValidationError, getValidationErrorForNewValue } = useValidation({ + value, + timezone, + onError, + props: internalProps, + validator: validateDate, + }); + + const handleValueStrChange = (newValueStr) => { + setInputValue(newValueStr); + + const newValue = dayjs(newValueStr, format); + setValue(newValue); + + if (onChange) { + onChange(newValue, { + validationError: getValidationErrorForNewValue(newValue), + }); + } + }; + + const rifmFormat = React.useMemo(() => { + const formattedDateWith1Digit = staticDateWith1DigitTokens.format(format); + const inferredFormatPatternWith1Digits = formattedDateWith1Digit.replace( + ACCEPT_REGEX, + MASK_USER_INPUT_SYMBOL, + ); + const inferredFormatPatternWith2Digits = staticDateWith2DigitTokens + .format(format) + .replace(ACCEPT_REGEX, '_'); + + if (inferredFormatPatternWith1Digits !== inferredFormatPatternWith2Digits) { + throw new Error( + `Mask does not support numbers with variable length such as 'M'.`, + ); + } + + const maskToUse = inferredFormatPatternWith1Digits; + + return function formatMaskedDate(valueToFormat) { + let outputCharIndex = 0; + return valueToFormat + .split('') + .map((character, characterIndex) => { + ACCEPT_REGEX.lastIndex = 0; + + if (outputCharIndex > maskToUse.length - 1) { + return ''; + } + + const maskChar = maskToUse[outputCharIndex]; + const nextMaskChar = maskToUse[outputCharIndex + 1]; + + const acceptedChar = ACCEPT_REGEX.test(character) ? character : ''; + const formattedChar = + maskChar === MASK_USER_INPUT_SYMBOL + ? acceptedChar + : maskChar + acceptedChar; + + outputCharIndex += formattedChar.length; + + const isLastCharacter = characterIndex === valueToFormat.length - 1; + if ( + isLastCharacter && + nextMaskChar && + nextMaskChar !== MASK_USER_INPUT_SYMBOL + ) { + // when cursor at the end of mask part (e.g. month) prerender next symbol "21" -> "21/" + return formattedChar ? formattedChar + nextMaskChar : ''; + } + + return formattedChar; + }) + .join(''); + }; + }, [format]); + + const rifmProps = useRifm({ + value: inputValue, + onChange: handleValueStrChange, + format: rifmFormat, + }); + + return ( + + ); +} + +function MaskedFieldDatePicker(props) { + return ; +} + +export default function MaskedMaterialTextField() { + return ( + + + + ); +} 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 new file mode 100644 index 000000000000..a589022eada6 --- /dev/null +++ b/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.tsx @@ -0,0 +1,168 @@ +import * as React from 'react'; +import dayjs, { Dayjs } from 'dayjs'; +import { useRifm } from 'rifm'; +import TextField from '@mui/material/TextField'; +import useControlled from '@mui/utils/useControlled'; +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 } from '@mui/x-date-pickers/hooks'; +import { useValidation, validateDate } from '@mui/x-date-pickers/validation'; + +const MASK_USER_INPUT_SYMBOL = '_'; +const ACCEPT_REGEX = /[\d]/gi; + +const staticDateWith2DigitTokens = dayjs('2019-11-21T11:30:00.000'); +const staticDateWith1DigitTokens = dayjs('2019-01-01T09:00:00.000'); + +function getValueStrFromValue(value: Dayjs | null, format: string) { + if (value == null) { + return ''; + } + + return value.isValid() ? value.format(format) : ''; +} + +function MaskedField(props: DatePickerFieldProps) { + const { slots, slotProps, ...other } = props; + + const { forwardedProps, internalProps } = useSplitFieldProps(other, 'date'); + + const { + format, + value: valueProp, + defaultValue, + onChange, + timezone, + onError, + } = internalProps; + + const [value, setValue] = useControlled({ + controlled: valueProp, + default: defaultValue ?? null, + name: 'MaskedField', + state: 'value', + }); + + // Control the input text + const [inputValue, setInputValue] = React.useState(() => + getValueStrFromValue(value, format), + ); + + React.useEffect(() => { + if (value && value.isValid()) { + const newDisplayDate = getValueStrFromValue(value, format); + setInputValue(newDisplayDate); + } + }, [format, value]); + + const parsedFormat = useParsedFormat(internalProps); + + const { hasValidationError, getValidationErrorForNewValue } = useValidation({ + value, + timezone, + onError, + props: internalProps, + validator: validateDate, + }); + + const handleValueStrChange = (newValueStr: string) => { + setInputValue(newValueStr); + + const newValue = dayjs(newValueStr, format); + setValue(newValue); + + if (onChange) { + onChange(newValue, { + validationError: getValidationErrorForNewValue(newValue), + }); + } + }; + + const rifmFormat = React.useMemo(() => { + const formattedDateWith1Digit = staticDateWith1DigitTokens.format(format); + const inferredFormatPatternWith1Digits = formattedDateWith1Digit.replace( + ACCEPT_REGEX, + MASK_USER_INPUT_SYMBOL, + ); + const inferredFormatPatternWith2Digits = staticDateWith2DigitTokens + .format(format) + .replace(ACCEPT_REGEX, '_'); + + if (inferredFormatPatternWith1Digits !== inferredFormatPatternWith2Digits) { + throw new Error( + `Mask does not support numbers with variable length such as 'M'.`, + ); + } + + const maskToUse = inferredFormatPatternWith1Digits; + + return function formatMaskedDate(valueToFormat: string) { + let outputCharIndex = 0; + return valueToFormat + .split('') + .map((character, characterIndex) => { + ACCEPT_REGEX.lastIndex = 0; + + if (outputCharIndex > maskToUse.length - 1) { + return ''; + } + + const maskChar = maskToUse[outputCharIndex]; + const nextMaskChar = maskToUse[outputCharIndex + 1]; + + const acceptedChar = ACCEPT_REGEX.test(character) ? character : ''; + const formattedChar = + maskChar === MASK_USER_INPUT_SYMBOL + ? acceptedChar + : maskChar + acceptedChar; + + outputCharIndex += formattedChar.length; + + const isLastCharacter = characterIndex === valueToFormat.length - 1; + if ( + isLastCharacter && + nextMaskChar && + nextMaskChar !== MASK_USER_INPUT_SYMBOL + ) { + // when cursor at the end of mask part (e.g. month) prerender next symbol "21" -> "21/" + return formattedChar ? formattedChar + nextMaskChar : ''; + } + + return formattedChar; + }) + .join(''); + }; + }, [format]); + + const rifmProps = useRifm({ + value: inputValue, + onChange: handleValueStrChange, + format: rifmFormat, + }); + + return ( + + ); +} + +function MaskedFieldDatePicker(props: DatePickerProps) { + return ; +} + +export default function MaskedMaterialTextField() { + return ( + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.tsx.preview b/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.tsx.preview new file mode 100644 index 000000000000..1340a82dd55d --- /dev/null +++ b/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/custom-field/custom-behavior/ReadOnlyMaterialTextField.js b/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.js similarity index 97% rename from docs/data/date-pickers/custom-field/custom-behavior/ReadOnlyMaterialTextField.js rename to docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.js index b5050747ff1f..5f1cdc46e732 100644 --- a/docs/data/date-pickers/custom-field/custom-behavior/ReadOnlyMaterialTextField.js +++ b/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.js @@ -58,7 +58,7 @@ function ReadOnlyFieldDatePicker(props) { ); } -export default function ReadOnlyMaterialTextField() { +export default function MaterialDatePicker() { return ( diff --git a/docs/data/date-pickers/custom-field/custom-behavior/ReadOnlyMaterialTextField.tsx b/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.tsx similarity index 97% rename from docs/data/date-pickers/custom-field/custom-behavior/ReadOnlyMaterialTextField.tsx rename to docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.tsx index 04223bd73a6e..d8a54a2bebdb 100644 --- a/docs/data/date-pickers/custom-field/custom-behavior/ReadOnlyMaterialTextField.tsx +++ b/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.tsx @@ -62,7 +62,7 @@ function ReadOnlyFieldDatePicker(props: DatePickerProps) { ); } -export default function ReadOnlyMaterialTextField() { +export default function MaterialDatePicker() { return ( diff --git a/docs/data/date-pickers/custom-field/custom-behavior/ReadOnlyMaterialTextField.tsx.preview b/docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.tsx.preview similarity index 100% rename from docs/data/date-pickers/custom-field/custom-behavior/ReadOnlyMaterialTextField.tsx.preview rename to docs/data/date-pickers/custom-field/behavior-read-only-text-field/MaterialDatePicker.tsx.preview diff --git a/docs/data/date-pickers/custom-field/custom-field.md b/docs/data/date-pickers/custom-field/custom-field.md index 36417d5bde6e..31c5ed4bb824 100644 --- a/docs/data/date-pickers/custom-field/custom-field.md +++ b/docs/data/date-pickers/custom-field/custom-field.md @@ -125,30 +125,35 @@ The new accessible DOM structure will become compatible with Joy UI in the futur ## With a custom editing experience -### Using an `Autocomplete` +### Using an Autocomplete -If your user can only select a value in a small list of available dates, -you can replace the field with an `Autocomplete` listing those dates: +If your user can only select a value in a small list of available dates, you can replace the field with the [Autocomplete](/material-ui/react-autocomplete/) component to list those dates: {{"demo": "PickerWithAutocompleteField.js", "defaultCodeOpen": false}} -### Using a read-only `TextField` +### Using a masked Text Field + +If you want to use a simple mask approach for the field editing instead of the built-in logic, you can replace the default field with the [TextField](/material-ui/react-text-field/) component using a masked input value built with the [rifm](https://github.com/realadvisor/rifm) package. + +{{"demo": "behavior-masked-text-field/MaskedMaterialTextField.js", "defaultCodeOpen": false}} + +### Using a read-only Text Field If you want users to select a value exclusively through the views -but you still want the UI to look like a `TextField`, you can replace the field with a read-only `TextField`: +but you still want the UI to look like a Text Field, you can replace the field with a read-only [Text Field](/material-ui/react-text-field/) component: -{{"demo": "custom-behavior/ReadOnlyMaterialTextField.js", "defaultCodeOpen": false}} +{{"demo": "behavior-read-only-text-field/MaterialDatePicker.js", "defaultCodeOpen": false}} -### Using a `Button` +### Using a Button If you want users to select a value exclusively through the views -and you don't want the UI to look like a `TextField`, you can replace the field with a `Button`: +and you don't want the UI to look like a Text Field, you can replace the field with the [Button](/material-ui/react-button/) component: -{{"demo": "PickerWithButtonField.js", "defaultCodeOpen": false}} +{{"demo": "behavior-button/MaterialDatePicker.js", "defaultCodeOpen": false}} -The same can be applied to the `DateRangePicker`: +The same logic can be applied to any Range Picker: -{{"demo": "DateRangePickerWithButtonField.js", "defaultCodeOpen": false}} +{{"demo": "behavior-button/MaterialDateRangePicker.js", "defaultCodeOpen": false}} ## How to build a custom field diff --git a/docs/data/date-pickers/localization/localization.md b/docs/data/date-pickers/localization/localization.md index 9c17517d832a..dab97f4bcb5d 100644 --- a/docs/data/date-pickers/localization/localization.md +++ b/docs/data/date-pickers/localization/localization.md @@ -46,7 +46,7 @@ function App({ children }) { } ``` -Note that `createTheme` accepts any number of arguments. +Note that `createTheme()` accepts any number of arguments. If you are already using the [translations of the core components](/material-ui/guides/localization/#locale-text) or the [translations of the Data Grid](/x/react-data-grid/localization/#locale-text), you can add `deDE` as a new argument. ```jsx @@ -73,7 +73,7 @@ function App({ children }) { ### Using LocalizationProvider -If you want to pass language translations without using `createTheme` and `ThemeProvider`, +If you want to pass language translations without using `createTheme()` and `ThemeProvider`, you can directly load the language translations from the `@mui/x-date-pickers` or `@mui/x-date-pickers-pro` package and pass them to the `LocalizationProvider`. ```jsx diff --git a/docs/data/migration/migration-charts-v7/migration-charts-v7.md b/docs/data/migration/migration-charts-v7/migration-charts-v7.md new file mode 100644 index 000000000000..a23f9406feff --- /dev/null +++ b/docs/data/migration/migration-charts-v7/migration-charts-v7.md @@ -0,0 +1,34 @@ +--- +productId: x-charts +--- + +# Migration from v7 to v8 + +

This guide describes the changes needed to migrate Charts from v7 to v8.

+ +## Introduction + +This is a reference guide for upgrading `@mui/x-charts` from v7 to v8. +The change between v7 and v8 is mostly here to match the version with other MUI X packages. +No big breaking changes are expected. + +## Start using the new release + +In `package.json`, change the version of the charts package to `next`. + +```diff +-"@mui/x-charts": "^7.0.0", ++"@mui/x-charts": "next", +``` + +Using `next` ensures that it will always use the latest v8 pre-release version, but you can also use a fixed version, like `8.0.0-alpha.0`. + +## Breaking changes + +Since v8 is a major release, it contains some changes that affect the public API. +These changes were done for consistency, improve stability and make room for new features. +Below are described the steps you need to make to migrate from v7 to v8. + +:::info +The list is currently empty, but as we move forward with development during the alpha and beta phases, we'll feed this page with all changes in the API. +::: 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 new file mode 100644 index 000000000000..eea548d0fbc0 --- /dev/null +++ b/docs/data/migration/migration-data-grid-v7/migration-data-grid-v7.md @@ -0,0 +1,78 @@ +--- +productId: x-data-grid +--- + +# Migration from v7 to v8 + +

This guide describes the changes needed to migrate the Data Grid from v7 to v8.

+ +## Introduction + +This is a reference guide for upgrading `@mui/x-data-grid` from v7 to v8. + +## Start using the new release + +In `package.json`, change the version of the Data Grid package to `next`. + +```diff +-"@mui/x-data-grid": "^7.0.0", ++"@mui/x-data-grid": "next", + +-"@mui/x-data-grid-pro": "^7.0.0", ++"@mui/x-data-grid-pro": "next", + +-"@mui/x-data-grid-premium": "^7.0.0", ++"@mui/x-data-grid-premium": "next", +``` + +Using `next` ensures that it will always use the latest v8 pre-release version, but you can also use a fixed version, like `8.0.0-alpha.0`. + +## Breaking changes + +Since v8 is a major release, it contains some changes that affect the public API. +These changes were done for consistency, improve stability and make room for new features. +Below are described the steps you need to make to migrate from v7 to v8. + +:::info +The list is currently empty, but as we move forward with development during the alpha and beta phases, we'll feed this page with all changes in the API. +::: + + diff --git a/docs/data/migration/migration-pickers-v7/migration-pickers-v7.md b/docs/data/migration/migration-pickers-v7/migration-pickers-v7.md index c5a7a72d92f3..18a1f57af80f 100644 --- a/docs/data/migration/migration-pickers-v7/migration-pickers-v7.md +++ b/docs/data/migration/migration-pickers-v7/migration-pickers-v7.md @@ -19,6 +19,8 @@ In `package.json`, change the version of the date pickers package to `next`. +"@mui/x-date-pickers": "next", ``` +Using `next` ensures that it will always use the latest v8 pre-release version, but you can also use a fixed version, like `8.0.0-alpha.0`. + Since `v8` is a major release, it contains changes that affect the public API. These changes were done for consistency, improved stability and to make room for new features. Described below are the steps needed to migrate from v7 to v8. @@ -256,3 +258,59 @@ const theme = createTheme({ }, }); ``` + +## Removed types + +The following types are no longer exported by `@mui/x-date-pickers` and/or `@mui/x-date-pickers-pro`. +If you were using them, you need to replace them with the following code: + +- `UseDateFieldComponentProps` + + ```ts + import { UseDateFieldProps } from '@mui/x-date-pickers/DateField'; + import { PickerValidDate } from '@mui/x-date-pickers/models'; + + type UseDateFieldComponentProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, + TChildProps extends {}, + > = Omit< + TChildProps, + keyof UseDateFieldProps + > & + UseDateFieldProps; + ``` + +- `UseTimeFieldComponentProps` + + ```ts + import { UseTimeFieldProps } from '@mui/x-date-pickers/TimeField'; + import { PickerValidDate } from '@mui/x-date-pickers/models'; + + type UseTimeFieldComponentProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, + TChildProps extends {}, + > = Omit< + TChildProps, + keyof UseTimeFieldProps + > & + UseTimeFieldProps; + ``` + +- `UseDateTimeFieldComponentProps` + + ```ts + import { UseDateTimeFieldProps } from '@mui/x-date-pickers/DateTimeField'; + import { PickerValidDate } from '@mui/x-date-pickers/models'; + + type UseDateTimeFieldComponentProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean, + TChildProps extends {}, + > = Omit< + TChildProps, + keyof UseDateTimeFieldProps + > & + UseDateTimeFieldProps; + ``` diff --git a/docs/data/migration/migration-tree-view-v7/migration-tree-view-v7.md b/docs/data/migration/migration-tree-view-v7/migration-tree-view-v7.md index fe24aa3d566d..b8e283448299 100644 --- a/docs/data/migration/migration-tree-view-v7/migration-tree-view-v7.md +++ b/docs/data/migration/migration-tree-view-v7/migration-tree-view-v7.md @@ -19,6 +19,8 @@ In `package.json`, change the version of the Tree View package to `next`. +"@mui/x-tree-view": "next", ``` +Using `next` ensures that it will always use the latest v8 pre-release version, but you can also use a fixed version, like `8.0.0-alpha.0`. + Since `v8` is a major release, it contains changes that affect the public API. These changes were done for consistency, improved stability and to make room for new features. Described below are the steps needed to migrate from v7 to v8. diff --git a/docs/data/pages.ts b/docs/data/pages.ts index 0315101ba449..361b46837c94 100644 --- a/docs/data/pages.ts +++ b/docs/data/pages.ts @@ -120,8 +120,9 @@ const pages: MuiPage[] = [ pathname: '/x/react-data-grid/server-side-data-group', title: 'Server-side data', plan: 'pro', + newFeature: true, children: [ - { pathname: '/x/react-data-grid/server-side-data', title: 'Overview' }, + { pathname: '/x/react-data-grid/server-side-data', title: 'Overview', plan: 'pro' }, { pathname: '/x/react-data-grid/server-side-data/tree-data', plan: 'pro' }, { pathname: '/x/react-data-grid/server-side-data/lazy-loading', @@ -135,8 +136,7 @@ const pages: MuiPage[] = [ }, { pathname: '/x/react-data-grid/server-side-data/row-grouping', - plan: 'pro', - planned: true, + plan: 'premium', }, { pathname: '/x/react-data-grid/server-side-data/aggregation', @@ -541,6 +541,7 @@ const pages: MuiPage[] = [ pathname: '/x/migration-v8', subheader: 'Upgrade to v8', children: [ + { pathname: '/x/migration/migration-data-grid-v7', title: 'Breaking changes: Data Grid' }, { pathname: '/x/migration/migration-pickers-v7', title: 'Breaking changes: Date and Time Pickers', @@ -549,6 +550,10 @@ const pages: MuiPage[] = [ pathname: '/x/migration/migration-tree-view-v7', title: 'Breaking changes: Tree View', }, + { + pathname: '/x/migration/migration-charts-v7', + title: 'Breaking changes: Charts', + }, ], }, { diff --git a/docs/data/tree-view/rich-tree-view/focus/focus.md b/docs/data/tree-view/rich-tree-view/focus/focus.md index a293e75b0d20..9a23786173ea 100644 --- a/docs/data/tree-view/rich-tree-view/focus/focus.md +++ b/docs/data/tree-view/rich-tree-view/focus/focus.md @@ -41,7 +41,7 @@ apiRef.current.focusItem( :::info This method only works with items that are currently visible. -Calling `apiRef.focusItem` on an item whose parent is collapsed will do nothing. +Calling `apiRef.focusItem()` on an item whose parent is collapsed does nothing. ::: {{"demo": "ApiMethodFocusItem.js"}} diff --git a/docs/data/tree-view/rich-tree-view/items/items.md b/docs/data/tree-view/rich-tree-view/items/items.md index 964ee3b266f2..49db9da0a4e2 100644 --- a/docs/data/tree-view/rich-tree-view/items/items.md +++ b/docs/data/tree-view/rich-tree-view/items/items.md @@ -179,7 +179,7 @@ const item = apiRef.current.getItem( ### Get an item's DOM element by ID -Use the `getItemDOMElement` API method to get an item's DOM element by its ID. +Use the `getItemDOMElement()` API method to get an item's DOM element by its ID. ```ts const itemElement = apiRef.current.getItemDOMElement( diff --git a/docs/data/tree-view/rich-tree-view/selection/selection.md b/docs/data/tree-view/rich-tree-view/selection/selection.md index 6012a68e78d6..02036050d502 100644 --- a/docs/data/tree-view/rich-tree-view/selection/selection.md +++ b/docs/data/tree-view/rich-tree-view/selection/selection.md @@ -117,13 +117,13 @@ const apiRef = useTreeViewApiRef(); return ; ``` -When your component first renders, `apiRef` will be `undefined`. +When your component first renders, `apiRef` is `undefined`. After this initial render, `apiRef` holds methods to interact imperatively with the Tree View. ::: ### Select or deselect an item -Use the `selectItem` API method to select or deselect an item: +Use the `selectItem()` API method to select or deselect an item: ```ts apiRef.current.selectItem({ diff --git a/docs/data/tree-view/simple-tree-view/focus/focus.md b/docs/data/tree-view/simple-tree-view/focus/focus.md index ef9cd7d5558e..8cc4576f617e 100644 --- a/docs/data/tree-view/simple-tree-view/focus/focus.md +++ b/docs/data/tree-view/simple-tree-view/focus/focus.md @@ -41,7 +41,7 @@ apiRef.current.focusItem( :::info This method only works with items that are currently visible. -Calling `apiRef.focusItem` on an item whose parent is collapsed will do nothing. +Calling `apiRef.focusItem()` on an item whose parent is collapsed does nothing. ::: {{"demo": "ApiMethodFocusItem.js"}} diff --git a/docs/data/tree-view/simple-tree-view/items/items.md b/docs/data/tree-view/simple-tree-view/items/items.md index 6c1bc4f06e64..cd516f83665a 100644 --- a/docs/data/tree-view/simple-tree-view/items/items.md +++ b/docs/data/tree-view/simple-tree-view/items/items.md @@ -91,13 +91,13 @@ const apiRef = useTreeViewApiRef(); return {children}; ``` -When your component first renders, `apiRef` will be `undefined`. +When your component first renders, `apiRef` is `undefined`. After this initial render, `apiRef` holds methods to interact imperatively with the Tree View. ::: ### Get an item's DOM element by ID -Use the `getItemDOMElement` API method to get an item's DOM element by its ID. +Use the `getItemDOMElement()` API method to get an item's DOM element by its ID. ```ts const itemElement = apiRef.current.getItemDOMElement( diff --git a/docs/data/tree-view/simple-tree-view/selection/selection.md b/docs/data/tree-view/simple-tree-view/selection/selection.md index aceb02cb7215..c9f44a4e63ef 100644 --- a/docs/data/tree-view/simple-tree-view/selection/selection.md +++ b/docs/data/tree-view/simple-tree-view/selection/selection.md @@ -91,7 +91,7 @@ After this initial render, `apiRef` holds methods to interact imperatively with ### Select or deselect an item -Use the `selectItem` API method to select or deselect an item: +Use the `selectItem()` API method to select or deselect an item: ```ts apiRef.current.selectItem({ diff --git a/docs/package.json b/docs/package.json index 45bdb6951efb..7c1d665c25d3 100644 --- a/docs/package.json +++ b/docs/package.json @@ -91,6 +91,7 @@ "react-runner": "^1.0.5", "react-simple-code-editor": "^0.14.1", "recast": "^0.23.9", + "rifm": "0.12.1", "rimraf": "^6.0.1", "rxjs": "^7.8.1", "styled-components": "^6.1.13", diff --git a/docs/pages/x/migration/migration-charts-v7.js b/docs/pages/x/migration/migration-charts-v7.js new file mode 100644 index 000000000000..09b47f90866e --- /dev/null +++ b/docs/pages/x/migration/migration-charts-v7.js @@ -0,0 +1,7 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import * as pageProps from 'docsx/data/migration/migration-charts-v7/migration-charts-v7.md?muiMarkdown'; + +export default function Page() { + return ; +} diff --git a/docs/pages/x/migration/migration-data-grid-v7.js b/docs/pages/x/migration/migration-data-grid-v7.js new file mode 100644 index 000000000000..454b6a6e813e --- /dev/null +++ b/docs/pages/x/migration/migration-data-grid-v7.js @@ -0,0 +1,7 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import * as pageProps from 'docsx/data/migration/migration-data-grid-v7/migration-data-grid-v7.md?muiMarkdown'; + +export default function Page() { + return ; +} diff --git a/packages/x-charts/src/ChartsLegend/DefaultChartsLegend.tsx b/packages/x-charts/src/ChartsLegend/DefaultChartsLegend.tsx index 7278a105536f..24d575ca0095 100644 --- a/packages/x-charts/src/ChartsLegend/DefaultChartsLegend.tsx +++ b/packages/x-charts/src/ChartsLegend/DefaultChartsLegend.tsx @@ -20,7 +20,7 @@ export interface LegendRendererProps series: FormattedSeries; seriesToDisplay: LegendPerItemProps['itemsToDisplay']; /** - * @deprecated Use the `useDrawingArea` hook instead. + * @deprecated Use the `useDrawingArea()` hook instead. */ drawingArea: Omit; /** @@ -76,7 +76,7 @@ DefaultChartsLegend.propTypes = { */ direction: PropTypes.oneOf(['column', 'row']).isRequired, /** - * @deprecated Use the `useDrawingArea` hook instead. + * @deprecated Use the `useDrawingArea()` hook instead. */ drawingArea: PropTypes.shape({ bottom: PropTypes.number.isRequired, diff --git a/packages/x-charts/src/ChartsTooltip/ChartsTooltip.tsx b/packages/x-charts/src/ChartsTooltip/ChartsTooltip.tsx index 4566eddb96d6..51c770bbb827 100644 --- a/packages/x-charts/src/ChartsTooltip/ChartsTooltip.tsx +++ b/packages/x-charts/src/ChartsTooltip/ChartsTooltip.tsx @@ -11,11 +11,13 @@ import { InteractionContext, ItemInteractionData, } from '../context/InteractionProvider'; +import { useSvgRef } from '../hooks/useSvgRef'; import { generateVirtualElement, - useMouseTracker, getTooltipHasData, TriggerOptions, + usePointerType, + VirtualElement, } from './utils'; import { ChartSeriesType } from '../models/seriesType/config'; import { ChartsItemContentProps, ChartsItemTooltipContent } from './ChartsItemTooltipContent'; @@ -133,14 +135,22 @@ function ChartsTooltip(inProps: ChartsTooltipProps }); const { trigger = 'axis', itemContent, axisContent, slots, slotProps } = props; - const mousePosition = useMouseTracker(); + const svgRef = useSvgRef(); + const pointerType = usePointerType(); + + const popperRef: PopperProps['popperRef'] = React.useRef(null); + + const virtualElement = React.useRef(null); + if (virtualElement.current === null) { + virtualElement.current = generateVirtualElement(null); + } const { item, axis } = React.useContext(InteractionContext); const displayedData = trigger === 'item' ? item : axis; const tooltipHasData = getTooltipHasData(trigger, displayedData); - const popperOpen = mousePosition !== null && tooltipHasData; + const popperOpen = pointerType !== null && tooltipHasData; const classes = useUtilityClasses({ classes: props.classes }); @@ -150,14 +160,14 @@ function ChartsTooltip(inProps: ChartsTooltipProps externalSlotProps: slotProps?.popper, additionalProps: { open: popperOpen, - placement: - mousePosition?.pointerType === 'mouse' ? ('right-start' as const) : ('top' as const), - anchorEl: generateVirtualElement(mousePosition), + placement: pointerType?.pointerType === 'mouse' ? ('right-start' as const) : ('top' as const), + popperRef, + anchorEl: virtualElement.current, modifiers: [ { name: 'offset', options: { - offset: [0, mousePosition?.pointerType === 'touch' ? 40 - mousePosition.height : 0], + offset: [0, pointerType?.pointerType === 'touch' ? 40 - pointerType.height : 0], }, }, ], @@ -165,6 +175,26 @@ function ChartsTooltip(inProps: ChartsTooltipProps ownerState: {}, }); + React.useEffect(() => { + const element = svgRef.current; + if (element === null) { + return () => {}; + } + + const handleMove = (event: PointerEvent) => { + virtualElement.current = generateVirtualElement({ + x: event.clientX, + y: event.clientY, + }); + popperRef.current?.update(); + }; + element.addEventListener('pointermove', handleMove); + + return () => { + element.removeEventListener('pointermove', handleMove); + }; + }, [svgRef]); + if (trigger === 'none') { return null; } diff --git a/packages/x-charts/src/ChartsTooltip/contentDisplayed.test.tsx b/packages/x-charts/src/ChartsTooltip/contentDisplayed.test.tsx index 6b4634241e3d..bde48ebc9f35 100644 --- a/packages/x-charts/src/ChartsTooltip/contentDisplayed.test.tsx +++ b/packages/x-charts/src/ChartsTooltip/contentDisplayed.test.tsx @@ -54,6 +54,7 @@ describe('ChartsTooltip', () => { ); const svg = document.querySelector('svg')!; + fireEvent.pointerEnter(svg); // Trigger the tooltip firePointerEvent(svg, 'pointermove', { clientX: 198, clientY: 60, @@ -120,6 +121,7 @@ describe('ChartsTooltip', () => { ); const svg = document.querySelector('svg')!; + fireEvent.pointerEnter(svg); // Trigger the tooltip firePointerEvent(svg, 'pointermove', { clientX: 150, clientY: 60, @@ -191,6 +193,7 @@ describe('ChartsTooltip', () => { fireEvent.pointerEnter(rectangles[0]); + fireEvent.pointerEnter(svg); // Trigger the tooltip firePointerEvent(svg, 'pointermove', { clientX: 150, clientY: 60, @@ -235,6 +238,7 @@ describe('ChartsTooltip', () => { fireEvent.pointerEnter(rectangles[0]); + fireEvent.pointerEnter(svg); // Trigger the tooltip firePointerEvent(svg, 'pointermove', { clientX: 150, clientY: 60, diff --git a/packages/x-charts/src/ChartsTooltip/utils.tsx b/packages/x-charts/src/ChartsTooltip/utils.tsx index 89faff2a7b17..24e2e2220391 100644 --- a/packages/x-charts/src/ChartsTooltip/utils.tsx +++ b/packages/x-charts/src/ChartsTooltip/utils.tsx @@ -10,8 +10,28 @@ type MousePosition = { height: number; }; -export function generateVirtualElement(mousePosition: MousePosition | null) { - if (mousePosition === null) { +export type VirtualElement = { + getBoundingClientRect: () => { + width: number; + height: number; + x: number; + y: number; + top: number; + right: number; + bottom: number; + left: number; + toJSON: () => string; + }; +}; +/** + * Generate a virtual element for the tooltip. + * Default to (0, 0) is the argument is not provided, or null. + * @param mousePosition { x: number, y: number} + */ +export function generateVirtualElement( + mousePosition?: Pick | null, +): VirtualElement { + if (!mousePosition) { return { getBoundingClientRect: () => ({ width: 0, @@ -47,6 +67,9 @@ export function generateVirtualElement(mousePosition: MousePosition | null) { export type UseMouseTrackerReturnValue = null | MousePosition; +/** + * @deprecated We recommend using vanilla JS to let popper track mouse position. + */ export function useMouseTracker(): UseMouseTrackerReturnValue { const svgRef = useSvgRef(); @@ -59,6 +82,8 @@ export function useMouseTracker(): UseMouseTrackerReturnValue { return () => {}; } + const controller = new AbortController(); + const handleOut = (event: PointerEvent) => { if (event.pointerType !== 'mouse') { setMousePosition(null); @@ -74,18 +99,57 @@ export function useMouseTracker(): UseMouseTrackerReturnValue { }); }; - element.addEventListener('pointerdown', handleMove); - element.addEventListener('pointermove', handleMove); + element.addEventListener('pointerdown', handleMove, { signal: controller.signal }); + element.addEventListener('pointermove', handleMove, { signal: controller.signal }); + element.addEventListener('pointerup', handleOut, { signal: controller.signal }); + + return () => { + // Calling `.abort()` removes ALL event listeners + // For more info, see https://kettanaito.com/blog/dont-sleep-on-abort-controller + controller.abort(); + }; + }, [svgRef]); + + return mousePosition; +} + +type PointerType = Pick; + +export function usePointerType(): null | PointerType { + const svgRef = useSvgRef(); + + // Use a ref to avoid rerendering on every mousemove event. + const [pointerType, setPointerType] = React.useState(null); + + React.useEffect(() => { + const element = svgRef.current; + if (element === null) { + return () => {}; + } + + const handleOut = (event: PointerEvent) => { + if (event.pointerType !== 'mouse') { + setPointerType(null); + } + }; + + const handleEnter = (event: PointerEvent) => { + setPointerType({ + height: event.height, + pointerType: event.pointerType as PointerType['pointerType'], + }); + }; + + element.addEventListener('pointerenter', handleEnter); element.addEventListener('pointerup', handleOut); return () => { - element.removeEventListener('pointerdown', handleMove); - element.removeEventListener('pointermove', handleMove); + element.removeEventListener('pointerenter', handleEnter); element.removeEventListener('pointerup', handleOut); }; }, [svgRef]); - return mousePosition; + return pointerType; } export type TriggerOptions = 'item' | 'axis' | 'none'; diff --git a/packages/x-data-grid-generator/src/hooks/index.ts b/packages/x-data-grid-generator/src/hooks/index.ts index 84dd7368aea4..223d6170c94b 100644 --- a/packages/x-data-grid-generator/src/hooks/index.ts +++ b/packages/x-data-grid-generator/src/hooks/index.ts @@ -1,6 +1,7 @@ export * from './useDemoData'; export * from './useBasicDemoData'; -export * from './useMovieData'; +export { useMovieData } from './useMovieData'; +export type { Movie } from './useMovieData'; export * from './useQuery'; export * from './useMockServer'; export { loadServerRows } from './serverUtils'; diff --git a/packages/x-data-grid-generator/src/hooks/serverUtils.ts b/packages/x-data-grid-generator/src/hooks/serverUtils.ts index 095893126643..80651b133af5 100644 --- a/packages/x-data-grid-generator/src/hooks/serverUtils.ts +++ b/packages/x-data-grid-generator/src/hooks/serverUtils.ts @@ -10,7 +10,6 @@ import { GridValidRowModel, } from '@mui/x-data-grid-pro'; import { GridStateColDef } from '@mui/x-data-grid-pro/internals'; -import { UseDemoDataOptions } from './useDemoData'; import { randomInt } from '../services/random-generator'; export interface FakeServerResponse { @@ -53,14 +52,9 @@ export interface ServerSideQueryOptions { sortModel?: GridSortModel; firstRowToRender?: number; lastRowToRender?: number; + groupFields?: string[]; } -export const DEFAULT_DATASET_OPTIONS: UseDemoDataOptions = { - dataSet: 'Commodity', - rowLength: 100, - maxColumns: 6, -}; - declare const DISABLE_CHANCE_RANDOM: any; export const disableDelay = typeof DISABLE_CHANCE_RANDOM !== 'undefined' && DISABLE_CHANCE_RANDOM; @@ -323,7 +317,7 @@ export const loadServerRows = ( }); }; -interface ProcessTreeDataRowsResponse { +interface NestedDataRowsResponse { rows: GridRowModel[]; rootRowCount: number; } @@ -333,6 +327,7 @@ const findTreeDataRowChildren = ( parentPath: string[], pathKey: string = 'path', depth: number = 1, // the depth of the children to find relative to parentDepth, `-1` to find all + rowQualifier?: (row: GridRowModel) => boolean, ) => { const parentDepth = parentPath.length; const children = []; @@ -346,7 +341,9 @@ const findTreeDataRowChildren = ( ((depth < 0 && rowPath.length > parentDepth) || rowPath.length === parentDepth + depth) && parentPath.every((value, index) => value === rowPath[index]) ) { - children.push(row); + if (!rowQualifier || rowQualifier(row)) { + children.push(row); + } } } return children; @@ -427,14 +424,14 @@ const getTreeDataFilteredRows: GetTreeDataFilteredRows = ( }; /** - * Simulates server data loading + * Simulates server data for tree-data feature */ export const processTreeDataRows = ( rows: GridRowModel[], queryOptions: ServerSideQueryOptions, serverOptions: ServerOptions, columnsWithDefaultColDef: GridColDef[], -): Promise => { +): Promise => { const { minDelay = 100, maxDelay = 300 } = serverOptions; const pathKey = 'path'; // TODO: Support filtering and cursor based pagination @@ -490,3 +487,124 @@ export const processTreeDataRows = ( }, delay); // simulate network latency }); }; + +/** + * Simulates server data for row grouping feature + */ +export const processRowGroupingRows = ( + rows: GridValidRowModel[], + queryOptions: ServerSideQueryOptions, + serverOptions: ServerOptions, + columnsWithDefaultColDef: GridColDef[], +): Promise => { + const { minDelay = 100, maxDelay = 300 } = serverOptions; + const pathKey = 'path'; + + if (maxDelay < minDelay) { + throw new Error('serverOptions.minDelay is larger than serverOptions.maxDelay '); + } + + if (queryOptions.groupKeys == null) { + throw new Error('serverOptions.groupKeys must be defined to compute row grouping data'); + } + + if (queryOptions.groupFields == null) { + throw new Error('serverOptions.groupFields must be defined to compute row grouping data'); + } + + const delay = randomInt(minDelay, maxDelay); + + const pathsToAutogenerate = new Set(); + let rowsWithPaths = rows; + const rowsWithMissingGroups: GridValidRowModel[] = []; + + // add paths and generate parent rows based on `groupFields` + const groupFields = queryOptions.groupFields; + if (groupFields.length > 0) { + rowsWithPaths = rows.reduce((acc, row) => { + const partialPath = groupFields.map((field) => String(row[field])); + for (let index = 0; index < partialPath.length; index += 1) { + const value = partialPath[index]; + if (value === undefined) { + if (index === 0) { + rowsWithMissingGroups.push({ ...row, group: false }); + } + return acc; + } + const parentPath = partialPath.slice(0, index + 1); + const strigifiedPath = parentPath.join(','); + if (!pathsToAutogenerate.has(strigifiedPath)) { + pathsToAutogenerate.add(strigifiedPath); + } + } + acc.push({ ...row, path: [...partialPath, ''] }); + return acc; + }, []); + } else { + rowsWithPaths = rows.map((row) => ({ ...row, path: [''] })); + } + + const autogeneratedRows = Array.from(pathsToAutogenerate).map((path) => { + const pathArray = path.split(','); + return { + id: `auto-generated-parent-${pathArray.join('-')}`, + path: pathArray.slice(0, pathArray.length), + group: pathArray.slice(-1)[0], + }; + }); + + // apply plain filtering + const filteredRows = getTreeDataFilteredRows( + [...autogeneratedRows, ...rowsWithPaths, ...rowsWithMissingGroups], + queryOptions.filterModel, + columnsWithDefaultColDef, + ) as GridValidRowModel[]; + + // get root row count + const rootRows = findTreeDataRowChildren(filteredRows, []); + const rootRowCount = rootRows.length; + + let filteredRowsWithMissingGroups: GridValidRowModel[] = []; + let childRows = rootRows; + if (queryOptions.groupKeys.length === 0) { + filteredRowsWithMissingGroups = filteredRows.filter(({ group }) => group === false); + } else { + childRows = findTreeDataRowChildren(filteredRows, queryOptions.groupKeys); + } + + let childRowsWithDescendantCounts = childRows.map((row) => { + const descendants = findTreeDataRowChildren( + filteredRows, + row[pathKey], + pathKey, + -1, + ({ id }) => typeof id !== 'string' || !id.startsWith('auto-generated-parent-'), + ); + const descendantCount = descendants.length; + return { ...row, descendantCount } as GridRowModel; + }); + + if (queryOptions.sortModel) { + const rowComparator = getRowComparator(queryOptions.sortModel, columnsWithDefaultColDef); + const sortedMissingGroups = [...filteredRowsWithMissingGroups].sort(rowComparator); + const sortedChildRows = [...childRowsWithDescendantCounts].sort(rowComparator); + childRowsWithDescendantCounts = [...sortedMissingGroups, ...sortedChildRows]; + } + + if (queryOptions.paginationModel && queryOptions.groupKeys.length === 0) { + // Only paginate root rows, grid should refetch root rows when `paginationModel` updates + const { pageSize, page } = queryOptions.paginationModel; + if (pageSize < childRowsWithDescendantCounts.length) { + childRowsWithDescendantCounts = childRowsWithDescendantCounts.slice( + page * pageSize, + (page + 1) * pageSize, + ); + } + } + + return new Promise((resolve) => { + setTimeout(() => { + resolve({ rows: childRowsWithDescendantCounts, rootRowCount }); + }, 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 83c43fd30c05..e9c168f0ca08 100644 --- a/packages/x-data-grid-generator/src/hooks/useMockServer.ts +++ b/packages/x-data-grid-generator/src/hooks/useMockServer.ts @@ -9,23 +9,24 @@ import { GridInitialState, GridColumnVisibilityModel, } from '@mui/x-data-grid-pro'; -import { - UseDemoDataOptions, - getColumnsFromOptions, - extrapolateSeed, - deepFreeze, -} from './useDemoData'; +import { extrapolateSeed, deepFreeze } from './useDemoData'; +import { getCommodityColumns } from '../columns/commodities.columns'; +import { getEmployeeColumns } from '../columns/employees.columns'; import { GridColDefGenerator } from '../services/gridColDefGenerator'; import { getRealGridData, GridDemoData } from '../services/real-data-service'; -import { addTreeDataOptionsToDemoData } from '../services/tree-data-generator'; +import { + addTreeDataOptionsToDemoData, + AddPathToDemoDataOptions, +} from '../services/tree-data-generator'; import { loadServerRows, processTreeDataRows, - DEFAULT_DATASET_OPTIONS, + processRowGroupingRows, DEFAULT_SERVER_OPTIONS, } from './serverUtils'; import type { ServerOptions } from './serverUtils'; import { randomInt } from '../services'; +import { getMovieRows, getMovieColumns } from './useMovieData'; const dataCache = new LRUCache({ max: 10, @@ -43,6 +44,66 @@ type UseMockServerResponse = { loadNewData: () => void; }; +type DataSet = 'Commodity' | 'Employee' | 'Movies'; + +interface UseMockServerOptions { + dataSet: DataSet; + /** + * Has no effect when DataSet='Movies' + */ + rowLength: number; + maxColumns?: number; + visibleFields?: string[]; + editable?: boolean; + treeData?: AddPathToDemoDataOptions; + rowGrouping?: boolean; +} + +interface GridMockServerData { + rows: GridRowModel[]; + columns: GridColDefGenerator[] | GridColDef[]; + initialState?: GridInitialState; +} + +interface ColumnsOptions + extends Pick {} + +const GET_DEFAULT_DATASET_OPTIONS: (isRowGrouping: boolean) => UseMockServerOptions = ( + isRowGrouping, +) => ({ + dataSet: isRowGrouping ? 'Movies' : 'Commodity', + rowLength: isRowGrouping ? getMovieRows().length : 100, + maxColumns: 6, +}); + +const getColumnsFromOptions = (options: ColumnsOptions): GridColDefGenerator[] | GridColDef[] => { + let columns; + + switch (options.dataSet) { + case 'Commodity': + columns = getCommodityColumns(options.editable); + break; + case 'Employee': + columns = getEmployeeColumns(); + break; + case 'Movies': + columns = getMovieColumns(); + break; + default: + throw new Error('Unknown dataset'); + } + + if (options.visibleFields) { + columns = columns.map((col) => + options.visibleFields?.includes(col.field) ? col : { ...col, hide: true }, + ); + } + if (options.maxColumns) { + columns = columns.slice(0, options.maxColumns); + } + return columns; +}; + function decodeParams(url: string): GridGetRowsParams { const params = new URL(url).searchParams; const decodedParams = {} as any; @@ -76,12 +137,18 @@ const getInitialState = (columns: GridColDefGenerator[], groupingField?: string) const defaultColDef = getGridDefaultColumnTypes(); +function sendEmptyResponse() { + return new Promise((resolve) => { + resolve({ rows: [], rowCount: 0 }); + }); +} + export const useMockServer = ( - dataSetOptions?: Partial, + dataSetOptions?: Partial, serverOptions?: ServerOptions & { verbose?: boolean }, shouldRequestsFail?: boolean, ): UseMockServerResponse => { - const [data, setData] = React.useState(); + const [data, setData] = React.useState(); const [index, setIndex] = React.useState(0); const shouldRequestsFailRef = React.useRef(shouldRequestsFail ?? false); @@ -91,7 +158,11 @@ export const useMockServer = ( } }, [shouldRequestsFail]); - const options = { ...DEFAULT_DATASET_OPTIONS, ...dataSetOptions }; + const isRowGrouping = dataSetOptions?.rowGrouping ?? false; + + const options = { ...GET_DEFAULT_DATASET_OPTIONS(isRowGrouping), ...dataSetOptions }; + + const isTreeData = options.treeData?.groupingField != null; const columns = React.useMemo(() => { return getColumnsFromOptions({ @@ -116,8 +187,6 @@ export const useMockServer = ( [columns], ); - const isTreeData = options.treeData?.groupingField != null; - const getGroupKey = React.useMemo(() => { if (isTreeData) { return (row: GridRowModel): string => row[options.treeData!.groupingField!]; @@ -144,6 +213,13 @@ export const useMockServer = ( return undefined; } + if (options.dataSet === 'Movies') { + const rowsData = { rows: getMovieRows(), columns }; + setData(rowsData); + dataCache.set(cacheKey, rowsData); + return undefined; + } + let active = true; (async () => { @@ -193,10 +269,8 @@ export const useMockServer = ( const fetchRows = React.useCallback( async (requestUrl: string): Promise => { - if (!data || !requestUrl) { - return new Promise((resolve) => { - resolve({ rows: [], rowCount: 0 }); - }); + if (!requestUrl || !data?.rows) { + return sendEmptyResponse(); } const params = decodeParams(requestUrl); const verbose = serverOptions?.verbose ?? true; @@ -224,9 +298,21 @@ export const useMockServer = ( }); } - if (isTreeData /* || TODO: `isRowGrouping` */) { + if (isTreeData) { const { rows, rootRowCount } = await processTreeDataRows( - data.rows, + data?.rows ?? [], + params, + serverOptionsWithDefault, + columnsWithDefaultColDef, + ); + + getRowsResponse = { + rows: rows.slice().map((row) => ({ ...row, path: undefined })), + rowCount: rootRowCount, + }; + } else if (isRowGrouping) { + const { rows, rootRowCount } = await processRowGroupingRows( + data?.rows ?? [], params, serverOptionsWithDefault, columnsWithDefaultColDef, @@ -237,9 +323,8 @@ export const useMockServer = ( rowCount: rootRowCount, }; } else { - // plain data const { returnedRows, nextCursor, totalRowCount } = await loadServerRows( - data.rows, + data?.rows ?? [], { ...params, ...params.paginationModel }, serverOptionsWithDefault, columnsWithDefaultColDef, @@ -262,12 +347,13 @@ export const useMockServer = ( serverOptions?.useCursorPagination, isTreeData, columnsWithDefaultColDef, + isRowGrouping, ], ); return { columns: columnsWithDefaultColDef, - initialState, + initialState: options.dataSet === 'Movies' ? {} : initialState, getGroupKey, getChildrenCount, fetchRows, diff --git a/packages/x-data-grid-generator/src/hooks/useMovieData.ts b/packages/x-data-grid-generator/src/hooks/useMovieData.ts index 7820a7471fde..b9b196716839 100644 --- a/packages/x-data-grid-generator/src/hooks/useMovieData.ts +++ b/packages/x-data-grid-generator/src/hooks/useMovieData.ts @@ -546,6 +546,9 @@ const ROWS: GridRowModel[] = [ }, ]; +export const getMovieColumns = (): GridColDef[] => COLUMNS; +export const getMovieRows = (): GridRowModel[] => ROWS; + export const useMovieData = () => { return { rows: ROWS, diff --git a/packages/x-data-grid-generator/src/hooks/useQuery.ts b/packages/x-data-grid-generator/src/hooks/useQuery.ts index 62ae140bcdcd..5387a7e1f485 100644 --- a/packages/x-data-grid-generator/src/hooks/useQuery.ts +++ b/packages/x-data-grid-generator/src/hooks/useQuery.ts @@ -7,9 +7,15 @@ import { getColumnsFromOptions, getInitialState, } from './useDemoData'; -import { DEFAULT_DATASET_OPTIONS, DEFAULT_SERVER_OPTIONS, loadServerRows } from './serverUtils'; +import { DEFAULT_SERVER_OPTIONS, loadServerRows } from './serverUtils'; import type { ServerOptions, QueryOptions, PageInfo } from './serverUtils'; +const DEFAULT_DATASET_OPTIONS: UseDemoDataOptions = { + dataSet: 'Commodity', + rowLength: 100, + maxColumns: 6, +}; + export const createFakeServer = ( dataSetOptions?: Partial, serverOptions?: ServerOptions, diff --git a/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx b/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx index 8106dec99163..46c76914dda7 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx @@ -86,6 +86,7 @@ import { rowGroupingStateInitializer, } from '../hooks/features/rowGrouping/useGridRowGrouping'; import { useGridRowGroupingPreProcessors } from '../hooks/features/rowGrouping/useGridRowGroupingPreProcessors'; +import { useGridDataSourceRowGroupingPreProcessors } from '../hooks/features/rowGrouping/useGridDataSourceRowGroupingPreProcessors'; import { useGridExcelExport } from '../hooks/features/export/useGridExcelExport'; import { cellSelectionStateInitializer, @@ -105,6 +106,7 @@ export const useDataGridPremiumComponent = ( useGridRowSelectionPreProcessors(apiRef, props); useGridRowReorderPreProcessors(apiRef, props); useGridRowGroupingPreProcessors(apiRef, props); + useGridDataSourceRowGroupingPreProcessors(apiRef, props); useGridTreeDataPreProcessors(apiRef, props); useGridDataSourceTreeDataPreProcessors(apiRef, props); useGridLazyLoaderPreProcessors(apiRef, props); diff --git a/packages/x-data-grid-premium/src/components/GridDataSourceGroupingCriteriaCell.tsx b/packages/x-data-grid-premium/src/components/GridDataSourceGroupingCriteriaCell.tsx new file mode 100644 index 000000000000..0f7f159ee84c --- /dev/null +++ b/packages/x-data-grid-premium/src/components/GridDataSourceGroupingCriteriaCell.tsx @@ -0,0 +1,147 @@ +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 { + useGridSelector, + getDataGridUtilityClass, + GridRenderCellParams, + GridGroupNode, +} from '@mui/x-data-grid-pro'; +import { useGridApiContext } from '../hooks/utils/useGridApiContext'; +import { useGridRootProps } from '../hooks/utils/useGridRootProps'; +import { DataGridPremiumProcessedProps } from '../models/dataGridPremiumProps'; +import { GridPrivateApiPremium } from '../models/gridApiPremium'; +import { GridStatePremium } from '../models/gridStatePremium'; + +type OwnerState = DataGridPremiumProcessedProps; + +const useUtilityClasses = (ownerState: OwnerState) => { + const { classes } = ownerState; + + const slots = { + root: ['groupingCriteriaCell'], + toggle: ['groupingCriteriaCellToggle'], + loadingContainer: ['groupingCriteriaCellLoadingContainer'], + }; + + return composeClasses(slots, getDataGridUtilityClass, classes); +}; + +interface GridGroupingCriteriaCellProps extends GridRenderCellParams { + hideDescendantCount?: boolean; +} + +interface GridGroupingCriteriaCellIconProps + extends Pick { + descendantCount: number; +} + +function GridGroupingCriteriaCellIcon(props: GridGroupingCriteriaCellIconProps) { + const apiRef = useGridPrivateApiContext() as React.MutableRefObject; + const rootProps = useGridRootProps(); + const classes = useUtilityClasses(rootProps); + const { rowNode, id, field, descendantCount } = props; + + const loadingSelector = (state: GridStatePremium) => state.dataSource.loading[id] ?? false; + const errorSelector = (state: GridStatePremium) => state.dataSource.errors[id]; + const isDataLoading = useGridSelector(apiRef, loadingSelector); + const error = useGridSelector(apiRef, errorSelector); + + const handleClick = (event: React.MouseEvent) => { + if (!rowNode.childrenExpanded) { + // always fetch/get from cache the children when the node is expanded + apiRef.current.unstable_dataSource.fetchRows(id); + } else { + apiRef.current.setRowChildrenExpansion(id, !rowNode.childrenExpanded); + } + apiRef.current.setCellFocus(id, field); + event.stopPropagation(); + }; + + const Icon = rowNode.childrenExpanded + ? rootProps.slots.groupingCriteriaCollapseIcon + : rootProps.slots.groupingCriteriaExpandIcon; + + if (isDataLoading) { + return ( +
+ +
+ ); + } + + return descendantCount > 0 ? ( + + + + + + + + ) : null; +} + +export function GridDataSourceGroupingCriteriaCell(props: GridGroupingCriteriaCellProps) { + const { id, field, rowNode, hideDescendantCount, formattedValue } = props; + + const rootProps = useGridRootProps(); + const apiRef = useGridApiContext(); + const rowSelector = (state: GridStatePremium) => state.rows.dataRowIdToModelLookup[id]; + const row = useGridSelector(apiRef, rowSelector); + const classes = useUtilityClasses(rootProps); + + let descendantCount = 0; + if (row) { + descendantCount = Math.max(rootProps.unstable_dataSource?.getChildrenCount?.(row) ?? 0, 0); + } + + let cellContent: React.ReactNode; + + const colDef = apiRef.current.getColumn(rowNode.groupingField!); + if (typeof colDef?.renderCell === 'function') { + cellContent = colDef.renderCell(props); + } else if (typeof formattedValue !== 'undefined') { + cellContent = {formattedValue}; + } else { + cellContent = {rowNode.groupingKey}; + } + + return ( + + `calc(var(--DataGrid-cellOffsetMultiplier) * ${theme.spacing(rowNode.depth)})`, + }} + > +
+ +
+ {cellContent} + {!hideDescendantCount && descendantCount > 0 ? ( + ({descendantCount}) + ) : null} +
+ ); +} 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 caa1a42af8f5..194c62efa521 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 @@ -12,10 +12,12 @@ import { GridColumnRawLookup, isSingleSelectColDef } from '@mui/x-data-grid-pro/ import { GridApiPremium } from '../../../models/gridApiPremium'; import { GridGroupingColumnFooterCell } from '../../../components/GridGroupingColumnFooterCell'; import { GridGroupingCriteriaCell } from '../../../components/GridGroupingCriteriaCell'; +import { GridDataSourceGroupingCriteriaCell } from '../../../components/GridDataSourceGroupingCriteriaCell'; import { GridGroupingColumnLeafCell } from '../../../components/GridGroupingColumnLeafCell'; import { getRowGroupingFieldFromGroupingCriteria, GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, + RowGroupingStrategy, } from './gridRowGroupingUtils'; import { gridRowGroupingSanitizedModelSelector } from './gridRowGroupingSelector'; @@ -25,11 +27,24 @@ const GROUPING_COL_DEF_DEFAULT_PROPERTIES: Omit = { disableReorder: true, }; -const GROUPING_COL_DEF_FORCED_PROPERTIES: Pick = { +const GROUPING_COL_DEF_FORCED_PROPERTIES_DEFAULT: Pick< + GridColDef, + 'type' | 'editable' | 'groupable' +> = { editable: false, groupable: false, }; +const GROUPING_COL_DEF_FORCED_PROPERTIES_DATA_SOURCE: Pick< + GridColDef, + 'type' | 'editable' | 'groupable' | 'filterable' | 'sortable' | 'aggregable' +> = { + ...GROUPING_COL_DEF_FORCED_PROPERTIES_DEFAULT, + // TODO: Support these features on the grouping column(s) + filterable: false, + sortable: false, +}; + /** * When sorting two cells with different grouping criteria, we consider that the cell with the grouping criteria coming first in the model should be displayed below. * This can occur when some rows don't have all the fields. In which case we want the rows with the missing field to be displayed above. @@ -122,6 +137,7 @@ interface CreateGroupingColDefMonoCriteriaParams { * This value comes `prop.groupingColDef`. */ colDefOverride: GridGroupingColDefOverride | null | undefined; + strategy?: RowGroupingStrategy; } /** @@ -132,11 +148,17 @@ export const createGroupingColDefForOneGroupingCriteria = ({ groupedByColDef, groupingCriteria, colDefOverride, + strategy = RowGroupingStrategy.Default, }: CreateGroupingColDefMonoCriteriaParams): GridColDef => { const { leafField, mainGroupingCriteria, hideDescendantCount, ...colDefOverrideProperties } = colDefOverride ?? {}; const leafColDef = leafField ? columnsLookup[leafField] : null; + const CriteriaCell = + strategy === RowGroupingStrategy.Default + ? GridGroupingCriteriaCell + : GridDataSourceGroupingCriteriaCell; + // The properties that do not depend on the presence of a `leafColDef` and that can be overridden by `colDefOverride` const commonProperties: Partial = { width: Math.max( @@ -170,7 +192,7 @@ export const createGroupingColDefForOneGroupingCriteria = ({ // Render current grouping criteria groups if (params.rowNode.groupingField === groupingCriteria) { return ( - )} hideDescendantCount={hideDescendantCount} /> @@ -222,7 +244,7 @@ export const createGroupingColDefForOneGroupingCriteria = ({ // The properties that can't be overridden with `colDefOverride` const forcedProperties: Pick = { field: getRowGroupingFieldFromGroupingCriteria(groupingCriteria), - ...GROUPING_COL_DEF_FORCED_PROPERTIES, + ...GROUPING_COL_DEF_FORCED_PROPERTIES_DEFAULT, }; return { @@ -246,6 +268,7 @@ interface CreateGroupingColDefSeveralCriteriaParams { * This value comes `prop.groupingColDef`. */ colDefOverride: GridGroupingColDefOverride | null | undefined; + strategy?: RowGroupingStrategy; } /** @@ -256,11 +279,17 @@ export const createGroupingColDefForAllGroupingCriteria = ({ columnsLookup, rowGroupingModel, colDefOverride, + strategy = RowGroupingStrategy.Default, }: CreateGroupingColDefSeveralCriteriaParams): GridColDef => { const { leafField, mainGroupingCriteria, hideDescendantCount, ...colDefOverrideProperties } = colDefOverride ?? {}; const leafColDef = leafField ? columnsLookup[leafField] : null; + const CriteriaCell = + strategy === RowGroupingStrategy.Default + ? GridGroupingCriteriaCell + : GridDataSourceGroupingCriteriaCell; + // The properties that do not depend on the presence of a `leafColDef` and that can be overridden by `colDefOverride` const commonProperties: Partial = { headerName: apiRef.current.getLocaleText('groupingColumnHeaderName'), @@ -296,7 +325,7 @@ export const createGroupingColDefForAllGroupingCriteria = ({ // Render the groups return ( - )} hideDescendantCount={hideDescendantCount} /> @@ -344,7 +373,9 @@ export const createGroupingColDefForAllGroupingCriteria = ({ // The properties that can't be overridden with `colDefOverride` const forcedProperties: Pick = { field: GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, - ...GROUPING_COL_DEF_FORCED_PROPERTIES, + ...(strategy === RowGroupingStrategy.Default + ? GROUPING_COL_DEF_FORCED_PROPERTIES_DEFAULT + : GROUPING_COL_DEF_FORCED_PROPERTIES_DATA_SOURCE), }; return { 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 1cdec6a7c663..0310fb7b338d 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 @@ -9,12 +9,16 @@ import { GridRowModel, GridColDef, GridKeyValue, + GridDataSource, } from '@mui/x-data-grid-pro'; import { passFilterLogic, GridAggregatedFilterItemApplier, GridAggregatedFilterItemApplierResult, GridColumnRawLookup, + GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, + getRowGroupingCriteriaFromGroupingField, + isGroupingColumn, } from '@mui/x-data-grid-pro/internals'; import { DataGridPremiumProcessedProps } from '../../../models/dataGridPremiumProps'; import { @@ -26,9 +30,16 @@ import { GridStatePremium } from '../../../models/gridStatePremium'; import { gridRowGroupingSanitizedModelSelector } from './gridRowGroupingSelector'; import { GridPrivateApiPremium } from '../../../models/gridApiPremium'; -export const GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD = '__row_group_by_columns_group__'; +export { + GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD, + getRowGroupingCriteriaFromGroupingField, + isGroupingColumn, +}; -export const ROW_GROUPING_STRATEGY = 'grouping-columns'; +export enum RowGroupingStrategy { + Default = 'grouping-columns', + DataSource = 'grouping-columns-data-source', +} export const getRowGroupingFieldFromGroupingCriteria = (groupingCriteria: string | null) => { if (groupingCriteria === null) { @@ -38,20 +49,6 @@ export const getRowGroupingFieldFromGroupingCriteria = (groupingCriteria: string return `__row_group_by_columns_group_${groupingCriteria}__`; }; -export const getRowGroupingCriteriaFromGroupingField = (groupingColDefField: string) => { - const match = groupingColDefField.match(/^__row_group_by_columns_group_(.*)__$/); - - if (!match) { - return null; - } - - return match[1]; -}; - -export const isGroupingColumn = (field: string) => - field === GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD || - getRowGroupingCriteriaFromGroupingField(field) !== null; - interface FilterRowTreeFromTreeDataParams { rowTree: GridRowTreeConfig; isRowMatchingFilters: GridAggregatedFilterItemApplier | null; @@ -178,10 +175,11 @@ export const filterRowTreeFromGroupingColumns = ( export const getColDefOverrides = ( groupingColDefProp: DataGridPremiumProcessedProps['groupingColDef'], fields: string[], + strategy?: RowGroupingStrategy, ) => { if (typeof groupingColDefProp === 'function') { return groupingColDefProp({ - groupingName: ROW_GROUPING_STRATEGY, + groupingName: strategy ?? RowGroupingStrategy.Default, fields, }); } @@ -199,6 +197,7 @@ export const mergeStateWithRowGroupingModel = export const setStrategyAvailability = ( privateApiRef: React.MutableRefObject, disableRowGrouping: boolean, + dataSource?: GridDataSource, ) => { let isAvailable: () => boolean; if (disableRowGrouping) { @@ -210,7 +209,9 @@ export const setStrategyAvailability = ( }; } - privateApiRef.current.setStrategyAvailability('rowTree', ROW_GROUPING_STRATEGY, isAvailable); + const strategy = dataSource ? RowGroupingStrategy.DataSource : RowGroupingStrategy.Default; + + privateApiRef.current.setStrategyAvailability('rowTree', strategy, isAvailable); }; export const getCellGroupingCriteria = ({ 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 new file mode 100644 index 000000000000..07ad4690d059 --- /dev/null +++ b/packages/x-data-grid-premium/src/hooks/features/rowGrouping/useGridDataSourceRowGroupingPreProcessors.ts @@ -0,0 +1,134 @@ +import * as React from 'react'; +import { GridRowId, gridRowTreeSelector, gridColumnLookupSelector } from '@mui/x-data-grid-pro'; +import { + GridStrategyProcessor, + useGridRegisterStrategyProcessor, + createRowTree, + updateRowTree, + getVisibleRowsLookup, + skipSorting, + skipFiltering, + GridRowsPartialUpdates, +} from '@mui/x-data-grid-pro/internals'; +import { DataGridPremiumProcessedProps } from '../../../models/dataGridPremiumProps'; +import { getGroupingRules, RowGroupingStrategy } from './gridRowGroupingUtils'; +import { GridPrivateApiPremium } from '../../../models/gridApiPremium'; +import { gridRowGroupingSanitizedModelSelector } from './gridRowGroupingSelector'; + +export const useGridDataSourceRowGroupingPreProcessors = ( + apiRef: React.MutableRefObject, + props: Pick< + DataGridPremiumProcessedProps, + | 'disableRowGrouping' + | 'groupingColDef' + | 'rowGroupingColumnMode' + | 'defaultGroupingExpansionDepth' + | 'isGroupExpandedByDefault' + | 'unstable_dataSource' + >, +) => { + const createRowTreeForRowGrouping = React.useCallback>( + (params) => { + const getGroupKey = props.unstable_dataSource?.getGroupKey; + if (!getGroupKey) { + throw new Error('MUI X: No `getGroupKey` method provided with the dataSource.'); + } + + const getChildrenCount = props.unstable_dataSource?.getChildrenCount; + if (!getChildrenCount) { + throw new Error('MUI X: No `getChildrenCount` method provided with the dataSource.'); + } + + const sanitizedRowGroupingModel = gridRowGroupingSanitizedModelSelector(apiRef); + const columnsLookup = gridColumnLookupSelector(apiRef); + const groupingRules = getGroupingRules({ + sanitizedRowGroupingModel, + columnsLookup, + }); + apiRef.current.caches.rowGrouping.rulesOnLastRowTreeCreation = groupingRules; + + const getRowTreeBuilderNode = (rowId: GridRowId) => { + const parentPath = (params.updates as GridRowsPartialUpdates).groupKeys ?? []; + const row = params.dataRowIdToModelLookup[rowId]; + const groupingRule = groupingRules[parentPath.length]; + const groupingValueGetter = groupingRule?.groupingValueGetter; + const leafKey = + groupingValueGetter?.( + row[groupingRule.field] as never, + row, + columnsLookup[groupingRule.field], + apiRef, + ) ?? getGroupKey(params.dataRowIdToModelLookup[rowId]); + return { + id: rowId, + path: [...parentPath, leafKey ?? rowId.toString()].map((key, i) => ({ + key, + field: groupingRules[i]?.field ?? null, + })), + serverChildrenCount: getChildrenCount(params.dataRowIdToModelLookup[rowId]) ?? 0, + }; + }; + + if (params.updates.type === 'full') { + return createRowTree({ + previousTree: params.previousTree, + nodes: params.updates.rows.map(getRowTreeBuilderNode), + defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, + isGroupExpandedByDefault: props.isGroupExpandedByDefault, + groupingName: RowGroupingStrategy.DataSource, + }); + } + + return updateRowTree({ + nodes: { + inserted: (params.updates as GridRowsPartialUpdates).actions.insert.map( + getRowTreeBuilderNode, + ), + modified: (params.updates as GridRowsPartialUpdates).actions.modify.map( + getRowTreeBuilderNode, + ), + removed: (params.updates as GridRowsPartialUpdates).actions.remove, + }, + previousTree: params.previousTree!, + previousGroupsToFetch: params.previousGroupsToFetch, + previousTreeDepth: params.previousTreeDepths!, + defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, + isGroupExpandedByDefault: props.isGroupExpandedByDefault, + groupingName: RowGroupingStrategy.DataSource, + }); + }, + [ + apiRef, + props.unstable_dataSource, + props.defaultGroupingExpansionDepth, + props.isGroupExpandedByDefault, + ], + ); + + const filterRows = React.useCallback>(() => { + const rowTree = gridRowTreeSelector(apiRef); + + return skipFiltering(rowTree); + }, [apiRef]); + + const sortRows = React.useCallback>(() => { + const rowTree = gridRowTreeSelector(apiRef); + + return skipSorting(rowTree); + }, [apiRef]); + + useGridRegisterStrategyProcessor( + apiRef, + RowGroupingStrategy.DataSource, + 'rowTreeCreation', + createRowTreeForRowGrouping, + ); + useGridRegisterStrategyProcessor(apiRef, RowGroupingStrategy.DataSource, 'filtering', filterRows); + useGridRegisterStrategyProcessor(apiRef, RowGroupingStrategy.DataSource, 'sorting', sortRows); + useGridRegisterStrategyProcessor( + apiRef, + RowGroupingStrategy.DataSource, + 'visibleRowsLookupCreation', + getVisibleRowsLookup, + ); +}; 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 b779e23a2064..fe562ab900c3 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 @@ -19,7 +19,7 @@ import { import { DataGridPremiumProcessedProps } from '../../../models/dataGridPremiumProps'; import { getRowGroupingFieldFromGroupingCriteria, - ROW_GROUPING_STRATEGY, + RowGroupingStrategy, isGroupingColumn, mergeStateWithRowGroupingModel, setStrategyAvailability, @@ -63,6 +63,7 @@ export const useGridRowGrouping = ( | 'disableRowGrouping' | 'slotProps' | 'slots' + | 'unstable_dataSource' >, ) => { apiRef.current.registerControlState({ @@ -73,7 +74,7 @@ export const useGridRowGrouping = ( changeEvent: 'rowGroupingModelChange', }); - /** + /* * API METHODS */ const setRowGroupingModel = React.useCallback( @@ -165,6 +166,16 @@ export const useGridRowGrouping = ( [props.disableRowGrouping], ); + const addGetRowsParams = React.useCallback>( + (params) => { + return { + ...params, + groupFields: gridRowGroupingModelSelector(apiRef), + }; + }, + [apiRef], + ); + const stateExportPreProcessing = React.useCallback>( (prevState, context) => { const rowGroupingModelToExport = gridRowGroupingModelSelector(apiRef); @@ -209,10 +220,11 @@ export const useGridRowGrouping = ( ); useGridRegisterPipeProcessor(apiRef, 'columnMenu', addColumnMenuButtons); + useGridRegisterPipeProcessor(apiRef, 'getRowsParams', addGetRowsParams); useGridRegisterPipeProcessor(apiRef, 'exportState', stateExportPreProcessing); useGridRegisterPipeProcessor(apiRef, 'restoreState', stateRestorePreProcessing); - /** + /* * EVENTS */ const handleCellKeyDown = React.useCallback>( @@ -233,10 +245,15 @@ export const useGridRowGrouping = ( return; } + if (props.unstable_dataSource && !params.rowNode.childrenExpanded) { + apiRef.current.unstable_dataSource.fetchRows(params.id); + return; + } + apiRef.current.setRowChildrenExpansion(params.id, !params.rowNode.childrenExpanded); } }, - [apiRef, props.rowGroupingColumnMode], + [apiRef, props.rowGroupingColumnMode, props.unstable_dataSource], ); const checkGroupingColumnsModelDiff = React.useCallback< @@ -258,7 +275,7 @@ export const useGridRowGrouping = ( // Refresh the row tree creation strategy processing // TODO: Add a clean way to re-run a strategy processing without publishing a private event - if (apiRef.current.getActiveStrategy('rowTree') === ROW_GROUPING_STRATEGY) { + if (apiRef.current.getActiveStrategy('rowTree') === RowGroupingStrategy.Default) { apiRef.current.publishEvent('activeStrategyProcessorChange', 'rowTreeCreation'); } } @@ -267,8 +284,11 @@ export const useGridRowGrouping = ( useGridApiEventHandler(apiRef, 'cellKeyDown', handleCellKeyDown); useGridApiEventHandler(apiRef, 'columnsChange', checkGroupingColumnsModelDiff); useGridApiEventHandler(apiRef, 'rowGroupingModelChange', checkGroupingColumnsModelDiff); + useGridApiEventHandler(apiRef, 'rowGroupingModelChange', () => + apiRef.current.unstable_dataSource.fetchRows(), + ); - /** + /* * EFFECTS */ React.useEffect(() => { 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 2fa403ec72d5..ac43f038695a 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 @@ -31,7 +31,7 @@ import { import { filterRowTreeFromGroupingColumns, getColDefOverrides, - ROW_GROUPING_STRATEGY, + RowGroupingStrategy, isGroupingColumn, setStrategyAvailability, getCellGroupingCriteria, @@ -48,6 +48,7 @@ export const useGridRowGroupingPreProcessors = ( | 'rowGroupingColumnMode' | 'defaultGroupingExpansionDepth' | 'isGroupExpandedByDefault' + | 'unstable_dataSource' >, ) => { const getGroupingColDefs = React.useCallback( @@ -56,6 +57,10 @@ export const useGridRowGroupingPreProcessors = ( return []; } + const strategy = props.unstable_dataSource + ? RowGroupingStrategy.DataSource + : RowGroupingStrategy.Default; + const groupingColDefProp = props.groupingColDef; // We can't use `gridGroupingRowsSanitizedModelSelector` here because the new columns are not in the state yet @@ -73,8 +78,9 @@ export const useGridRowGroupingPreProcessors = ( createGroupingColDefForAllGroupingCriteria({ apiRef, rowGroupingModel, - colDefOverride: getColDefOverrides(groupingColDefProp, rowGroupingModel), + colDefOverride: getColDefOverrides(groupingColDefProp, rowGroupingModel, strategy), columnsLookup: columnsState.lookup, + strategy, }), ]; } @@ -86,6 +92,7 @@ export const useGridRowGroupingPreProcessors = ( colDefOverride: getColDefOverrides(groupingColDefProp, [groupingCriteria]), groupedByColDef: columnsState.lookup[groupingCriteria], columnsLookup: columnsState.lookup, + strategy, }), ); } @@ -95,7 +102,13 @@ export const useGridRowGroupingPreProcessors = ( } } }, - [apiRef, props.groupingColDef, props.rowGroupingColumnMode, props.disableRowGrouping], + [ + apiRef, + props.groupingColDef, + props.rowGroupingColumnMode, + props.disableRowGrouping, + props.unstable_dataSource, + ], ); const updateGroupingColumn = React.useCallback>( @@ -177,7 +190,7 @@ export const useGridRowGroupingPreProcessors = ( nodes: params.updates.rows.map(getRowTreeBuilderNode), defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, isGroupExpandedByDefault: props.isGroupExpandedByDefault, - groupingName: ROW_GROUPING_STRATEGY, + groupingName: RowGroupingStrategy.Default, }); } @@ -191,7 +204,7 @@ export const useGridRowGroupingPreProcessors = ( previousTreeDepth: params.previousTreeDepths!, defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, isGroupExpandedByDefault: props.isGroupExpandedByDefault, - groupingName: ROW_GROUPING_STRATEGY, + groupingName: RowGroupingStrategy.Default, }); }, [apiRef, props.defaultGroupingExpansionDepth, props.isGroupExpandedByDefault], @@ -228,35 +241,29 @@ export const useGridRowGroupingPreProcessors = ( useGridRegisterPipeProcessor(apiRef, 'hydrateColumns', updateGroupingColumn); useGridRegisterStrategyProcessor( apiRef, - ROW_GROUPING_STRATEGY, + RowGroupingStrategy.Default, 'rowTreeCreation', createRowTreeForRowGrouping, ); - useGridRegisterStrategyProcessor(apiRef, ROW_GROUPING_STRATEGY, 'filtering', filterRows); - useGridRegisterStrategyProcessor(apiRef, ROW_GROUPING_STRATEGY, 'sorting', sortRows); + useGridRegisterStrategyProcessor(apiRef, RowGroupingStrategy.Default, 'filtering', filterRows); + useGridRegisterStrategyProcessor(apiRef, RowGroupingStrategy.Default, 'sorting', sortRows); useGridRegisterStrategyProcessor( apiRef, - ROW_GROUPING_STRATEGY, + RowGroupingStrategy.Default, 'visibleRowsLookupCreation', getVisibleRowsLookup, ); - /** - * 1ST RENDER - */ useFirstRender(() => { - setStrategyAvailability(apiRef, props.disableRowGrouping); + setStrategyAvailability(apiRef, props.disableRowGrouping, props.unstable_dataSource); }); - /** - * EFFECTS - */ const isFirstRender = React.useRef(true); React.useEffect(() => { if (!isFirstRender.current) { - setStrategyAvailability(apiRef, props.disableRowGrouping); + setStrategyAvailability(apiRef, props.disableRowGrouping, props.unstable_dataSource); } else { isFirstRender.current = false; } - }, [apiRef, props.disableRowGrouping]); + }, [apiRef, props.disableRowGrouping, props.unstable_dataSource]); }; diff --git a/packages/x-data-grid-pro/src/hooks/features/dataSource/cache.ts b/packages/x-data-grid-pro/src/hooks/features/dataSource/cache.ts index dde8cad3d39f..5645235abf01 100644 --- a/packages/x-data-grid-pro/src/hooks/features/dataSource/cache.ts +++ b/packages/x-data-grid-pro/src/hooks/features/dataSource/cache.ts @@ -15,6 +15,7 @@ function getKey(params: GridGetRowsParams) { params.filterModel, params.sortModel, params.groupKeys, + params.groupFields, ]); } diff --git a/packages/x-data-grid-pro/src/hooks/features/dataSource/gridDataSourceSelector.ts b/packages/x-data-grid-pro/src/hooks/features/dataSource/gridDataSourceSelector.ts index 6b6aa5a9404d..09e2d34c9099 100644 --- a/packages/x-data-grid-pro/src/hooks/features/dataSource/gridDataSourceSelector.ts +++ b/packages/x-data-grid-pro/src/hooks/features/dataSource/gridDataSourceSelector.ts @@ -21,8 +21,6 @@ export const gridGetRowsParamsSelector = createSelector( (filterModel, sortModel, paginationModel) => { return { groupKeys: [], - // TODO: Implement with `rowGrouping` - groupFields: [], paginationModel, sortModel, filterModel, diff --git a/packages/x-data-grid-pro/src/hooks/features/dataSource/useGridDataSource.ts b/packages/x-data-grid-pro/src/hooks/features/dataSource/useGridDataSource.ts index 4e365d10d7ee..31f3d209a695 100644 --- a/packages/x-data-grid-pro/src/hooks/features/dataSource/useGridDataSource.ts +++ b/packages/x-data-grid-pro/src/hooks/features/dataSource/useGridDataSource.ts @@ -85,7 +85,10 @@ export const useGridDataSource = ( apiRef.current.resetDataSourceState(); } - const fetchParams = gridGetRowsParamsSelector(apiRef); + const fetchParams = { + ...gridGetRowsParamsSelector(apiRef), + ...apiRef.current.unstable_applyPipeProcessors('getRowsParams', {}), + }; const cachedData = apiRef.current.unstable_dataSource.cache.get(fetchParams); @@ -122,7 +125,8 @@ export const useGridDataSource = ( const fetchRowChildren = React.useCallback( async (id) => { - if (!props.treeData) { + const pipedParams = apiRef.current.unstable_applyPipeProcessors('getRowsParams', {}); + if (!props.treeData && (pipedParams.groupFields?.length ?? 0) === 0) { nestedDataManager.clearPendingRequest(id); return; } @@ -138,7 +142,11 @@ export const useGridDataSource = ( return; } - const fetchParams = { ...gridGetRowsParamsSelector(apiRef), groupKeys: rowNode.path }; + const fetchParams = { + ...gridGetRowsParamsSelector(apiRef), + ...pipedParams, + groupKeys: rowNode.path, + }; const cachedData = apiRef.current.unstable_dataSource.cache.get(fetchParams); diff --git a/packages/x-data-grid-pro/src/hooks/features/detailPanel/gridDetailPanelToggleColDef.tsx b/packages/x-data-grid-pro/src/hooks/features/detailPanel/gridDetailPanelToggleColDef.tsx index 76e80c840299..16998e55615f 100644 --- a/packages/x-data-grid-pro/src/hooks/features/detailPanel/gridDetailPanelToggleColDef.tsx +++ b/packages/x-data-grid-pro/src/hooks/features/detailPanel/gridDetailPanelToggleColDef.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; import { GRID_STRING_COL_DEF, GridColDef } from '@mui/x-data-grid'; +import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from '@mui/x-data-grid/internals'; import { GridApiPro } from '../../../models/gridApiPro'; import { GridDetailPanelToggleCell } from '../../../components/GridDetailPanelToggleCell'; import { gridDetailPanelExpandedRowIdsSelector } from './gridDetailPanelSelector'; -export const GRID_DETAIL_PANEL_TOGGLE_FIELD = '__detail_panel_toggle__'; +export { GRID_DETAIL_PANEL_TOGGLE_FIELD }; export const GRID_DETAIL_PANEL_TOGGLE_COL_DEF: GridColDef = { ...GRID_STRING_COL_DEF, diff --git a/packages/x-data-grid-pro/src/hooks/features/serverSideTreeData/useGridDataSourceTreeDataPreProcessors.tsx b/packages/x-data-grid-pro/src/hooks/features/serverSideTreeData/useGridDataSourceTreeDataPreProcessors.tsx index c09bc45b8ded..0c6b841f939d 100644 --- a/packages/x-data-grid-pro/src/hooks/features/serverSideTreeData/useGridDataSourceTreeDataPreProcessors.tsx +++ b/packages/x-data-grid-pro/src/hooks/features/serverSideTreeData/useGridDataSourceTreeDataPreProcessors.tsx @@ -34,8 +34,7 @@ import { } from '../../../utils/tree/models'; import { updateRowTree } from '../../../utils/tree/updateRowTree'; import { getVisibleRowsLookup } from '../../../utils/tree/utils'; - -const DATA_SOURCE_TREE_DATA_STRATEGY = 'dataSourceTreeData'; +import { TreeDataStrategy } from '../treeData/gridTreeDataUtils'; export const useGridDataSourceTreeDataPreProcessors = ( privateApiRef: React.MutableRefObject, @@ -53,7 +52,7 @@ export const useGridDataSourceTreeDataPreProcessors = ( const setStrategyAvailability = React.useCallback(() => { privateApiRef.current.setStrategyAvailability( 'rowTree', - DATA_SOURCE_TREE_DATA_STRATEGY, + TreeDataStrategy.DataSource, props.treeData && props.unstable_dataSource ? () => true : () => false, ); }, [privateApiRef, props.treeData, props.unstable_dataSource]); @@ -64,7 +63,7 @@ export const useGridDataSourceTreeDataPreProcessors = ( let colDefOverride: GridGroupingColDefOverride | null | undefined; if (typeof groupingColDefProp === 'function') { const params: GridGroupingColDefOverrideParams = { - groupingName: DATA_SOURCE_TREE_DATA_STRATEGY, + groupingName: TreeDataStrategy.DataSource, fields: [], }; @@ -171,7 +170,7 @@ export const useGridDataSourceTreeDataPreProcessors = ( nodes: params.updates.rows.map(getRowTreeBuilderNode), defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, isGroupExpandedByDefault: props.isGroupExpandedByDefault, - groupingName: DATA_SOURCE_TREE_DATA_STRATEGY, + groupingName: TreeDataStrategy.DataSource, onDuplicatePath, }); } @@ -187,7 +186,7 @@ export const useGridDataSourceTreeDataPreProcessors = ( previousTreeDepth: params.previousTreeDepths!, defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, isGroupExpandedByDefault: props.isGroupExpandedByDefault, - groupingName: DATA_SOURCE_TREE_DATA_STRATEGY, + groupingName: TreeDataStrategy.DataSource, }); }, [ @@ -212,25 +211,20 @@ export const useGridDataSourceTreeDataPreProcessors = ( useGridRegisterPipeProcessor(privateApiRef, 'hydrateColumns', updateGroupingColumn); useGridRegisterStrategyProcessor( privateApiRef, - DATA_SOURCE_TREE_DATA_STRATEGY, + TreeDataStrategy.DataSource, 'rowTreeCreation', createRowTreeForTreeData, ); useGridRegisterStrategyProcessor( privateApiRef, - DATA_SOURCE_TREE_DATA_STRATEGY, + TreeDataStrategy.DataSource, 'filtering', filterRows, ); + useGridRegisterStrategyProcessor(privateApiRef, TreeDataStrategy.DataSource, 'sorting', sortRows); useGridRegisterStrategyProcessor( privateApiRef, - DATA_SOURCE_TREE_DATA_STRATEGY, - 'sorting', - sortRows, - ); - useGridRegisterStrategyProcessor( - privateApiRef, - DATA_SOURCE_TREE_DATA_STRATEGY, + TreeDataStrategy.DataSource, 'visibleRowsLookupCreation', getVisibleRowsLookup, ); diff --git a/packages/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataGroupColDef.ts b/packages/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataGroupColDef.ts index 4fe1addd677f..f9f27fdd5d74 100644 --- a/packages/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataGroupColDef.ts +++ b/packages/x-data-grid-pro/src/hooks/features/treeData/gridTreeDataGroupColDef.ts @@ -1,4 +1,5 @@ import { GRID_STRING_COL_DEF, GridColDef } from '@mui/x-data-grid'; +import { GRID_TREE_DATA_GROUPING_FIELD } from '@mui/x-data-grid/internals'; /** * TODO: Add sorting and filtering on the value and the filteredDescendantCount @@ -19,7 +20,7 @@ export const GRID_TREE_DATA_GROUPING_COL_DEF: Omit; } -export const TREE_DATA_STRATEGY = 'tree-data'; +export enum TreeDataStrategy { + Default = 'tree-data', + DataSource = 'tree-data-source', +} /** * A node is visible if one of the following criteria is met: diff --git a/packages/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx b/packages/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx index 74c0fcfcda38..6a5753c9eb60 100644 --- a/packages/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx +++ b/packages/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx @@ -19,7 +19,7 @@ import { GRID_TREE_DATA_GROUPING_COL_DEF_FORCED_PROPERTIES, } from './gridTreeDataGroupColDef'; import { DataGridProProcessedProps } from '../../../models/dataGridProProps'; -import { filterRowTreeFromTreeData, TREE_DATA_STRATEGY } from './gridTreeDataUtils'; +import { filterRowTreeFromTreeData, TreeDataStrategy } from './gridTreeDataUtils'; import { GridPrivateApiPro } from '../../../models/gridApiPro'; import { GridGroupingColDefOverride, @@ -52,7 +52,7 @@ export const useGridTreeDataPreProcessors = ( const setStrategyAvailability = React.useCallback(() => { privateApiRef.current.setStrategyAvailability( 'rowTree', - TREE_DATA_STRATEGY, + TreeDataStrategy.Default, props.treeData && !props.unstable_dataSource ? () => true : () => false, ); }, [privateApiRef, props.treeData, props.unstable_dataSource]); @@ -63,7 +63,7 @@ export const useGridTreeDataPreProcessors = ( let colDefOverride: GridGroupingColDefOverride | null | undefined; if (typeof groupingColDefProp === 'function') { const params: GridGroupingColDefOverrideParams = { - groupingName: TREE_DATA_STRATEGY, + groupingName: TreeDataStrategy.Default, fields: [], }; @@ -158,7 +158,7 @@ export const useGridTreeDataPreProcessors = ( nodes: params.updates.rows.map(getRowTreeBuilderNode), defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, isGroupExpandedByDefault: props.isGroupExpandedByDefault, - groupingName: TREE_DATA_STRATEGY, + groupingName: TreeDataStrategy.Default, onDuplicatePath, }); } @@ -173,7 +173,7 @@ export const useGridTreeDataPreProcessors = ( previousTreeDepth: params.previousTreeDepths!, defaultGroupingExpansionDepth: props.defaultGroupingExpansionDepth, isGroupExpandedByDefault: props.isGroupExpandedByDefault, - groupingName: TREE_DATA_STRATEGY, + groupingName: TreeDataStrategy.Default, }); }, [props.getTreeDataPath, props.defaultGroupingExpansionDepth, props.isGroupExpandedByDefault], @@ -211,15 +211,20 @@ export const useGridTreeDataPreProcessors = ( useGridRegisterPipeProcessor(privateApiRef, 'hydrateColumns', updateGroupingColumn); useGridRegisterStrategyProcessor( privateApiRef, - TREE_DATA_STRATEGY, + TreeDataStrategy.Default, 'rowTreeCreation', createRowTreeForTreeData, ); - useGridRegisterStrategyProcessor(privateApiRef, TREE_DATA_STRATEGY, 'filtering', filterRows); - useGridRegisterStrategyProcessor(privateApiRef, TREE_DATA_STRATEGY, 'sorting', sortRows); useGridRegisterStrategyProcessor( privateApiRef, - TREE_DATA_STRATEGY, + TreeDataStrategy.Default, + 'filtering', + filterRows, + ); + useGridRegisterStrategyProcessor(privateApiRef, TreeDataStrategy.Default, 'sorting', sortRows); + useGridRegisterStrategyProcessor( + privateApiRef, + TreeDataStrategy.Default, 'visibleRowsLookupCreation', getVisibleRowsLookup, ); diff --git a/packages/x-data-grid-pro/src/internals/index.ts b/packages/x-data-grid-pro/src/internals/index.ts index c0de9e27064b..ed619bb0cc9e 100644 --- a/packages/x-data-grid-pro/src/internals/index.ts +++ b/packages/x-data-grid-pro/src/internals/index.ts @@ -33,7 +33,6 @@ export { useGridRowReorder } from '../hooks/features/rowReorder/useGridRowReorde export { useGridRowReorderPreProcessors } from '../hooks/features/rowReorder/useGridRowReorderPreProcessors'; export { useGridTreeData } from '../hooks/features/treeData/useGridTreeData'; export { useGridTreeDataPreProcessors } from '../hooks/features/treeData/useGridTreeDataPreProcessors'; -export { TREE_DATA_STRATEGY } from '../hooks/features/treeData/gridTreeDataUtils'; export { useGridRowPinning, rowPinningStateInitializer, @@ -61,4 +60,6 @@ export { sortRowTree } from '../utils/tree/sortRowTree'; export { insertNodeInTree, removeNodeFromTree, getVisibleRowsLookup } from '../utils/tree/utils'; export type { RowTreeBuilderGroupingCriterion } from '../utils/tree/models'; +export { skipSorting, skipFiltering } from '../hooks/features/serverSideTreeData/utils'; + export * from './propValidation'; diff --git a/packages/x-data-grid/src/components/GridRow.tsx b/packages/x-data-grid/src/components/GridRow.tsx index 8358424a82a7..c3447c435a7a 100644 --- a/packages/x-data-grid/src/components/GridRow.tsx +++ b/packages/x-data-grid/src/components/GridRow.tsx @@ -19,7 +19,7 @@ import { useGridVisibleRows } from '../hooks/utils/useGridVisibleRows'; import { findParentElementFromClassName, isEventTargetInPortal } from '../utils/domUtils'; import { GRID_CHECKBOX_SELECTION_COL_DEF } from '../colDef/gridCheckboxSelectionColDef'; import { GRID_ACTIONS_COLUMN_TYPE } from '../colDef/gridActionsColDef'; -import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from '../constants/gridDetailPanelToggleField'; +import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from '../internals/constants'; import type { GridDimensions } from '../hooks/features/dimensions'; import { gridSortModelSelector } from '../hooks/features/sorting/gridSortingSelector'; import { gridRowMaximumTreeDepthSelector } from '../hooks/features/rows/gridRowsSelector'; diff --git a/packages/x-data-grid/src/components/cell/GridBooleanCell.tsx b/packages/x-data-grid/src/components/cell/GridBooleanCell.tsx index 543e199229c2..7bdd414d0ff8 100644 --- a/packages/x-data-grid/src/components/cell/GridBooleanCell.tsx +++ b/packages/x-data-grid/src/components/cell/GridBooleanCell.tsx @@ -2,13 +2,16 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { SvgIconProps } from '@mui/material/SvgIcon'; import composeClasses from '@mui/utils/composeClasses'; +import { useGridSelector } from '../../hooks/utils/useGridSelector'; +import { gridRowMaximumTreeDepthSelector } from '../../hooks/features/rows/gridRowsSelector'; import { getDataGridUtilityClass } from '../../constants/gridClasses'; -import { GridRenderCellParams } from '../../models/params/gridCellParams'; import { useGridRootProps } from '../../hooks/utils/useGridRootProps'; import { useGridApiContext } from '../../hooks/utils/useGridApiContext'; -import { DataGridProcessedProps } from '../../models/props/DataGridProps'; -import { GridColDef } from '../../models/colDef/gridColDef'; import { isAutogeneratedRowNode } from '../../hooks/features/rows/gridRowsUtils'; +import type { DataGridProcessedProps } from '../../models/props/DataGridProps'; +import type { GridColDef } from '../../models/colDef/gridColDef'; +import type { GridRenderCellParams } from '../../models/params/gridCellParams'; +import { GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD } from '../../internals/constants'; type OwnerState = { classes: DataGridProcessedProps['classes'] }; @@ -49,11 +52,20 @@ function GridBooleanCellRaw(props: GridBooleanCellProps) { const ownerState = { classes: rootProps.classes }; const classes = useUtilityClasses(ownerState); + const maxDepth = useGridSelector(apiRef, gridRowMaximumTreeDepthSelector); + const isServerSideRowGroupingRow = + // @ts-expect-error - Access tree data prop + maxDepth > 0 && rowNode.type === 'group' && rootProps.treeData === false; + const Icon = React.useMemo( () => (value ? rootProps.slots.booleanCellTrueIcon : rootProps.slots.booleanCellFalseIcon), [rootProps.slots.booleanCellFalseIcon, rootProps.slots.booleanCellTrueIcon, value], ); + if (isServerSideRowGroupingRow && value === undefined) { + return null; + } + return ( { - if (params.field !== '__row_group_by_columns_group__' && isAutogeneratedRowNode(params.rowNode)) { + if ( + params.field !== GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD && + isAutogeneratedRowNode(params.rowNode) + ) { return ''; } diff --git a/packages/x-data-grid/src/components/containers/GridRootStyles.ts b/packages/x-data-grid/src/components/containers/GridRootStyles.ts index eb1ccdc74996..84cb0ec975eb 100644 --- a/packages/x-data-grid/src/components/containers/GridRootStyles.ts +++ b/packages/x-data-grid/src/components/containers/GridRootStyles.ts @@ -130,6 +130,9 @@ export const GridRootStyles = styled('div', { { [`& .${c.treeDataGroupingCellLoadingContainer}`]: styles.treeDataGroupingCellLoadingContainer, }, + { + [`& .${c.groupingCriteriaCellLoadingContainer}`]: styles.groupingCriteriaCellLoadingContainer, + }, { [`& .${c.detailPanelToggleCell}`]: styles.detailPanelToggleCell }, { [`& .${c['detailPanelToggleCell--expanded']}`]: styles['detailPanelToggleCell--expanded'], @@ -708,7 +711,7 @@ export const GridRootStyles = styled('div', { alignSelf: 'stretch', marginRight: t.spacing(2), }, - [`& .${c.treeDataGroupingCellLoadingContainer}`]: { + [`& .${c.treeDataGroupingCellLoadingContainer}, .${c.groupingCriteriaCellLoadingContainer}`]: { display: 'flex', alignItems: 'center', justifyContent: 'center', diff --git a/packages/x-data-grid/src/constants/gridClasses.ts b/packages/x-data-grid/src/constants/gridClasses.ts index ca4ed8ad672f..06ce8915846a 100644 --- a/packages/x-data-grid/src/constants/gridClasses.ts +++ b/packages/x-data-grid/src/constants/gridClasses.ts @@ -631,6 +631,11 @@ export interface GridClasses { * Styles applied to the toggle of the grouping criteria cell */ groupingCriteriaCellToggle: string; + /** + * Styles applied to the loading container of the grouping cell of the tree data. + * @ignore - do not document. + */ + groupingCriteriaCellLoadingContainer: string; /** * Styles applied to the pinned rows container. */ @@ -810,6 +815,7 @@ export const gridClasses = generateUtilityClasses('MuiDataGrid', [ 'treeDataGroupingCellLoadingContainer', 'groupingCriteriaCell', 'groupingCriteriaCellToggle', + 'groupingCriteriaCellLoadingContainer', 'pinnedRows', 'pinnedRows--top', 'pinnedRows--bottom', diff --git a/packages/x-data-grid/src/constants/gridDetailPanelToggleField.ts b/packages/x-data-grid/src/constants/gridDetailPanelToggleField.ts deleted file mode 100644 index 7ad670d344b7..000000000000 --- a/packages/x-data-grid/src/constants/gridDetailPanelToggleField.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Can't import from pro package - hence duplication -export const GRID_DETAIL_PANEL_TOGGLE_FIELD = '__detail_panel_toggle__'; diff --git a/packages/x-data-grid/src/hooks/core/pipeProcessing/gridPipeProcessingApi.ts b/packages/x-data-grid/src/hooks/core/pipeProcessing/gridPipeProcessingApi.ts index 7cd516a68c06..d47a8bbe98b0 100644 --- a/packages/x-data-grid/src/hooks/core/pipeProcessing/gridPipeProcessingApi.ts +++ b/packages/x-data-grid/src/hooks/core/pipeProcessing/gridPipeProcessingApi.ts @@ -20,6 +20,7 @@ import { import { GridRowEntry, GridRowId } from '../../../models/gridRows'; import { GridHydrateRowsValue } from '../../features/rows/gridRowsInterfaces'; import { GridPreferencePanelsValue } from '../../features/preferencesPanel'; +import { GridGetRowsParams } from '../../../models/gridDataSource'; import { HeightEntry } from '../../features/rows/gridRowsMetaInterfaces'; export type GridPipeProcessorGroup = keyof GridPipeProcessingLookup; @@ -30,6 +31,7 @@ export interface GridPipeProcessingLookup { context: GridColDef; }; exportState: { value: GridInitialStateCommunity; context: GridExportStateParams }; + getRowsParams: { value: Partial }; hydrateColumns: { value: GridHydrateColumnsValue; }; diff --git a/packages/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx b/packages/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx index 3a89a3254b60..ee5325a7ad5b 100644 --- a/packages/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx +++ b/packages/x-data-grid/src/hooks/features/columnHeaders/useGridColumnHeaders.tsx @@ -368,7 +368,7 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { return null; } - const { renderedColumns, firstColumnToRender, lastColumnToRender } = columnsToRender; + const { firstColumnToRender, lastColumnToRender } = columnsToRender; const rowStructure = columnGroupsHeaderStructure[depth]; @@ -464,7 +464,7 @@ export const useGridColumnHeaders = (props: UseGridColumnHeadersProps) => { pinnedPosition={pinnedPosition} style={style} indexInSection={indexInSection} - sectionLength={renderedColumns.length} + sectionLength={rowStructure.length} gridHasFiller={gridHasFiller} /> ); diff --git a/packages/x-data-grid/src/hooks/features/focus/useGridFocus.ts b/packages/x-data-grid/src/hooks/features/focus/useGridFocus.ts index f45197e90ea4..3990d6ca8c6c 100644 --- a/packages/x-data-grid/src/hooks/features/focus/useGridFocus.ts +++ b/packages/x-data-grid/src/hooks/features/focus/useGridFocus.ts @@ -439,7 +439,7 @@ export const useGridFocus = ( const nextRow = currentPage.rows[clamp(lastFocusedRowIndex, 0, currentPage.rows.length - 1)]; - nextRowId = nextRow.id ?? null; + nextRowId = nextRow?.id ?? null; } apiRef.current.setState((state) => ({ diff --git a/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts b/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts index bed67114ffcf..8622525c32dd 100644 --- a/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts +++ b/packages/x-data-grid/src/hooks/features/keyboardNavigation/useGridKeyboardNavigation.ts @@ -1,5 +1,10 @@ import * as React from 'react'; import { useRtl } from '@mui/system/RtlProvider'; +import { + GRID_TREE_DATA_GROUPING_FIELD, + GRID_DETAIL_PANEL_TOGGLE_FIELD, +} from '../../../internals/constants'; +import { isGroupingColumn } from '../../../internals/utils/gridRowGroupingUtils'; import { GridEventListener } from '../../../models/events'; import { GridPrivateApiCommunity } from '../../../models/api/gridApiCommunity'; import { GridCellParams } from '../../../models/params/gridCellParams'; @@ -16,7 +21,6 @@ import { GRID_CHECKBOX_SELECTION_COL_DEF } from '../../../colDef/gridCheckboxSel import { gridClasses } from '../../../constants/gridClasses'; import { GridCellModes } from '../../../models/gridEditRowModel'; import { isNavigationKey } from '../../../utils/keyboardUtils'; -import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from '../../../constants/gridDetailPanelToggleField'; import { GridRowId } from '../../../models'; import { gridFocusColumnGroupHeaderSelector } from '../focus'; import { gridColumnGroupsHeaderMaxDepthSelector } from '../columnGrouping/gridColumnGroupsSelector'; @@ -587,8 +591,7 @@ export const useGridKeyboardNavigation = ( const colDef = (params as GridCellParams).colDef; if ( colDef && - // `GRID_TREE_DATA_GROUPING_FIELD` from the Pro package - colDef.field === '__tree_data_group__' + (colDef.field === GRID_TREE_DATA_GROUPING_FIELD || isGroupingColumn(colDef.field)) ) { break; } diff --git a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts index 7fd6366dca12..2c8515776a13 100644 --- a/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts +++ b/packages/x-data-grid/src/hooks/features/rowSelection/useGridRowSelection.ts @@ -29,7 +29,7 @@ import { isKeyboardEvent, isNavigationKey } from '../../../utils/keyboardUtils'; import { useGridVisibleRows } from '../../utils/useGridVisibleRows'; import { GridStateInitializer } from '../../utils/useGridInitializeState'; import { GridRowSelectionModel } from '../../../models'; -import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from '../../../constants/gridDetailPanelToggleField'; +import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from '../../../internals/constants'; import { gridClasses } from '../../../constants/gridClasses'; import { isEventTargetInPortal } from '../../../utils/domUtils'; import { isMultipleRowSelectionEnabled, findRowsToSelect, findRowsToDeselect } from './utils'; diff --git a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts index 12c5e75859b5..ef9dafab2321 100644 --- a/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts +++ b/packages/x-data-grid/src/hooks/features/rows/useGridRowSpanning.ts @@ -1,5 +1,6 @@ import * as React from 'react'; import useLazyRef from '@mui/utils/useLazyRef'; +import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from '../../../internals/constants'; import { gridVisibleColumnDefinitionsSelector } from '../columns/gridColumnsSelector'; import { useGridVisibleRows } from '../../utils/useGridVisibleRows'; import { gridRenderContextSelector } from '../virtualization/gridVirtualizationSelectors'; @@ -15,6 +16,7 @@ import { isRowContextInitialized, getCellValue, } from './gridRowSpanningUtils'; +import { GRID_CHECKBOX_SELECTION_FIELD } from '../../../colDef/gridCheckboxSelectionColDef'; export interface GridRowSpanningState { spannedCells: Record>; @@ -31,7 +33,11 @@ export type RowRange = { firstRowIndex: number; lastRowIndex: number }; const EMPTY_STATE = { spannedCells: {}, hiddenCells: {}, hiddenCellOriginMap: {} }; const EMPTY_RANGE: RowRange = { firstRowIndex: 0, lastRowIndex: 0 }; -const skippedFields = new Set(['__check__', '__reorder__', '__detail_panel_toggle__']); +const skippedFields = new Set([ + GRID_CHECKBOX_SELECTION_FIELD, + '__reorder__', + GRID_DETAIL_PANEL_TOGGLE_FIELD, +]); /** * Default number of rows to process during state initialization to avoid flickering. * Number `20` is arbitrarily chosen to be large enough to cover most of the cases without diff --git a/packages/x-data-grid/src/internals/constants.ts b/packages/x-data-grid/src/internals/constants.ts new file mode 100644 index 000000000000..2db12bb9c9d2 --- /dev/null +++ b/packages/x-data-grid/src/internals/constants.ts @@ -0,0 +1,3 @@ +export const GRID_TREE_DATA_GROUPING_FIELD = '__tree_data_group__'; +export const GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD = '__row_group_by_columns_group__'; +export const GRID_DETAIL_PANEL_TOGGLE_FIELD = '__detail_panel_toggle__'; diff --git a/packages/x-data-grid/src/internals/index.ts b/packages/x-data-grid/src/internals/index.ts index d07c6dd7d371..41f12e166367 100644 --- a/packages/x-data-grid/src/internals/index.ts +++ b/packages/x-data-grid/src/internals/index.ts @@ -177,4 +177,5 @@ export type { GridApiCaches } from '../models/gridApiCaches'; export { serializeCellValue } from '../hooks/features/export/serializers/csvSerializer'; export * from './utils'; +export * from './constants'; export type { Localization } from '../utils/getGridLocalization'; diff --git a/packages/x-data-grid/src/internals/utils/gridRowGroupingUtils.ts b/packages/x-data-grid/src/internals/utils/gridRowGroupingUtils.ts new file mode 100644 index 000000000000..5026a99260bf --- /dev/null +++ b/packages/x-data-grid/src/internals/utils/gridRowGroupingUtils.ts @@ -0,0 +1,15 @@ +import { GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD } from '../constants'; + +export const getRowGroupingCriteriaFromGroupingField = (groupingColDefField: string) => { + const match = groupingColDefField.match(/^__row_group_by_columns_group_(.*)__$/); + + if (!match) { + return null; + } + + return match[1]; +}; + +export const isGroupingColumn = (field: string) => + field === GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD || + getRowGroupingCriteriaFromGroupingField(field) !== null; diff --git a/packages/x-data-grid/src/internals/utils/index.ts b/packages/x-data-grid/src/internals/utils/index.ts index 5bdfb7c8c66f..4dd26999637b 100644 --- a/packages/x-data-grid/src/internals/utils/index.ts +++ b/packages/x-data-grid/src/internals/utils/index.ts @@ -1,3 +1,4 @@ export * from './computeSlots'; export * from './useProps'; export * from './propValidation'; +export * from './gridRowGroupingUtils'; diff --git a/packages/x-date-pickers-pro/src/DateRangeCalendar/timezone.DateRangeCalendar.test.tsx b/packages/x-date-pickers-pro/src/DateRangeCalendar/timezone.DateRangeCalendar.test.tsx new file mode 100644 index 000000000000..2d345ab1cf08 --- /dev/null +++ b/packages/x-date-pickers-pro/src/DateRangeCalendar/timezone.DateRangeCalendar.test.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { screen, fireEvent } from '@mui/internal-test-utils'; +import { describeAdapters } from 'test/utils/pickers'; +import { DateRangeCalendar } from './DateRangeCalendar'; + +describe(' - Timezone', () => { + describeAdapters('Timezone prop', DateRangeCalendar, ({ adapter, render }) => { + if (!adapter.isTimezoneCompatible) { + return; + } + + it('should correctly render month days when timezone changes', () => { + function DateCalendarWithControlledTimezone() { + const [timezone, setTimezone] = React.useState('Europe/Paris'); + return ( + + + + + ); + } + render(); + + expect( + screen.getAllByRole('gridcell', { + name: (_, element) => element.nodeName === 'BUTTON', + }).length, + ).to.equal(30); + + fireEvent.click(screen.getByRole('button', { name: 'Switch timezone' })); + + // the amount of rendered days should remain the same after changing timezone + expect( + screen.getAllByRole('gridcell', { + name: (_, element) => element.nodeName === 'BUTTON', + }).length, + ).to.equal(30); + }); + }); +}); diff --git a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.types.ts b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.types.ts index 95cd8b8a6b63..09d6ca578fe9 100644 --- a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.types.ts +++ b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.types.ts @@ -1,4 +1,5 @@ -import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { BaseDateValidationProps, MakeRequired } from '@mui/x-date-pickers/internals'; +import { BaseSingleInputFieldProps, PickerValidDate } from '@mui/x-date-pickers/models'; import { DesktopDateRangePickerProps, DesktopDateRangePickerSlots, @@ -9,6 +10,12 @@ import { MobileDateRangePickerSlots, MobileDateRangePickerSlotProps, } from '../MobileDateRangePicker'; +import { + DateRange, + DateRangeValidationError, + RangeFieldSection, + UseDateRangeFieldProps, +} from '../models'; export interface DateRangePickerSlots extends DesktopDateRangePickerSlots, @@ -42,3 +49,21 @@ export interface DateRangePickerProps< */ slotProps?: DateRangePickerSlotProps; } + +/** + * Props the field can receive when used inside a `DateRangePicker`, `DesktopDateRangePicker` or `MobileDateRangePicker` component. + */ +export type DateRangePickerFieldProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = true, +> = MakeRequired< + UseDateRangeFieldProps, + 'format' | 'timezone' | 'value' | keyof BaseDateValidationProps +> & + BaseSingleInputFieldProps< + DateRange, + TDate, + RangeFieldSection, + false, + DateRangeValidationError + >; diff --git a/packages/x-date-pickers-pro/src/DateRangePicker/index.ts b/packages/x-date-pickers-pro/src/DateRangePicker/index.ts index 4f65679f8be2..561b96d1667c 100644 --- a/packages/x-date-pickers-pro/src/DateRangePicker/index.ts +++ b/packages/x-date-pickers-pro/src/DateRangePicker/index.ts @@ -3,6 +3,7 @@ export type { DateRangePickerProps, DateRangePickerSlots, DateRangePickerSlotProps, + DateRangePickerFieldProps, } from './DateRangePicker.types'; export { DateRangePickerToolbar } from './DateRangePickerToolbar'; diff --git a/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePicker.types.ts b/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePicker.types.ts index aaa9942d7bf1..4a1e5d054f52 100644 --- a/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePicker.types.ts +++ b/packages/x-date-pickers-pro/src/DateTimeRangePicker/DateTimeRangePicker.types.ts @@ -1,4 +1,9 @@ -import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { + BaseDateValidationProps, + BaseTimeValidationProps, + MakeRequired, +} from '@mui/x-date-pickers/internals'; +import { BaseSingleInputFieldProps, PickerValidDate } from '@mui/x-date-pickers/models'; import { DesktopDateTimeRangePickerProps, DesktopDateTimeRangePickerSlots, @@ -9,6 +14,8 @@ import { MobileDateTimeRangePickerSlots, MobileDateTimeRangePickerSlotProps, } from '../MobileDateTimeRangePicker'; +import { UseDateTimeRangeFieldProps } from '../internals/models'; +import { DateRange, DateTimeRangeValidationError, RangeFieldSection } from '../models'; export interface DateTimeRangePickerSlots extends DesktopDateTimeRangePickerSlots, @@ -42,3 +49,26 @@ export interface DateTimeRangePickerProps< */ slotProps?: DateTimeRangePickerSlotProps; } + +/** + * Props the field can receive when used inside a `DateTimeRangePicker`, `DesktopDateTimeRangePicker` or `MobileDateTimeRangePicker` component. + */ +export type DateTimeRangePickerFieldProps< + TDate extends PickerValidDate, + TEnableAccessibleFieldDOMStructure extends boolean = true, +> = MakeRequired< + UseDateTimeRangeFieldProps, + | 'format' + | 'timezone' + | 'value' + | 'ampm' + | keyof BaseDateValidationProps + | keyof BaseTimeValidationProps +> & + BaseSingleInputFieldProps< + DateRange, + TDate, + RangeFieldSection, + false, + DateTimeRangeValidationError + >; diff --git a/packages/x-date-pickers-pro/src/DateTimeRangePicker/index.ts b/packages/x-date-pickers-pro/src/DateTimeRangePicker/index.ts index 6f24b2da3829..4de7da316eed 100644 --- a/packages/x-date-pickers-pro/src/DateTimeRangePicker/index.ts +++ b/packages/x-date-pickers-pro/src/DateTimeRangePicker/index.ts @@ -3,6 +3,7 @@ export type { DateTimeRangePickerProps, DateTimeRangePickerSlots, DateTimeRangePickerSlotProps, + DateTimeRangePickerFieldProps, } from './DateTimeRangePicker.types'; export { DateTimeRangePickerTabs } from './DateTimeRangePickerTabs'; diff --git a/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx b/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx index efae31fdc727..a395e353576e 100644 --- a/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx @@ -1,9 +1,9 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import { PickerViewRendererLookup } from '@mui/x-date-pickers/internals'; +import { PickerViewRendererLookup, useUtils } from '@mui/x-date-pickers/internals'; import { extractValidationProps } from '@mui/x-date-pickers/validation'; -import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { PickerOwnerState, PickerValidDate } from '@mui/x-date-pickers/models'; import resolveComponentProps from '@mui/utils/resolveComponentProps'; import { refType } from '@mui/utils'; import { rangeValueManager } from '../internals/utils/valueManagers'; @@ -40,6 +40,8 @@ const DesktopDateRangePicker = React.forwardRef(function DesktopDateRangePicker< inProps: DesktopDateRangePickerProps, ref: React.Ref, ) { + const utils = useUtils(); + // Props with the default values common to all date time pickers const defaultizedProps = useDateRangePickerDefaultizedProps< TDate, @@ -54,6 +56,7 @@ const DesktopDateRangePicker = React.forwardRef(function DesktopDateRangePicker< const props = { ...defaultizedProps, viewRenderers, + format: utils.formats.keyboardDate, calendars: defaultizedProps.calendars ?? 2, views: ['day'] as const, openTo: 'day' as const, @@ -63,7 +66,7 @@ const DesktopDateRangePicker = React.forwardRef(function DesktopDateRangePicker< }, slotProps: { ...defaultizedProps.slotProps, - field: (ownerState: any) => ({ + field: (ownerState: PickerOwnerState) => ({ ...resolveComponentProps(defaultizedProps.slotProps?.field, ownerState), ...extractValidationProps(defaultizedProps), ref, diff --git a/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/DesktopDateTimeRangePicker.tsx b/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/DesktopDateTimeRangePicker.tsx index 0e9c9db1e4f9..3293cb8f4529 100644 --- a/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/DesktopDateTimeRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/DesktopDateTimeRangePicker/DesktopDateTimeRangePicker.tsx @@ -11,7 +11,8 @@ import { useUtils, } from '@mui/x-date-pickers/internals'; import { extractValidationProps } from '@mui/x-date-pickers/validation'; -import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { PickerOwnerState, PickerValidDate } from '@mui/x-date-pickers/models'; +import { PickersLayoutOwnerState } from '@mui/x-date-pickers/PickersLayout'; import resolveComponentProps from '@mui/utils/resolveComponentProps'; import { refType } from '@mui/utils'; import { @@ -185,7 +186,7 @@ const DesktopDateTimeRangePicker = React.forwardRef(function DesktopDateTimeRang }, slotProps: { ...defaultizedProps.slotProps, - field: (ownerState: any) => ({ + field: (ownerState: PickerOwnerState) => ({ ...resolveComponentProps(defaultizedProps.slotProps?.field, ownerState), ...extractValidationProps(defaultizedProps), ref, @@ -199,7 +200,7 @@ const DesktopDateTimeRangePicker = React.forwardRef(function DesktopDateTimeRang toolbarVariant: 'desktop', ...defaultizedProps.slotProps?.toolbar, }, - actionBar: (ownerState: any) => ({ + actionBar: (ownerState: PickersLayoutOwnerState) => ({ actions: actionBarActions, ...resolveComponentProps(defaultizedProps.slotProps?.actionBar, ownerState), }), diff --git a/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.tsx b/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.tsx index e6ec08e98baa..712683c2ef7a 100644 --- a/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.tsx @@ -1,9 +1,9 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import { PickerViewRendererLookup } from '@mui/x-date-pickers/internals'; +import { PickerViewRendererLookup, useUtils } from '@mui/x-date-pickers/internals'; import { extractValidationProps } from '@mui/x-date-pickers/validation'; -import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { PickerOwnerState, PickerValidDate } from '@mui/x-date-pickers/models'; import resolveComponentProps from '@mui/utils/resolveComponentProps'; import { refType } from '@mui/utils'; import { rangeValueManager } from '../internals/utils/valueManagers'; @@ -40,6 +40,8 @@ const MobileDateRangePicker = React.forwardRef(function MobileDateRangePicker< inProps: MobileDateRangePickerProps, ref: React.Ref, ) { + const utils = useUtils(); + // Props with the default values common to all date time pickers const defaultizedProps = useDateRangePickerDefaultizedProps< TDate, @@ -54,6 +56,7 @@ const MobileDateRangePicker = React.forwardRef(function MobileDateRangePicker< const props = { ...defaultizedProps, viewRenderers, + format: utils.formats.keyboardDate, // Force one calendar on mobile to avoid layout issues calendars: 1, views: ['day'] as const, @@ -64,7 +67,7 @@ const MobileDateRangePicker = React.forwardRef(function MobileDateRangePicker< }, slotProps: { ...defaultizedProps.slotProps, - field: (ownerState: any) => ({ + field: (ownerState: PickerOwnerState) => ({ ...resolveComponentProps(defaultizedProps.slotProps?.field, ownerState), ...extractValidationProps(defaultizedProps), ref, diff --git a/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/MobileDateTimeRangePicker.tsx b/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/MobileDateTimeRangePicker.tsx index ee64407b61a2..08faf52286df 100644 --- a/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/MobileDateTimeRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/MobileDateTimeRangePicker/MobileDateTimeRangePicker.tsx @@ -15,7 +15,7 @@ import { useUtils, } from '@mui/x-date-pickers/internals'; import { extractValidationProps } from '@mui/x-date-pickers/validation'; -import { PickerValidDate } from '@mui/x-date-pickers/models'; +import { PickerOwnerState, PickerValidDate } from '@mui/x-date-pickers/models'; import resolveComponentProps from '@mui/utils/resolveComponentProps'; import { renderDigitalClockTimeView, @@ -184,7 +184,7 @@ const MobileDateTimeRangePicker = React.forwardRef(function MobileDateTimeRangeP }, slotProps: { ...defaultizedProps.slotProps, - field: (ownerState: any) => ({ + field: (ownerState: PickerOwnerState) => ({ ...resolveComponentProps(defaultizedProps.slotProps?.field, ownerState), ...extractValidationProps(defaultizedProps), ref, diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx b/packages/x-date-pickers-pro/src/internals/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx index 297f44b9fb72..ac09ecea1cb9 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/internals/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx @@ -12,7 +12,12 @@ import { ExportedBaseTabsProps, PickersProvider, } from '@mui/x-date-pickers/internals'; -import { PickerValidDate, FieldRef, InferError } from '@mui/x-date-pickers/models'; +import { + PickerValidDate, + FieldRef, + InferError, + PickerOwnerState, +} from '@mui/x-date-pickers/models'; import { DesktopRangePickerAdditionalViewProps, UseDesktopRangePickerParams, @@ -90,10 +95,11 @@ export const useDesktopRangePicker = < open, actions, layoutProps, + providerProps, renderCurrentView, shouldRestoreFocus, fieldProps: pickerFieldProps, - contextValue, + ownerState, } = usePicker< DateRange, TDate, @@ -107,6 +113,7 @@ export const useDesktopRangePicker = < wrapperVariant: 'desktop', autoFocusView: false, fieldRef: rangePosition === 'start' ? startFieldRef : endFieldRef, + localeText, additionalViewProps: { rangePosition, onRangePositionChange, @@ -143,7 +150,7 @@ export const useDesktopRangePicker = < TEnableAccessibleFieldDOMStructure, InferError >, - TExternalProps + PickerOwnerState >({ elementType: Field, externalSlotProps: slotProps?.field, @@ -163,7 +170,7 @@ export const useDesktopRangePicker = < ref: fieldContainerRef, ...(fieldType === 'single-input' ? { inputRef, name } : {}), }, - ownerState: props, + ownerState, }); const enrichedFieldProps = useEnrichedRangePickerFieldProps< @@ -210,7 +217,7 @@ export const useDesktopRangePicker = < const Layout = slots?.layout ?? PickersLayout; const renderPicker = () => ( - + , {}, - UsePickerProps, TDate, any, any, any, any> + PickerOwnerState >; fieldRoot?: SlotComponentProps>; fieldSeparator?: SlotComponentProps>; @@ -114,7 +114,7 @@ export interface UseEnrichedRangePickerFieldPropsParams< TEnableAccessibleFieldDOMStructure extends boolean, TError, > extends Pick< - UsePickerResponse, TView, RangeFieldSection, any>, + UsePickerResponse, TDate, TView, RangeFieldSection, any>, 'open' | 'actions' >, UseRangePositionResponse { diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx b/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx index c17afc51b900..c6e759ffd24c 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMobileRangePicker/useMobileRangePicker.tsx @@ -11,7 +11,12 @@ import { PickersProvider, } from '@mui/x-date-pickers/internals'; import { usePickersTranslations } from '@mui/x-date-pickers/hooks'; -import { PickerValidDate, FieldRef, InferError } from '@mui/x-date-pickers/models'; +import { + PickerValidDate, + FieldRef, + InferError, + PickerOwnerState, +} from '@mui/x-date-pickers/models'; import useId from '@mui/utils/useId'; import { MobileRangePickerAdditionalViewProps, @@ -86,9 +91,10 @@ export const useMobileRangePicker = < open, actions, layoutProps, + providerProps, renderCurrentView, fieldProps: pickerFieldProps, - contextValue, + ownerState, } = usePicker< DateRange, TDate, @@ -102,6 +108,7 @@ export const useMobileRangePicker = < wrapperVariant: 'mobile', autoFocusView: true, fieldRef: rangePosition === 'start' ? startFieldRef : endFieldRef, + localeText, additionalViewProps: { rangePosition, onRangePositionChange, @@ -119,7 +126,7 @@ export const useMobileRangePicker = < TEnableAccessibleFieldDOMStructure, InferError >, - TExternalProps + PickerOwnerState >({ elementType: Field, externalSlotProps: innerSlotProps?.field, @@ -137,7 +144,7 @@ export const useMobileRangePicker = < timezone, ...(fieldType === 'single-input' ? { inputRef, name } : {}), }, - ownerState: props, + ownerState, }); const isToolbarHidden = innerSlotProps?.toolbar?.hidden ?? false; @@ -215,7 +222,7 @@ export const useMobileRangePicker = < }; const renderPicker = () => ( - + = { + const startFieldProps = { error: !!validationError[0], ...startTextFieldProps, ...selectedSectionsResponse.start, @@ -125,11 +118,7 @@ export const useMultiInputDateRangeField = < autoFocus, // Do not add on end field. }; - const endFieldProps: UseDateFieldComponentProps< - TDate, - TEnableAccessibleFieldDOMStructure, - typeof sharedProps - > = { + const endFieldProps = { error: !!validationError[1], ...endTextFieldProps, ...selectedSectionsResponse.end, diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts index 60efbe608a10..14c14747d4e6 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts @@ -1,8 +1,5 @@ import useEventCallback from '@mui/utils/useEventCallback'; -import { - unstable_useDateTimeField as useDateTimeField, - UseDateTimeFieldComponentProps, -} from '@mui/x-date-pickers/DateTimeField'; +import { unstable_useDateTimeField as useDateTimeField } from '@mui/x-date-pickers/DateTimeField'; import { FieldChangeHandler, FieldChangeHandlerContext, @@ -104,11 +101,7 @@ export const useMultiInputDateTimeRangeField = < unstableEndFieldRef, }); - const startFieldProps: UseDateTimeFieldComponentProps< - TDate, - TEnableAccessibleFieldDOMStructure, - typeof sharedProps - > = { + const startFieldProps = { error: !!validationError[0], ...startTextFieldProps, ...selectedSectionsResponse.start, @@ -125,11 +118,7 @@ export const useMultiInputDateTimeRangeField = < autoFocus, // Do not add on end field. }; - const endFieldProps: UseDateTimeFieldComponentProps< - TDate, - TEnableAccessibleFieldDOMStructure, - typeof sharedProps - > = { + const endFieldProps = { error: !!validationError[1], ...endTextFieldProps, ...selectedSectionsResponse.end, diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts index 86db843894f5..1fc2affc9ac6 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts @@ -1,8 +1,5 @@ import useEventCallback from '@mui/utils/useEventCallback'; -import { - unstable_useTimeField as useTimeField, - UseTimeFieldComponentProps, -} from '@mui/x-date-pickers/TimeField'; +import { unstable_useTimeField as useTimeField } from '@mui/x-date-pickers/TimeField'; import { FieldChangeHandler, FieldChangeHandlerContext, @@ -104,11 +101,7 @@ export const useMultiInputTimeRangeField = < unstableEndFieldRef, }); - const startFieldProps: UseTimeFieldComponentProps< - TDate, - TEnableAccessibleFieldDOMStructure, - typeof sharedProps - > = { + const startFieldProps = { error: !!validationError[0], ...startTextFieldProps, ...selectedSectionsResponse.start, @@ -125,11 +118,7 @@ export const useMultiInputTimeRangeField = < autoFocus, // Do not add on end field. }; - const endFieldProps: UseTimeFieldComponentProps< - TDate, - TEnableAccessibleFieldDOMStructure, - typeof sharedProps - > = { + const endFieldProps = { error: !!validationError[1], ...endTextFieldProps, ...selectedSectionsResponse.end, diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useStaticRangePicker/useStaticRangePicker.tsx b/packages/x-date-pickers-pro/src/internals/hooks/useStaticRangePicker/useStaticRangePicker.tsx index fbb9d3a4f76c..24b10402c689 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useStaticRangePicker/useStaticRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/internals/hooks/useStaticRangePicker/useStaticRangePicker.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; import clsx from 'clsx'; import { styled } from '@mui/material/styles'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { PickersLayout, PickersLayoutSlotProps } from '@mui/x-date-pickers/PickersLayout'; import { usePicker, DIALOG_WIDTH, ExportedBaseToolbarProps, DateOrTimeViewWithMeridiem, + PickersProvider, } from '@mui/x-date-pickers/internals'; import { PickerValidDate } from '@mui/x-date-pickers/models'; import { @@ -40,7 +40,7 @@ export const useStaticRangePicker = < const { rangePosition, onRangePositionChange } = useRangePosition(props); - const { layoutProps, renderCurrentView } = usePicker< + const { layoutProps, providerProps, renderCurrentView } = usePicker< DateRange, TDate, TView, @@ -52,6 +52,7 @@ export const useStaticRangePicker = < props, autoFocusView: autoFocus ?? false, fieldRef: undefined, + localeText, additionalViewProps: { rangePosition, onRangePositionChange, @@ -70,7 +71,7 @@ export const useStaticRangePicker = < }; const renderPicker = () => ( - + {renderCurrentView()} - + ); return { renderPicker }; diff --git a/packages/x-date-pickers/src/DateCalendar/DayCalendar.tsx b/packages/x-date-pickers/src/DateCalendar/DayCalendar.tsx index 8998300b6407..5348469f3c84 100644 --- a/packages/x-date-pickers/src/DateCalendar/DayCalendar.tsx +++ b/packages/x-date-pickers/src/DateCalendar/DayCalendar.tsx @@ -534,9 +534,8 @@ export function DayCalendar(inProps: DayCalendarP ]); const weeksToDisplay = React.useMemo(() => { - const currentMonthWithTimezone = utils.setTimezone(currentMonth, timezone); - const toDisplay = utils.getWeekArray(currentMonthWithTimezone); - let nextMonth = utils.addMonths(currentMonthWithTimezone, 1); + const toDisplay = utils.getWeekArray(currentMonth); + let nextMonth = utils.addMonths(currentMonth, 1); while (fixedWeekNumber && toDisplay.length < fixedWeekNumber) { const additionalWeeks = utils.getWeekArray(nextMonth); const hasCommonWeek = utils.isSameDay( @@ -553,7 +552,7 @@ export function DayCalendar(inProps: DayCalendarP nextMonth = utils.addMonths(nextMonth, 1); } return toDisplay; - }, [currentMonth, fixedWeekNumber, utils, timezone]); + }, [currentMonth, fixedWeekNumber, utils]); return ( diff --git a/packages/x-date-pickers/src/DateCalendar/tests/timezone.DateCalendar.test.tsx b/packages/x-date-pickers/src/DateCalendar/tests/timezone.DateCalendar.test.tsx index 0f00aeee3039..a8624667be44 100644 --- a/packages/x-date-pickers/src/DateCalendar/tests/timezone.DateCalendar.test.tsx +++ b/packages/x-date-pickers/src/DateCalendar/tests/timezone.DateCalendar.test.tsx @@ -29,6 +29,49 @@ describe(' - Timezone', () => { expect(actualDate).toEqualDateTime(expectedDate); }); + it('should use "default" timezone for onChange when provided', () => { + const onChange = spy(); + const value = adapter.date('2022-04-25T15:30'); + + render(); + + fireEvent.click(screen.getByRole('gridcell', { name: '25' })); + const expectedDate = adapter.setDate(value, 25); + + // Check the `onChange` value (uses timezone prop) + const actualDate = onChange.lastCall.firstArg; + expect(adapter.getTimezone(actualDate)).to.equal(adapter.lib === 'dayjs' ? 'UTC' : 'system'); + expect(actualDate).toEqualDateTime(expectedDate); + }); + + it('should correctly render month days when timezone changes', () => { + function DateCalendarWithControlledTimezone() { + const [timezone, setTimezone] = React.useState('Europe/Paris'); + return ( + + + + + ); + } + render(); + + expect( + screen.getAllByRole('gridcell', { + name: (_, element) => element.nodeName === 'BUTTON', + }).length, + ).to.equal(30); + + fireEvent.click(screen.getByRole('button', { name: 'Switch timezone' })); + + // the amount of rendered days should remain the same after changing timezone + expect( + screen.getAllByRole('gridcell', { + name: (_, element) => element.nodeName === 'BUTTON', + }).length, + ).to.equal(30); + }); + TIMEZONE_TO_TEST.forEach((timezone) => { describe(`Timezone: ${timezone}`, () => { it('should use timezone prop for onChange when no value is provided', () => { diff --git a/packages/x-date-pickers/src/DateCalendar/useCalendarState.tsx b/packages/x-date-pickers/src/DateCalendar/useCalendarState.tsx index 29fff983fe3d..5e772c5d742d 100644 --- a/packages/x-date-pickers/src/DateCalendar/useCalendarState.tsx +++ b/packages/x-date-pickers/src/DateCalendar/useCalendarState.tsx @@ -43,6 +43,7 @@ export const createCalendarStateReducer = action: | ReducerAction<'finishMonthSwitchingAnimation'> | ReducerAction<'changeMonth', ChangeMonthPayload> + | ReducerAction<'changeMonthTimezone', { newTimezone: string }> | ReducerAction<'changeFocusedDay', ChangeFocusedDayPayload>, ): CalendarState => { switch (action.type) { @@ -54,6 +55,21 @@ export const createCalendarStateReducer = isMonthSwitchingAnimating: !reduceAnimations, }; + case 'changeMonthTimezone': { + const newTimezone = action.newTimezone; + if (utils.getTimezone(state.currentMonth) === newTimezone) { + return state; + } + let newCurrentMonth = utils.setTimezone(state.currentMonth, newTimezone); + if (utils.getMonth(newCurrentMonth) !== utils.getMonth(state.currentMonth)) { + newCurrentMonth = utils.setMonth(newCurrentMonth, utils.getMonth(state.currentMonth)); + } + return { + ...state, + currentMonth: newCurrentMonth, + }; + } + case 'finishMonthSwitchingAnimation': return { ...state, @@ -149,7 +165,9 @@ export const useCalendarState = ( granularity: SECTION_TYPE_GRANULARITY.day, }); }, - [], // eslint-disable-line react-hooks/exhaustive-deps + // We want the `referenceDate` to update on prop and `timezone` change (https://github.com/mui/mui-x/issues/10804) + // eslint-disable-next-line react-hooks/exhaustive-deps + [referenceDateProp, timezone], ); const [calendarState, dispatch] = React.useReducer(reducerFn, { @@ -159,6 +177,15 @@ export const useCalendarState = ( slideDirection: 'left', }); + // Ensure that `calendarState.currentMonth` timezone is updated when `referenceDate` (or timezone changes) + // https://github.com/mui/mui-x/issues/10804 + React.useEffect(() => { + dispatch({ + type: 'changeMonthTimezone', + newTimezone: utils.getTimezone(referenceDate), + }); + }, [referenceDate, utils]); + const handleChangeMonth = React.useCallback( (payload: ChangeMonthPayload) => { dispatch({ diff --git a/packages/x-date-pickers/src/DateField/DateField.types.ts b/packages/x-date-pickers/src/DateField/DateField.types.ts index 3db8ee716d74..2b6feddbc943 100644 --- a/packages/x-date-pickers/src/DateField/DateField.types.ts +++ b/packages/x-date-pickers/src/DateField/DateField.types.ts @@ -40,32 +40,28 @@ export interface UseDateFieldProps< BaseDateValidationProps, ExportedUseClearableFieldProps {} -export type UseDateFieldComponentProps< - TDate extends PickerValidDate, - TEnableAccessibleFieldDOMStructure extends boolean, - TChildProps extends {}, -> = Omit> & - UseDateFieldProps; - export type DateFieldProps< TDate extends PickerValidDate, TEnableAccessibleFieldDOMStructure extends boolean = true, -> = UseDateFieldComponentProps< - TDate, - TEnableAccessibleFieldDOMStructure, - BuiltInFieldTextFieldProps -> & { - /** - * Overridable component slots. - * @default {} - */ - slots?: DateFieldSlots; - /** - * The props used for each component slot. - * @default {} - */ - slotProps?: DateFieldSlotProps; -}; +> = + // The hook props + UseDateFieldProps & + // The TextField props + Omit< + BuiltInFieldTextFieldProps, + keyof UseDateFieldProps + > & { + /** + * Overridable component slots. + * @default {} + */ + slots?: DateFieldSlots; + /** + * The props used for each component slot. + * @default {} + */ + slotProps?: DateFieldSlotProps; + }; export type DateFieldOwnerState< TDate extends PickerValidDate, diff --git a/packages/x-date-pickers/src/DateField/index.ts b/packages/x-date-pickers/src/DateField/index.ts index cd6119e98a25..69c1964f1330 100644 --- a/packages/x-date-pickers/src/DateField/index.ts +++ b/packages/x-date-pickers/src/DateField/index.ts @@ -1,7 +1,3 @@ export { DateField } from './DateField'; export { useDateField as unstable_useDateField } from './useDateField'; -export type { - UseDateFieldProps, - UseDateFieldComponentProps, - DateFieldProps, -} from './DateField.types'; +export type { UseDateFieldProps, DateFieldProps } from './DateField.types'; diff --git a/packages/x-date-pickers/src/DatePicker/DatePicker.types.ts b/packages/x-date-pickers/src/DatePicker/DatePicker.types.ts index 8ba281c9b024..beb26c092ee6 100644 --- a/packages/x-date-pickers/src/DatePicker/DatePicker.types.ts +++ b/packages/x-date-pickers/src/DatePicker/DatePicker.types.ts @@ -4,7 +4,7 @@ import { DesktopDatePickerSlots, DesktopDatePickerSlotProps, } from '../DesktopDatePicker'; -import { DefaultizedProps } from '../internals/models/helpers'; +import { MakeRequired } from '../internals/models/helpers'; import { BaseDateValidationProps } from '../internals/models/validation'; import { MobileDatePickerProps, @@ -62,8 +62,8 @@ export interface DatePickerProps< export type DatePickerFieldProps< TDate extends PickerValidDate, TEnableAccessibleFieldDOMStructure extends boolean = true, -> = DefaultizedProps< +> = MakeRequired< UseDateFieldProps, - 'format' | 'timezone' | keyof BaseDateValidationProps + 'format' | 'timezone' | 'value' | keyof BaseDateValidationProps > & BaseSingleInputFieldProps; diff --git a/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts b/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts index 6f82e44e481b..f603981c1c95 100644 --- a/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts +++ b/packages/x-date-pickers/src/DateTimeField/DateTimeField.types.ts @@ -52,32 +52,28 @@ export interface UseDateTimeFieldProps< ampm?: boolean; } -export type UseDateTimeFieldComponentProps< - TDate extends PickerValidDate, - TEnableAccessibleFieldDOMStructure extends boolean, - TChildProps extends {}, -> = Omit> & - UseDateTimeFieldProps; - export type DateTimeFieldProps< TDate extends PickerValidDate, TEnableAccessibleFieldDOMStructure extends boolean = true, -> = UseDateTimeFieldComponentProps< - TDate, - TEnableAccessibleFieldDOMStructure, - BuiltInFieldTextFieldProps -> & { - /** - * Overridable component slots. - * @default {} - */ - slots?: DateTimeFieldSlots; - /** - * The props used for each component slot. - * @default {} - */ - slotProps?: DateTimeFieldSlotProps; -}; +> = + // The hook props + UseDateTimeFieldProps & + // The TextField props + Omit< + BuiltInFieldTextFieldProps, + keyof UseDateTimeFieldProps + > & { + /** + * Overridable component slots. + * @default {} + */ + slots?: DateTimeFieldSlots; + /** + * The props used for each component slot. + * @default {} + */ + slotProps?: DateTimeFieldSlotProps; + }; export type DateTimeFieldOwnerState< TDate extends PickerValidDate, diff --git a/packages/x-date-pickers/src/DateTimeField/index.ts b/packages/x-date-pickers/src/DateTimeField/index.ts index 95952dde9474..7f8882b9d04a 100644 --- a/packages/x-date-pickers/src/DateTimeField/index.ts +++ b/packages/x-date-pickers/src/DateTimeField/index.ts @@ -1,7 +1,3 @@ export { DateTimeField } from './DateTimeField'; export { useDateTimeField as unstable_useDateTimeField } from './useDateTimeField'; -export type { - UseDateTimeFieldProps, - UseDateTimeFieldComponentProps, - DateTimeFieldProps, -} from './DateTimeField.types'; +export type { UseDateTimeFieldProps, DateTimeFieldProps } from './DateTimeField.types'; diff --git a/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.types.ts b/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.types.ts index a26c276930b4..d71f06ea1fc4 100644 --- a/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.types.ts +++ b/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.types.ts @@ -5,7 +5,7 @@ import { DesktopDateTimePickerSlotProps, } from '../DesktopDateTimePicker'; import { DateOrTimeViewWithMeridiem } from '../internals/models'; -import { DefaultizedProps } from '../internals/models/helpers'; +import { MakeRequired } from '../internals/models/helpers'; import { BaseDateValidationProps, BaseTimeValidationProps } from '../internals/models/validation'; import { MobileDateTimePickerProps, @@ -77,10 +77,11 @@ export interface DateTimePickerProps< export type DateTimePickerFieldProps< TDate extends PickerValidDate, TEnableAccessibleFieldDOMStructure extends boolean = true, -> = DefaultizedProps< +> = MakeRequired< UseDateTimeFieldProps, | 'format' | 'timezone' + | 'value' | 'ampm' | keyof BaseDateValidationProps | keyof BaseTimeValidationProps diff --git a/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx b/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx index 5e6fedaeba68..a13c557ae11d 100644 --- a/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx +++ b/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx @@ -9,7 +9,7 @@ import { DatePickerViewRenderers, useDatePickerDefaultizedProps } from '../DateP import { usePickersTranslations } from '../hooks/usePickersTranslations'; import { useUtils } from '../internals/hooks/useUtils'; import { validateDate, extractValidationProps } from '../validation'; -import { DateView, PickerValidDate } from '../models'; +import { DateView, PickerOwnerState, PickerValidDate } from '../models'; import { useDesktopPicker } from '../internals/hooks/useDesktopPicker'; import { CalendarIcon } from '../icons'; import { DateField } from '../DateField'; @@ -71,7 +71,7 @@ const DesktopDatePicker = React.forwardRef(function DesktopDatePicker< }, slotProps: { ...defaultizedProps.slotProps, - field: (ownerState: any) => ({ + field: (ownerState: PickerOwnerState) => ({ ...resolveComponentProps(defaultizedProps.slotProps?.field, ownerState), ...extractValidationProps(defaultizedProps), ref, diff --git a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx index c9d146bdf670..03c825722a80 100644 --- a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx +++ b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx @@ -24,7 +24,7 @@ import { resolveTimeViewsResponse, } from '../internals/utils/date-time-utils'; import { PickersActionBarAction } from '../PickersActionBar'; -import { PickerValidDate } from '../models'; +import { PickerOwnerState, PickerValidDate } from '../models'; import { renderDigitalClockTimeView, renderMultiSectionDigitalClockTimeView, @@ -42,6 +42,7 @@ import { UsePickerViewsProps } from '../internals/hooks/usePicker/usePickerViews import { isInternalTimeView } from '../internals/utils/time-utils'; import { isDatePickerView } from '../internals/utils/date-utils'; import { buildGetOpenDialogAriaText } from '../locales/utils/getPickersLocalization'; +import { PickersLayoutOwnerState } from '../PickersLayout'; const rendererInterceptor = function rendererInterceptor< TDate extends PickerValidDate, @@ -196,7 +197,7 @@ const DesktopDateTimePicker = React.forwardRef(function DesktopDateTimePicker< }, slotProps: { ...defaultizedProps.slotProps, - field: (ownerState: any) => ({ + field: (ownerState: PickerOwnerState) => ({ ...resolveComponentProps(defaultizedProps.slotProps?.field, ownerState), ...extractValidationProps(defaultizedProps), ref, @@ -211,7 +212,7 @@ const DesktopDateTimePicker = React.forwardRef(function DesktopDateTimePicker< hidden: true, ...defaultizedProps.slotProps?.tabs, }, - actionBar: (ownerState: any) => ({ + actionBar: (ownerState: PickersLayoutOwnerState) => ({ actions: actionBarActions, ...resolveComponentProps(defaultizedProps.slotProps?.actionBar, ownerState), }), diff --git a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx index 98eb55cfc7d9..a090898a3adc 100644 --- a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx +++ b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx @@ -20,7 +20,7 @@ import { PickersActionBarAction } from '../PickersActionBar'; import { TimeViewWithMeridiem } from '../internals/models'; import { resolveTimeFormat } from '../internals/utils/time-utils'; import { resolveTimeViewsResponse } from '../internals/utils/date-time-utils'; -import { TimeView, PickerValidDate } from '../models'; +import { TimeView, PickerValidDate, PickerOwnerState } from '../models'; import { buildGetOpenDialogAriaText } from '../locales/utils/getPickersLocalization'; type DesktopTimePickerComponent = (< @@ -104,7 +104,7 @@ const DesktopTimePicker = React.forwardRef(function DesktopTimePicker< }, slotProps: { ...defaultizedProps.slotProps, - field: (ownerState: any) => ({ + field: (ownerState: PickerOwnerState) => ({ ...resolveComponentProps(defaultizedProps.slotProps?.field, ownerState), ...extractValidationProps(defaultizedProps), ref, diff --git a/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx b/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx index 4448fda1dfd7..14400dd2e990 100644 --- a/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx +++ b/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx @@ -9,7 +9,7 @@ import { DatePickerViewRenderers, useDatePickerDefaultizedProps } from '../DateP import { usePickersTranslations } from '../hooks/usePickersTranslations'; import { useUtils } from '../internals/hooks/useUtils'; import { extractValidationProps, validateDate } from '../validation'; -import { DateView, PickerValidDate } from '../models'; +import { DateView, PickerOwnerState, PickerValidDate } from '../models'; import { DateField } from '../DateField'; import { singleItemValueManager } from '../internals/utils/valueManagers'; import { renderDateViewCalendar } from '../dateViewRenderers'; @@ -68,7 +68,7 @@ const MobileDatePicker = React.forwardRef(function MobileDatePicker< }, slotProps: { ...defaultizedProps.slotProps, - field: (ownerState: any) => ({ + field: (ownerState: PickerOwnerState) => ({ ...resolveComponentProps(defaultizedProps.slotProps?.field, ownerState), ...extractValidationProps(defaultizedProps), ref, diff --git a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx index e8781d3a83c2..8e597a58c28b 100644 --- a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx +++ b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx @@ -13,7 +13,7 @@ import { import { usePickersTranslations } from '../hooks/usePickersTranslations'; import { useUtils } from '../internals/hooks/useUtils'; import { extractValidationProps, validateDateTime } from '../validation'; -import { DateOrTimeView, PickerValidDate } from '../models'; +import { DateOrTimeView, PickerOwnerState, PickerValidDate } from '../models'; import { useMobilePicker } from '../internals/hooks/useMobilePicker'; import { renderDateViewCalendar } from '../dateViewRenderers'; import { renderTimeViewClock } from '../timeViewRenderers'; @@ -78,7 +78,7 @@ const MobileDateTimePicker = React.forwardRef(function MobileDateTimePicker< }, slotProps: { ...defaultizedProps.slotProps, - field: (ownerState: any) => ({ + field: (ownerState: PickerOwnerState) => ({ ...resolveComponentProps(defaultizedProps.slotProps?.field, ownerState), ...extractValidationProps(defaultizedProps), ref, diff --git a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx index 5106ae24d54a..409dd225325f 100644 --- a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx +++ b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx @@ -10,7 +10,7 @@ import { TimePickerViewRenderers, useTimePickerDefaultizedProps } from '../TimeP import { usePickersTranslations } from '../hooks/usePickersTranslations'; import { useUtils } from '../internals/hooks/useUtils'; import { extractValidationProps, validateTime } from '../validation'; -import { PickerValidDate, TimeView } from '../models'; +import { PickerOwnerState, PickerValidDate, TimeView } from '../models'; import { useMobilePicker } from '../internals/hooks/useMobilePicker'; import { renderTimeViewClock } from '../timeViewRenderers'; import { resolveTimeFormat } from '../internals/utils/time-utils'; @@ -71,7 +71,7 @@ const MobileTimePicker = React.forwardRef(function MobileTimePicker< }, slotProps: { ...defaultizedProps.slotProps, - field: (ownerState: any) => ({ + field: (ownerState: PickerOwnerState) => ({ ...resolveComponentProps(defaultizedProps.slotProps?.field, ownerState), ...extractValidationProps(defaultizedProps), ref, diff --git a/packages/x-date-pickers/src/PickersLayout/PickersLayout.types.ts b/packages/x-date-pickers/src/PickersLayout/PickersLayout.types.ts index 69a3fac2cdc2..c63477c40062 100644 --- a/packages/x-date-pickers/src/PickersLayout/PickersLayout.types.ts +++ b/packages/x-date-pickers/src/PickersLayout/PickersLayout.types.ts @@ -12,7 +12,7 @@ import { ExportedPickersShortcutProps, PickersShortcuts, } from '../PickersShortcuts/PickersShortcuts'; -import { PickerValidDate } from '../models'; +import { PickerOwnerState, PickerValidDate } from '../models'; export interface ExportedPickersLayoutSlots< TValue, @@ -38,16 +38,9 @@ export interface ExportedPickersLayoutSlots< >; } -interface PickersLayoutActionBarOwnerState< - TValue, - TDate extends PickerValidDate, - TView extends DateOrTimeViewWithMeridiem, -> extends PickersLayoutProps { - wrapperVariant: WrapperVariant; -} - -interface PickersShortcutsOwnerState extends PickersShortcutsProps { +export interface PickersLayoutOwnerState extends PickerOwnerState { wrapperVariant: WrapperVariant; + isLandscape: boolean; } export interface ExportedPickersLayoutSlotProps< @@ -58,15 +51,11 @@ export interface ExportedPickersLayoutSlotProps< /** * Props passed down to the action bar component. */ - actionBar?: SlotComponentProps< - typeof PickersActionBar, - {}, - PickersLayoutActionBarOwnerState - >; + actionBar?: SlotComponentProps; /** * Props passed down to the shortcuts component. */ - shortcuts?: SlotComponentProps>; + shortcuts?: SlotComponentProps; /** * Props passed down to the layoutRoot component. */ diff --git a/packages/x-date-pickers/src/PickersLayout/index.ts b/packages/x-date-pickers/src/PickersLayout/index.ts index f2746aaa5d35..817295fc1c42 100644 --- a/packages/x-date-pickers/src/PickersLayout/index.ts +++ b/packages/x-date-pickers/src/PickersLayout/index.ts @@ -5,6 +5,7 @@ export type { PickersLayoutSlotProps, ExportedPickersLayoutSlots, ExportedPickersLayoutSlotProps, + PickersLayoutOwnerState, } from './PickersLayout.types'; export { default as usePickerLayout } from './usePickerLayout'; diff --git a/packages/x-date-pickers/src/PickersLayout/usePickerLayout.tsx b/packages/x-date-pickers/src/PickersLayout/usePickerLayout.tsx index 7f9b8ad28ddf..aa0b068d1a67 100644 --- a/packages/x-date-pickers/src/PickersLayout/usePickerLayout.tsx +++ b/packages/x-date-pickers/src/PickersLayout/usePickerLayout.tsx @@ -3,12 +3,13 @@ import * as React from 'react'; import useSlotProps from '@mui/utils/useSlotProps'; import composeClasses from '@mui/utils/composeClasses'; import { PickersActionBar, PickersActionBarAction } from '../PickersActionBar'; -import { PickersLayoutProps, SubComponents } from './PickersLayout.types'; -import { getPickersLayoutUtilityClass } from './pickersLayoutClasses'; +import { PickersLayoutOwnerState, PickersLayoutProps, SubComponents } from './PickersLayout.types'; +import { getPickersLayoutUtilityClass, PickersLayoutClasses } from './pickersLayoutClasses'; import { PickersShortcuts } from '../PickersShortcuts'; import { BaseToolbarProps } from '../internals/models/props/toolbar'; import { DateOrTimeViewWithMeridiem } from '../internals/models'; import { PickerValidDate } from '../models'; +import { usePickersPrivateContext } from '../internals/hooks/usePickersPrivateContext'; function toolbarHasView( toolbarProps: BaseToolbarProps | any, @@ -16,8 +17,11 @@ function toolbarHasView( return toolbarProps.view !== null; } -const useUtilityClasses = (ownerState: PickersLayoutProps) => { - const { classes, isLandscape } = ownerState; +const useUtilityClasses = ( + classes: Partial | undefined, + ownerState: PickersLayoutOwnerState, +) => { + const { isLandscape } = ownerState; const slots = { root: ['root', isLandscape && 'landscape'], contentWrapper: ['contentWrapper'], @@ -47,6 +51,8 @@ const usePickerLayout = < >( props: PickersLayoutProps, ): UsePickerLayoutResponse => { + const { ownerState: pickersOwnerState } = usePickersPrivateContext(); + const { wrapperVariant, onAccept, @@ -66,13 +72,19 @@ const usePickerLayout = < children, slots, slotProps, + classes: classesProp, // TODO: Remove this "as" hack. It get introduced to mark `value` prop in PickersLayoutProps as not required. // The true type should be // - For pickers value: TDate | null // - For range pickers value: [TDate | null, TDate | null] } = props as PickersLayoutPropsWithValueRequired; - const classes = useUtilityClasses(props); + const ownerState: PickersLayoutOwnerState = { + ...pickersOwnerState, + wrapperVariant, + isLandscape, + }; + const classes = useUtilityClasses(classesProp, ownerState); // Action bar const ActionBar = slots?.actionBar ?? PickersActionBar; @@ -88,7 +100,7 @@ const usePickerLayout = < wrapperVariant === 'desktop' ? [] : (['cancel', 'accept'] as PickersActionBarAction[]), }, className: classes.actionBar, - ownerState: { ...props, wrapperVariant }, + ownerState, }); const actionBar = ; @@ -108,7 +120,7 @@ const usePickerLayout = < readOnly, }, className: classes.toolbar, - ownerState: { ...props, wrapperVariant }, + ownerState, }); const toolbar = toolbarHasView(toolbarProps) && !!Toolbar ? : null; @@ -133,12 +145,7 @@ const usePickerLayout = < onChange: onSelectShortcut, }, className: classes.shortcuts, - ownerState: { - isValid, - isLandscape, - onChange: onSelectShortcut, - wrapperVariant, - }, + ownerState, }); const shortcuts = view && !!Shortcuts ? : null; diff --git a/packages/x-date-pickers/src/TimeField/TimeField.types.ts b/packages/x-date-pickers/src/TimeField/TimeField.types.ts index e60bc69b4703..03d2c63932c3 100644 --- a/packages/x-date-pickers/src/TimeField/TimeField.types.ts +++ b/packages/x-date-pickers/src/TimeField/TimeField.types.ts @@ -39,32 +39,28 @@ export interface UseTimeFieldProps< ampm?: boolean; } -export type UseTimeFieldComponentProps< - TDate extends PickerValidDate, - TEnableAccessibleFieldDOMStructure extends boolean, - TChildProps extends {}, -> = Omit> & - UseTimeFieldProps; - export type TimeFieldProps< TDate extends PickerValidDate, TEnableAccessibleFieldDOMStructure extends boolean = true, -> = UseTimeFieldComponentProps< - TDate, - TEnableAccessibleFieldDOMStructure, - BuiltInFieldTextFieldProps -> & { - /** - * Overridable component slots. - * @default {} - */ - slots?: TimeFieldSlots; - /** - * The props used for each component slot. - * @default {} - */ - slotProps?: TimeFieldSlotProps; -}; +> = + // The hook props + UseTimeFieldProps & + // The TextField props + Omit< + BuiltInFieldTextFieldProps, + keyof UseTimeFieldProps + > & { + /** + * Overridable component slots. + * @default {} + */ + slots?: TimeFieldSlots; + /** + * The props used for each component slot. + * @default {} + */ + slotProps?: TimeFieldSlotProps; + }; export type TimeFieldOwnerState< TDate extends PickerValidDate, diff --git a/packages/x-date-pickers/src/TimeField/index.ts b/packages/x-date-pickers/src/TimeField/index.ts index f335f0f8fd76..b721d815213c 100644 --- a/packages/x-date-pickers/src/TimeField/index.ts +++ b/packages/x-date-pickers/src/TimeField/index.ts @@ -1,7 +1,3 @@ export { TimeField } from './TimeField'; export { useTimeField as unstable_useTimeField } from './useTimeField'; -export type { - UseTimeFieldProps, - UseTimeFieldComponentProps, - TimeFieldProps, -} from './TimeField.types'; +export type { UseTimeFieldProps, TimeFieldProps } from './TimeField.types'; diff --git a/packages/x-date-pickers/src/TimePicker/TimePicker.types.ts b/packages/x-date-pickers/src/TimePicker/TimePicker.types.ts index a5ba2479c1e8..c8a39b3ce190 100644 --- a/packages/x-date-pickers/src/TimePicker/TimePicker.types.ts +++ b/packages/x-date-pickers/src/TimePicker/TimePicker.types.ts @@ -4,7 +4,7 @@ import { DesktopTimePickerSlotProps, } from '../DesktopTimePicker'; import { TimeViewWithMeridiem } from '../internals/models'; -import { DefaultizedProps } from '../internals/models/helpers'; +import { MakeRequired } from '../internals/models/helpers'; import { BaseTimeValidationProps } from '../internals/models/validation'; import { MobileTimePickerProps, @@ -62,8 +62,8 @@ export interface TimePickerProps< export type TimePickerFieldProps< TDate extends PickerValidDate, TEnableAccessibleFieldDOMStructure extends boolean = true, -> = DefaultizedProps< +> = MakeRequired< UseTimeFieldProps, - 'format' | 'timezone' | 'ampm' | keyof BaseTimeValidationProps + 'format' | 'timezone' | 'value' | 'ampm' | keyof BaseTimeValidationProps > & BaseSingleInputFieldProps; diff --git a/packages/x-date-pickers/src/internals/components/PickersProvider.tsx b/packages/x-date-pickers/src/internals/components/PickersProvider.tsx index 8dd0654a846c..758fe02252f3 100644 --- a/packages/x-date-pickers/src/internals/components/PickersProvider.tsx +++ b/packages/x-date-pickers/src/internals/components/PickersProvider.tsx @@ -1,10 +1,12 @@ import * as React from 'react'; -import { PickerValidDate } from '../../models'; +import { PickerOwnerState, PickerValidDate } from '../../models'; import { PickersInputLocaleText } from '../../locales'; import { LocalizationProvider } from '../../LocalizationProvider'; export const PickersContext = React.createContext(null); +export const PickersPrivateContext = React.createContext(null); + /** * Provides the context for the various parts of a picker component: * - contextValue: the context for the picker sub-components. @@ -12,20 +14,21 @@ export const PickersContext = React.createContext(nu * * @ignore - do not document. */ -export function PickersProvider( - props: PickersFieldProviderProps, -) { - const { contextValue, localeText, children } = props; +export function PickersProvider(props: PickersProviderProps) { + const { contextValue, privateContextValue, localeText, children } = props; return ( - {children} + + {children} + ); } -interface PickersFieldProviderProps { +export interface PickersProviderProps { contextValue: PickersContextValue; + privateContextValue: PickersPrivateContextValue; localeText: PickersInputLocaleText | undefined; children: React.ReactNode; } @@ -46,3 +49,9 @@ export interface PickersContextValue { */ open: boolean; } +export interface PickersPrivateContextValue { + /** + * The ownerState of the picker. + */ + ownerState: PickerOwnerState; +} diff --git a/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx b/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx index a874ca8cd3f1..c564adee2636 100644 --- a/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx @@ -18,6 +18,7 @@ import { FieldRef, BaseSingleInputFieldProps, InferError, + PickerOwnerState, } from '../../../models'; import { DateOrTimeViewWithMeridiem } from '../../models'; import { PickersProvider } from '../../components/PickersProvider'; @@ -76,15 +77,16 @@ export const useDesktopPicker = < actions, hasUIView, layoutProps, + providerProps, renderCurrentView, shouldRestoreFocus, fieldProps: pickerFieldProps, - contextValue, ownerState, } = usePicker({ ...pickerParams, props, fieldRef, + localeText, autoFocusView: true, additionalViewProps: {}, wrapperVariant: 'desktop', @@ -97,7 +99,7 @@ export const useDesktopPicker = < additionalProps: { position: 'end' as const, }, - ownerState: props, + ownerState, }); const OpenPickerButton = slots.openPickerButton ?? IconButton; @@ -110,7 +112,7 @@ export const useDesktopPicker = < 'aria-label': getOpenDialogAriaText(pickerFieldProps.value), edge: inputAdornmentProps.position, }, - ownerState: props, + ownerState, }); const OpenPickerIcon = slots.openPickerIcon; @@ -133,7 +135,7 @@ export const useDesktopPicker = < InferError > >, - TExternalProps + PickerOwnerState >({ elementType: Field, externalSlotProps: innerSlotProps?.field, @@ -156,7 +158,7 @@ export const useDesktopPicker = < focused: open ? true : undefined, ...(inputRef ? { inputRef } : {}), }, - ownerState: props, + ownerState, }); // TODO: Move to `useSlotProps` when https://github.com/mui/material-ui/pull/35088 will be merged @@ -208,7 +210,7 @@ export const useDesktopPicker = < const handleFieldRef = useForkRef(fieldRef, fieldProps.unstableFieldRef); const renderPicker = () => ( - + , {}, - UsePickerProps + PickerOwnerState >; textField?: SlotComponentProps>; - inputAdornment?: Partial; - openPickerButton?: SlotComponentProps< - typeof IconButton, - {}, - UseDesktopPickerProps - >; - openPickerIcon?: SlotComponentPropsFromProps< - Record, - {}, - PickerOwnerState - >; + inputAdornment?: SlotComponentPropsFromProps; + openPickerButton?: SlotComponentProps; + openPickerIcon?: SlotComponentPropsFromProps, {}, PickerOwnerState>; } export interface DesktopOnlyPickerProps diff --git a/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx b/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx index 89108c2cb621..7fce7070a4db 100644 --- a/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx @@ -17,6 +17,7 @@ import { PickerValidDate, FieldRef, InferError, + PickerOwnerState, } from '../../../models'; import { DateOrTimeViewWithMeridiem } from '../../models'; import { PickersProvider } from '../../components/PickersProvider'; @@ -71,13 +72,15 @@ export const useMobilePicker = < open, actions, layoutProps, + providerProps, renderCurrentView, fieldProps: pickerFieldProps, - contextValue, + ownerState, } = usePicker({ ...pickerParams, props, fieldRef, + localeText, autoFocusView: true, additionalViewProps: {}, wrapperVariant: 'mobile', @@ -96,7 +99,7 @@ export const useMobilePicker = < InferError > >, - TExternalProps + PickerOwnerState >({ elementType: Field, externalSlotProps: innerSlotProps?.field, @@ -121,7 +124,7 @@ export const useMobilePicker = < name, ...(inputRef ? { inputRef } : {}), }, - ownerState: props, + ownerState, }); // TODO: Move to `useSlotProps` when https://github.com/mui/material-ui/pull/35088 will be merged @@ -160,7 +163,7 @@ export const useMobilePicker = < const handleFieldRef = useForkRef(fieldRef, fieldProps.unstableFieldRef); const renderPicker = () => ( - + , {}, - UsePickerProps + PickerOwnerState >; textField?: SlotComponentProps>; } diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.ts index 45c351390d05..4a9aeafff9d3 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.ts @@ -6,6 +6,7 @@ import { usePickerLayoutProps } from './usePickerLayoutProps'; import { FieldSection, PickerValidDate, InferError } from '../../../models'; import { DateOrTimeViewWithMeridiem } from '../../models'; import { usePickerOwnerState } from './usePickerOwnerState'; +import { usePickerProvider } from './usePickerProvider'; export const usePicker = < TValue, @@ -24,6 +25,7 @@ export const usePicker = < autoFocusView, rendererInterceptor, fieldRef, + localeText, }: UsePickerParams< TValue, TDate, @@ -31,7 +33,7 @@ export const usePicker = < TSection, TExternalProps, TAdditionalProps ->): UsePickerResponse> => { +>): UsePickerResponse> => { if (process.env.NODE_ENV !== 'production') { if ((props as any).renderInput != null) { warnOnce([ @@ -72,7 +74,13 @@ export const usePicker = < propsFromPickerViews: pickerViewsResponse.layoutProps, }); - const pickerOwnerState = usePickerOwnerState({ props, pickerValueResponse }); + const pickerOwnerState = usePickerOwnerState({ props, pickerValueResponse, valueManager }); + + const providerProps = usePickerProvider({ + pickerValueResponse, + ownerState: pickerOwnerState, + localeText, + }); return { // Picker value @@ -88,8 +96,8 @@ export const usePicker = < // Picker layout layoutProps: pickerLayoutResponse.layoutProps, - // Picker context - contextValue: pickerValueResponse.contextValue, + // Picker provider + providerProps, // Picker owner state ownerState: pickerOwnerState, diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts index 5bc23631a90d..2c3759d734d0 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts @@ -13,6 +13,7 @@ import { import { UsePickerLayoutProps, UsePickerLayoutPropsResponse } from './usePickerLayoutProps'; import { FieldSection, PickerOwnerState, PickerValidDate } from '../../../models'; import { DateOrTimeViewWithMeridiem } from '../../models'; +import { UsePickerProviderParameters, UsePickerProviderReturnValue } from './usePickerProvider'; /** * Props common to all picker headless implementations. @@ -53,17 +54,20 @@ export interface UsePickerParams< Pick< UsePickerViewParams, 'additionalViewProps' | 'autoFocusView' | 'rendererInterceptor' | 'fieldRef' - > { + >, + Pick, 'localeText'> { props: TExternalProps; } export interface UsePickerResponse< TValue, + TDate extends PickerValidDate, TView extends DateOrTimeViewWithMeridiem, TSection extends FieldSection, TError, > extends Omit, 'viewProps' | 'layoutProps'>, Omit, 'layoutProps'>, UsePickerLayoutPropsResponse { - ownerState: PickerOwnerState; + ownerState: PickerOwnerState; + providerProps: UsePickerProviderReturnValue; } diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerOwnerState.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerOwnerState.ts index 07b5fbfccebb..7fe68d2d836b 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerOwnerState.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerOwnerState.ts @@ -1,25 +1,40 @@ import * as React from 'react'; import { FieldSection, PickerOwnerState } from '../../../models'; import type { UsePickerProps } from './usePicker.types'; -import { UsePickerValueResponse } from './usePickerValue.types'; +import { PickerValueManager, UsePickerValueResponse } from './usePickerValue.types'; +import { useUtils } from '../useUtils'; interface UsePickerOwnerStateParameters { props: UsePickerProps; pickerValueResponse: UsePickerValueResponse; + valueManager: PickerValueManager; } export function usePickerOwnerState( parameters: UsePickerOwnerStateParameters, -): PickerOwnerState { - const { props, pickerValueResponse } = parameters; +): PickerOwnerState { + const { props, pickerValueResponse, valueManager } = parameters; + + const utils = useUtils(); return React.useMemo( () => ({ - value: pickerValueResponse.viewProps.value, - open: pickerValueResponse.open, - disabled: props.disabled ?? false, - readOnly: props.readOnly ?? false, + isPickerValueEmpty: valueManager.areValuesEqual( + utils, + pickerValueResponse.viewProps.value, + valueManager.emptyValue, + ), + isPickerOpen: pickerValueResponse.open, + isPickerDisabled: props.disabled ?? false, + isPickerReadOnly: props.readOnly ?? false, }), - [pickerValueResponse.viewProps.value, pickerValueResponse.open, props.disabled, props.readOnly], + [ + utils, + valueManager, + pickerValueResponse.viewProps.value, + pickerValueResponse.open, + props.disabled, + props.readOnly, + ], ); } diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerProvider.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerProvider.ts new file mode 100644 index 000000000000..3bff7c032e67 --- /dev/null +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerProvider.ts @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { FieldSection, PickerOwnerState, PickerValidDate } from '../../../models'; +import { UsePickerValueResponse } from './usePickerValue.types'; +import { + PickersProviderProps, + PickersContextValue, + PickersPrivateContextValue, +} from '../../components/PickersProvider'; + +export interface UsePickerProviderParameters + extends Pick, 'localeText'> { + pickerValueResponse: UsePickerValueResponse; + ownerState: PickerOwnerState; +} + +export interface UsePickerProviderReturnValue + extends Omit, 'children'> {} + +export function usePickerProvider( + parameters: UsePickerProviderParameters, +): UsePickerProviderReturnValue { + const { pickerValueResponse, ownerState, localeText } = parameters; + + const contextValue = React.useMemo( + () => ({ + onOpen: pickerValueResponse.actions.onOpen, + onClose: pickerValueResponse.actions.onClose, + open: pickerValueResponse.open, + }), + [ + pickerValueResponse.actions.onOpen, + pickerValueResponse.actions.onClose, + pickerValueResponse.open, + ], + ); + + const privateContextValue = React.useMemo( + () => ({ ownerState }), + [ownerState], + ); + + return { + localeText, + contextValue, + privateContextValue, + }; +} diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.ts index ae7553494e0a..8d6255b5f562 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.ts @@ -28,7 +28,6 @@ import { PickerValueUpdaterParams, } from './usePickerValue.types'; import { useValueWithTimezone } from '../useValueWithTimezone'; -import { PickersContextValue } from '../../components/PickersProvider'; /** * Decide if the new value should be published @@ -458,21 +457,11 @@ export const usePickerValue = < isValid, }; - const contextValue = React.useMemo( - () => ({ - onOpen: handleOpen, - onClose: handleClose, - open: isOpen, - }), - [isOpen, handleClose, handleOpen], - ); - return { open: isOpen, fieldProps: fieldResponse, viewProps: viewResponse, layoutProps: layoutResponse, actions, - contextValue, }; }; diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.types.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.types.ts index 5b5952a1d3d0..a2882f62f57f 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.types.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.types.ts @@ -17,7 +17,6 @@ import { PickerShortcutChangeImportance, PickersShortcutsItemContext, } from '../../../PickersShortcuts'; -import { PickersContextValue } from '../../components/PickersProvider'; export interface PickerValueManager { /** @@ -328,5 +327,4 @@ export interface UsePickerValueResponse; fieldProps: UsePickerValueFieldResponse; layoutProps: UsePickerValueLayoutResponse; - contextValue: PickersContextValue; } diff --git a/packages/x-date-pickers/src/internals/hooks/usePickersPrivateContext.ts b/packages/x-date-pickers/src/internals/hooks/usePickersPrivateContext.ts new file mode 100644 index 000000000000..9d03e894944a --- /dev/null +++ b/packages/x-date-pickers/src/internals/hooks/usePickersPrivateContext.ts @@ -0,0 +1,17 @@ +'use client'; +import * as React from 'react'; +import { PickersContext, PickersPrivateContextValue } from '../components/PickersProvider'; + +/** + * Returns the private context passed by the picker that wraps the current component. + */ +export const usePickersPrivateContext = () => { + const value = React.useContext(PickersContext) as PickersPrivateContextValue | null; + if (value == null) { + throw new Error( + 'MUI X: The `usePickersPrivateContext` can only be called in components that are used inside a picker component.', + ); + } + + return value; +}; diff --git a/packages/x-date-pickers/src/internals/hooks/useStaticPicker/useStaticPicker.tsx b/packages/x-date-pickers/src/internals/hooks/useStaticPicker/useStaticPicker.tsx index 08873f535dd2..9d06d0e630c3 100644 --- a/packages/x-date-pickers/src/internals/hooks/useStaticPicker/useStaticPicker.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useStaticPicker/useStaticPicker.tsx @@ -3,7 +3,7 @@ import clsx from 'clsx'; import { styled } from '@mui/material/styles'; import { UseStaticPickerParams, UseStaticPickerProps } from './useStaticPicker.types'; import { usePicker } from '../usePicker'; -import { LocalizationProvider } from '../../../LocalizationProvider'; +import { PickersProvider } from '../../components/PickersProvider'; import { PickersLayout } from '../../../PickersLayout'; import { DIALOG_WIDTH } from '../../constants/dimensions'; import { FieldSection, PickerValidDate } from '../../../models'; @@ -32,7 +32,7 @@ export const useStaticPicker = < }: UseStaticPickerParams) => { const { localeText, slots, slotProps, className, sx, displayStaticWrapperAs, autoFocus } = props; - const { layoutProps, renderCurrentView } = usePicker< + const { layoutProps, providerProps, renderCurrentView } = usePicker< TDate | null, TDate, TView, @@ -44,6 +44,7 @@ export const useStaticPicker = < props, autoFocusView: autoFocus ?? false, fieldRef: undefined, + localeText, additionalViewProps: {}, wrapperVariant: displayStaticWrapperAs, }); @@ -51,7 +52,7 @@ export const useStaticPicker = < const Layout = slots?.layout ?? PickerStaticLayout; const renderPicker = () => ( - + {renderCurrentView()} - + ); return { renderPicker }; diff --git a/packages/x-date-pickers/src/internals/index.ts b/packages/x-date-pickers/src/internals/index.ts index 010b5eda898d..49700d346134 100644 --- a/packages/x-date-pickers/src/internals/index.ts +++ b/packages/x-date-pickers/src/internals/index.ts @@ -104,7 +104,12 @@ export type { export type { BaseClockProps, DesktopOnlyTimePickerProps } from './models/props/clock'; export type { BaseTabsProps, ExportedBaseTabsProps } from './models/props/tabs'; export type { BaseToolbarProps, ExportedBaseToolbarProps } from './models/props/toolbar'; -export type { DefaultizedProps, MakeOptional, SlotComponentPropsFromProps } from './models/helpers'; +export type { + DefaultizedProps, + MakeOptional, + MakeRequired, + SlotComponentPropsFromProps, +} from './models/helpers'; export type { WrapperVariant, TimeViewWithMeridiem, diff --git a/packages/x-date-pickers/src/internals/models/fields.ts b/packages/x-date-pickers/src/internals/models/fields.ts index e02b43315f56..e3097519c27d 100644 --- a/packages/x-date-pickers/src/internals/models/fields.ts +++ b/packages/x-date-pickers/src/internals/models/fields.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import type { UseFieldInternalProps } from '../hooks/useField'; -import { FieldSection, PickerValidDate } from '../../models'; +import { FieldSection, PickerOwnerState, PickerValidDate } from '../../models'; import type { ExportedUseClearableFieldProps } from '../../hooks/useClearableField'; export interface BaseFieldProps< @@ -18,4 +18,5 @@ export interface BaseFieldProps< format?: string; disabled?: boolean; ref?: React.Ref; + ownerState?: PickerOwnerState; } diff --git a/packages/x-date-pickers/src/models/pickers.ts b/packages/x-date-pickers/src/models/pickers.ts index c15c2e5d1e71..d255b1519e29 100644 --- a/packages/x-date-pickers/src/models/pickers.ts +++ b/packages/x-date-pickers/src/models/pickers.ts @@ -15,21 +15,21 @@ export type PickerValidDate = keyof PickerValidDateLookup extends never ? any : PickerValidDateLookup[keyof PickerValidDateLookup]; -export interface PickerOwnerState { +export interface PickerOwnerState { /** - * The value currently displayed in the field and in the view. + * `true` if the value is currently empty. */ - value: TValue; + isPickerValueEmpty: boolean; /** * `true` if the picker is open, `false` otherwise. */ - open: boolean; + isPickerOpen: boolean; /** * `true` if the picker is disabled, `false` otherwise. */ - disabled: boolean; + isPickerDisabled: boolean; /** * `true` if the picker is read-only, `false` otherwise. */ - readOnly: boolean; + isPickerReadOnly: boolean; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8b113498a62..20f5d2afcc95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -620,6 +620,9 @@ importers: recast: specifier: ^0.23.9 version: 0.23.9 + rifm: + specifier: 0.12.1 + version: 0.12.1(react@18.3.1) rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -8910,6 +8913,11 @@ packages: rfdc@1.3.1: resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} + rifm@0.12.1: + resolution: {integrity: sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg==} + peerDependencies: + react: '>=16.8' + rimraf@2.6.3: resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -18795,6 +18803,10 @@ snapshots: rfdc@1.3.1: {} + rifm@0.12.1(react@18.3.1): + dependencies: + react: 18.3.1 + rimraf@2.6.3: dependencies: glob: 7.2.3 diff --git a/scripts/x-data-grid-premium.exports.json b/scripts/x-data-grid-premium.exports.json index 7cd4d3eeba42..8a0a612168f7 100644 --- a/scripts/x-data-grid-premium.exports.json +++ b/scripts/x-data-grid-premium.exports.json @@ -74,15 +74,15 @@ { "name": "GRID_DATETIME_COL_DEF", "kind": "Variable" }, { "name": "GRID_DEFAULT_LOCALE_TEXT", "kind": "Variable" }, { "name": "GRID_DETAIL_PANEL_TOGGLE_COL_DEF", "kind": "Variable" }, - { "name": "GRID_DETAIL_PANEL_TOGGLE_FIELD", "kind": "Variable" }, + { "name": "GRID_DETAIL_PANEL_TOGGLE_FIELD", "kind": "ImportSpecifier" }, { "name": "GRID_EXPERIMENTAL_ENABLED", "kind": "Variable" }, { "name": "GRID_NUMERIC_COL_DEF", "kind": "Variable" }, { "name": "GRID_REORDER_COL_DEF", "kind": "Variable" }, { "name": "GRID_ROOT_GROUP_ID", "kind": "Variable" }, - { "name": "GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD", "kind": "Variable" }, + { "name": "GRID_ROW_GROUPING_SINGLE_GROUPING_FIELD", "kind": "ImportSpecifier" }, { "name": "GRID_SINGLE_SELECT_COL_DEF", "kind": "Variable" }, { "name": "GRID_STRING_COL_DEF", "kind": "Variable" }, - { "name": "GRID_TREE_DATA_GROUPING_FIELD", "kind": "Variable" }, + { "name": "GRID_TREE_DATA_GROUPING_FIELD", "kind": "ImportSpecifier" }, { "name": "GridActionsCell", "kind": "Function" }, { "name": "GridActionsCellItem", "kind": "Variable" }, { "name": "GridActionsCellItemProps", "kind": "TypeAlias" }, @@ -638,7 +638,7 @@ { "name": "gridVisibleRowsLookupSelector", "kind": "Variable" }, { "name": "GridWorkspacesIcon", "kind": "Variable" }, { "name": "isAutogeneratedRow", "kind": "Variable" }, - { "name": "isGroupingColumn", "kind": "Variable" }, + { "name": "isGroupingColumn", "kind": "ImportSpecifier" }, { "name": "isLeaf", "kind": "Function" }, { "name": "LicenseInfo", "kind": "Class" }, { "name": "LoadingOverlayPropsOverrides", "kind": "Interface" }, diff --git a/scripts/x-data-grid-pro.exports.json b/scripts/x-data-grid-pro.exports.json index 9d5b4e8b5799..20dd708a55fd 100644 --- a/scripts/x-data-grid-pro.exports.json +++ b/scripts/x-data-grid-pro.exports.json @@ -69,14 +69,14 @@ { "name": "GRID_DATETIME_COL_DEF", "kind": "Variable" }, { "name": "GRID_DEFAULT_LOCALE_TEXT", "kind": "Variable" }, { "name": "GRID_DETAIL_PANEL_TOGGLE_COL_DEF", "kind": "Variable" }, - { "name": "GRID_DETAIL_PANEL_TOGGLE_FIELD", "kind": "Variable" }, + { "name": "GRID_DETAIL_PANEL_TOGGLE_FIELD", "kind": "ImportSpecifier" }, { "name": "GRID_EXPERIMENTAL_ENABLED", "kind": "Variable" }, { "name": "GRID_NUMERIC_COL_DEF", "kind": "Variable" }, { "name": "GRID_REORDER_COL_DEF", "kind": "Variable" }, { "name": "GRID_ROOT_GROUP_ID", "kind": "Variable" }, { "name": "GRID_SINGLE_SELECT_COL_DEF", "kind": "Variable" }, { "name": "GRID_STRING_COL_DEF", "kind": "Variable" }, - { "name": "GRID_TREE_DATA_GROUPING_FIELD", "kind": "Variable" }, + { "name": "GRID_TREE_DATA_GROUPING_FIELD", "kind": "ImportSpecifier" }, { "name": "GridActionsCell", "kind": "Function" }, { "name": "GridActionsCellItem", "kind": "Variable" }, { "name": "GridActionsCellItemProps", "kind": "TypeAlias" }, diff --git a/scripts/x-date-pickers-pro.exports.json b/scripts/x-date-pickers-pro.exports.json index 7bce2723c04e..581ce00f24b3 100644 --- a/scripts/x-date-pickers-pro.exports.json +++ b/scripts/x-date-pickers-pro.exports.json @@ -61,6 +61,7 @@ { "name": "DateRangePickerDayClasses", "kind": "Interface" }, { "name": "DateRangePickerDayClassKey", "kind": "TypeAlias" }, { "name": "DateRangePickerDayProps", "kind": "Interface" }, + { "name": "DateRangePickerFieldProps", "kind": "TypeAlias" }, { "name": "DateRangePickerProps", "kind": "Interface" }, { "name": "DateRangePickerSlotProps", "kind": "Interface" }, { "name": "DateRangePickerSlots", "kind": "Interface" }, @@ -89,6 +90,7 @@ { "name": "DateTimePickerToolbarClassKey", "kind": "TypeAlias" }, { "name": "DateTimePickerToolbarProps", "kind": "Interface" }, { "name": "DateTimeRangePicker", "kind": "Variable" }, + { "name": "DateTimeRangePickerFieldProps", "kind": "TypeAlias" }, { "name": "DateTimeRangePickerProps", "kind": "Interface" }, { "name": "DateTimeRangePickerSlotProps", "kind": "Interface" }, { "name": "DateTimeRangePickerSlots", "kind": "Interface" }, @@ -292,6 +294,7 @@ { "name": "pickersLayoutClasses", "kind": "Variable" }, { "name": "PickersLayoutClassKey", "kind": "TypeAlias" }, { "name": "PickersLayoutContentWrapper", "kind": "Variable" }, + { "name": "PickersLayoutOwnerState", "kind": "Interface" }, { "name": "PickersLayoutProps", "kind": "Interface" }, { "name": "PickersLayoutRoot", "kind": "Variable" }, { "name": "PickersLayoutSlotProps", "kind": "Interface" }, @@ -409,10 +412,8 @@ { "name": "UseClearableFieldResponse", "kind": "TypeAlias" }, { "name": "UseClearableFieldSlotProps", "kind": "Interface" }, { "name": "UseClearableFieldSlots", "kind": "Interface" }, - { "name": "UseDateFieldComponentProps", "kind": "TypeAlias" }, { "name": "UseDateFieldProps", "kind": "Interface" }, { "name": "UseDateRangeFieldProps", "kind": "Interface" }, - { "name": "UseDateTimeFieldComponentProps", "kind": "TypeAlias" }, { "name": "UseDateTimeFieldProps", "kind": "Interface" }, { "name": "UseMultiInputDateRangeFieldComponentProps", "kind": "TypeAlias" }, { "name": "UseMultiInputDateRangeFieldProps", "kind": "Interface" }, @@ -428,7 +429,6 @@ { "name": "UseSingleInputDateTimeRangeFieldProps", "kind": "Interface" }, { "name": "UseSingleInputTimeRangeFieldProps", "kind": "Interface" }, { "name": "useSplitFieldProps", "kind": "Variable" }, - { "name": "UseTimeFieldComponentProps", "kind": "TypeAlias" }, { "name": "UseTimeFieldProps", "kind": "Interface" }, { "name": "useValidation", "kind": "Function" }, { "name": "validateDate", "kind": "Variable" }, diff --git a/scripts/x-date-pickers.exports.json b/scripts/x-date-pickers.exports.json index df1cacc6fcfe..94e004c0da2d 100644 --- a/scripts/x-date-pickers.exports.json +++ b/scripts/x-date-pickers.exports.json @@ -207,6 +207,7 @@ { "name": "pickersLayoutClasses", "kind": "Variable" }, { "name": "PickersLayoutClassKey", "kind": "TypeAlias" }, { "name": "PickersLayoutContentWrapper", "kind": "Variable" }, + { "name": "PickersLayoutOwnerState", "kind": "Interface" }, { "name": "PickersLayoutProps", "kind": "Interface" }, { "name": "PickersLayoutRoot", "kind": "Variable" }, { "name": "PickersLayoutSlotProps", "kind": "Interface" }, @@ -301,16 +302,13 @@ { "name": "UseClearableFieldResponse", "kind": "TypeAlias" }, { "name": "UseClearableFieldSlotProps", "kind": "Interface" }, { "name": "UseClearableFieldSlots", "kind": "Interface" }, - { "name": "UseDateFieldComponentProps", "kind": "TypeAlias" }, { "name": "UseDateFieldProps", "kind": "Interface" }, - { "name": "UseDateTimeFieldComponentProps", "kind": "TypeAlias" }, { "name": "UseDateTimeFieldProps", "kind": "Interface" }, { "name": "useParsedFormat", "kind": "Variable" }, { "name": "usePickerLayout", "kind": "ExportAssignment" }, { "name": "usePickersContext", "kind": "Variable" }, { "name": "usePickersTranslations", "kind": "Variable" }, { "name": "useSplitFieldProps", "kind": "Variable" }, - { "name": "UseTimeFieldComponentProps", "kind": "TypeAlias" }, { "name": "UseTimeFieldProps", "kind": "Interface" }, { "name": "useValidation", "kind": "Function" }, { "name": "validateDate", "kind": "Variable" },