diff --git a/.eslintrc.js b/.eslintrc.js index 86ca180db6c8..cc528a710906 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -136,6 +136,7 @@ module.exports = { ...(ENABLE_REACT_COMPILER_PLUGIN ? { 'react-compiler/react-compiler': 'error' } : {}), // TODO move to @mui/monorepo, codebase is moving away from default exports https://github.com/mui/material-ui/issues/21862 'import/prefer-default-export': 'off', + 'import/no-relative-packages': 'error', 'import/no-restricted-paths': [ 'error', { diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bd1291080571..942c4e9584b7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 + uses: github/codeql-action/init@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 with: languages: typescript # If you wish to specify custom queries, you can do so here or in a config file. @@ -29,4 +29,4 @@ jobs: # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 + uses: github/codeql-action/analyze@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 diff --git a/.github/workflows/create-cherry-pick-pr.yml b/.github/workflows/create-cherry-pick-pr.yml index 0d5381c25b9d..524b9dc6c78e 100644 --- a/.github/workflows/create-cherry-pick-pr.yml +++ b/.github/workflows/create-cherry-pick-pr.yml @@ -14,5 +14,4 @@ jobs: uses: mui/mui-public/.github/workflows/prs_create-cherry-pick-pr.yml@master permissions: contents: write - issues: write pull-requests: write diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 56ea25676377..900a104519fb 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -44,6 +44,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: Upload to code-scanning - uses: github/codeql-action/upload-sarif@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 + uses: github/codeql-action/upload-sarif@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4 with: sarif_file: results.sarif diff --git a/README.md b/README.md index d22304111b44..b2e30d211bdc 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,7 @@ [![Renovate status](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://github.com/mui/mui-x/issues/2081) [![Average time to resolve an issue](https://isitmaintained.com/badge/resolution/mui/mui-x.svg)](https://isitmaintained.com/project/mui/mui-x 'Average time to resolve an issue') [![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/mui-org)](https://opencollective.com/mui-org) -[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6293/badge)](https://bestpractices.coreinfrastructure.org/projects/6293) - - +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/6293/badge)](https://www.bestpractices.dev/projects/6293) diff --git a/docs/data/charts/areas-demo/AreaChartFillByValue.js b/docs/data/charts/areas-demo/AreaChartFillByValue.js index 7561daa0e7a6..3ff670df251f 100644 --- a/docs/data/charts/areas-demo/AreaChartFillByValue.js +++ b/docs/data/charts/areas-demo/AreaChartFillByValue.js @@ -45,6 +45,7 @@ export default function AreaChartFillByValue() { sx={{ [`& .${areaElementClasses.root}`]: { fill: 'url(#swich-color-id-1)', + filter: 'none', // Remove the default filter. }, }} > @@ -66,6 +67,7 @@ export default function AreaChartFillByValue() { sx={{ [`& .${areaElementClasses.root}`]: { fill: 'url(#swich-color-id-2)', + filter: 'none', // Remove the default filter. }, }} > diff --git a/docs/data/charts/areas-demo/AreaChartFillByValue.tsx b/docs/data/charts/areas-demo/AreaChartFillByValue.tsx index 97fc2f78ff59..dfc484454464 100644 --- a/docs/data/charts/areas-demo/AreaChartFillByValue.tsx +++ b/docs/data/charts/areas-demo/AreaChartFillByValue.tsx @@ -52,6 +52,7 @@ export default function AreaChartFillByValue() { sx={{ [`& .${areaElementClasses.root}`]: { fill: 'url(#swich-color-id-1)', + filter: 'none', // Remove the default filter. }, }} > @@ -73,6 +74,7 @@ export default function AreaChartFillByValue() { sx={{ [`& .${areaElementClasses.root}`]: { fill: 'url(#swich-color-id-2)', + filter: 'none', // Remove the default filter. }, }} > diff --git a/docs/data/charts/getting-started/getting-started.md b/docs/data/charts/getting-started/getting-started.md index 83dbff7001c7..bfac2ba84414 100644 --- a/docs/data/charts/getting-started/getting-started.md +++ b/docs/data/charts/getting-started/getting-started.md @@ -49,7 +49,7 @@ yarn add @mui/material @emotion/react @emotion/styled ### Usage with D3 To help folks using CommonJS, the `@mui/x-charts` package uses a vendored package named `@mui/x-charts-vendor` to access D3 libraries. -You can import D3 functions from `@mui/x-charts-vendor/d3-color`. +You can import D3 functions from `@mui/x-charts-vendor/d3-scale`. ## Rendering Charts diff --git a/docs/data/charts/line-demo/LineWithPrediction.js b/docs/data/charts/line-demo/LineWithPrediction.js index 277ce031aeb0..1e5858c5ddf6 100644 --- a/docs/data/charts/line-demo/LineWithPrediction.js +++ b/docs/data/charts/line-demo/LineWithPrediction.js @@ -40,11 +40,11 @@ function CustomAnimatedLine(props) { height={top + height + bottom} /> - - + + - - + + ); @@ -64,7 +64,8 @@ export default function LineWithPrediction() { height={200} width={400} slots={{ line: CustomAnimatedLine }} - slotProps={{ line: { limit: 5, sxAfter: { strokeDasharray: '10 5' } } }} + slotProps={{ line: { limit: 5 } }} + sx={{ '& .line-after path': { strokeDasharray: '10 5' } }} /> ); } diff --git a/docs/data/charts/line-demo/LineWithPrediction.tsx b/docs/data/charts/line-demo/LineWithPrediction.tsx index 55fd42deb244..981c7805dd3e 100644 --- a/docs/data/charts/line-demo/LineWithPrediction.tsx +++ b/docs/data/charts/line-demo/LineWithPrediction.tsx @@ -47,11 +47,11 @@ function CustomAnimatedLine(props: CustomAnimatedLineProps) { height={top + height + bottom} /> - - + + - - + + ); @@ -71,7 +71,8 @@ export default function LineWithPrediction() { height={200} width={400} slots={{ line: CustomAnimatedLine }} - slotProps={{ line: { limit: 5, sxAfter: { strokeDasharray: '10 5' } } as any }} + slotProps={{ line: { limit: 5 } as any }} + sx={{ '& .line-after path': { strokeDasharray: '10 5' } }} /> ); } diff --git a/docs/data/charts/line-demo/LineWithPrediction.tsx.preview b/docs/data/charts/line-demo/LineWithPrediction.tsx.preview index 1dc0a8f94a59..f15f69543aae 100644 --- a/docs/data/charts/line-demo/LineWithPrediction.tsx.preview +++ b/docs/data/charts/line-demo/LineWithPrediction.tsx.preview @@ -10,5 +10,6 @@ height={200} width={400} slots={{ line: CustomAnimatedLine }} - slotProps={{ line: { limit: 5, sxAfter: { strokeDasharray: '10 5' } } as any }} + slotProps={{ line: { limit: 5 } as any }} + sx={{ '& .line-after path': { strokeDasharray: '10 5' } }} /> \ No newline at end of file diff --git a/docs/data/charts/lines/CSSCustomization.js b/docs/data/charts/lines/CSSCustomization.js index 5e5fe6260821..b647eaa27f14 100644 --- a/docs/data/charts/lines/CSSCustomization.js +++ b/docs/data/charts/lines/CSSCustomization.js @@ -13,6 +13,7 @@ export default function CSSCustomization() { }, '& .MuiAreaElement-series-Germany': { fill: "url('#myGradient')", + filter: 'none', // Remove the default filtering }, }} xAxis={[ diff --git a/docs/data/charts/lines/CSSCustomization.tsx b/docs/data/charts/lines/CSSCustomization.tsx index 5e5fe6260821..b647eaa27f14 100644 --- a/docs/data/charts/lines/CSSCustomization.tsx +++ b/docs/data/charts/lines/CSSCustomization.tsx @@ -13,6 +13,7 @@ export default function CSSCustomization() { }, '& .MuiAreaElement-series-Germany': { fill: "url('#myGradient')", + filter: 'none', // Remove the default filtering }, }} xAxis={[ diff --git a/docs/data/data-grid/localization/data.json b/docs/data/data-grid/localization/data.json index d34a23693faa..e6d0c4013ace 100644 --- a/docs/data/data-grid/localization/data.json +++ b/docs/data/data-grid/localization/data.json @@ -3,280 +3,280 @@ "languageTag": "ar-SD", "importName": "arSD", "localeName": "Arabic (Sudan)", - "missingKeysCount": 8, - "totalKeysCount": 122, + "missingKeysCount": 9, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/arSD.ts" }, { "languageTag": "be-BY", "importName": "beBY", "localeName": "Belarusian", - "missingKeysCount": 34, - "totalKeysCount": 122, + "missingKeysCount": 35, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/beBY.ts" }, { "languageTag": "bg-BG", "importName": "bgBG", "localeName": "Bulgarian", - "missingKeysCount": 0, - "totalKeysCount": 122, + "missingKeysCount": 1, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/bgBG.ts" }, { "languageTag": "zh-HK", "importName": "zhHK", "localeName": "Chinese (Hong Kong)", - "missingKeysCount": 8, - "totalKeysCount": 122, + "missingKeysCount": 9, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/zhHK.ts" }, { "languageTag": "zh-CN", "importName": "zhCN", "localeName": "Chinese (Simplified)", - "missingKeysCount": 4, - "totalKeysCount": 122, + "missingKeysCount": 1, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/zhCN.ts" }, { "languageTag": "zh-TW", "importName": "zhTW", "localeName": "Chinese (Taiwan)", - "missingKeysCount": 8, - "totalKeysCount": 122, + "missingKeysCount": 9, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/zhTW.ts" }, { "languageTag": "hr-HR", "importName": "hrHR", "localeName": "Croatian", - "missingKeysCount": 0, - "totalKeysCount": 122, + "missingKeysCount": 1, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/hrHR.ts" }, { "languageTag": "cs-CZ", "importName": "csCZ", "localeName": "Czech", - "missingKeysCount": 4, - "totalKeysCount": 122, + "missingKeysCount": 5, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/csCZ.ts" }, { "languageTag": "da-DK", "importName": "daDK", "localeName": "Danish", - "missingKeysCount": 0, - "totalKeysCount": 122, + "missingKeysCount": 1, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/daDK.ts" }, { "languageTag": "nl-NL", "importName": "nlNL", "localeName": "Dutch", - "missingKeysCount": 4, - "totalKeysCount": 122, + "missingKeysCount": 5, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/nlNL.ts" }, { "languageTag": "fi-FI", "importName": "fiFI", "localeName": "Finnish", - "missingKeysCount": 4, - "totalKeysCount": 122, + "missingKeysCount": 5, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/fiFI.ts" }, { "languageTag": "fr-FR", "importName": "frFR", "localeName": "French", - "missingKeysCount": 0, - "totalKeysCount": 122, + "missingKeysCount": 1, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/frFR.ts" }, { "languageTag": "de-DE", "importName": "deDE", "localeName": "German", - "missingKeysCount": 0, - "totalKeysCount": 122, + "missingKeysCount": 1, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/deDE.ts" }, { "languageTag": "el-GR", "importName": "elGR", "localeName": "Greek", - "missingKeysCount": 8, - "totalKeysCount": 122, + "missingKeysCount": 9, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/elGR.ts" }, { "languageTag": "he-IL", "importName": "heIL", "localeName": "Hebrew", - "missingKeysCount": 4, - "totalKeysCount": 122, + "missingKeysCount": 5, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/heIL.ts" }, { "languageTag": "hu-HU", "importName": "huHU", "localeName": "Hungarian", - "missingKeysCount": 6, - "totalKeysCount": 122, + "missingKeysCount": 7, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/huHU.ts" }, { "languageTag": "is-IS", "importName": "isIS", "localeName": "Icelandic", - "missingKeysCount": 8, - "totalKeysCount": 122, + "missingKeysCount": 9, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/isIS.ts" }, { "languageTag": "it-IT", "importName": "itIT", "localeName": "Italian", - "missingKeysCount": 0, - "totalKeysCount": 122, + "missingKeysCount": 1, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/itIT.ts" }, { "languageTag": "ja-JP", "importName": "jaJP", "localeName": "Japanese", - "missingKeysCount": 0, - "totalKeysCount": 122, + "missingKeysCount": 1, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/jaJP.ts" }, { "languageTag": "ko-KR", "importName": "koKR", "localeName": "Korean", - "missingKeysCount": 35, - "totalKeysCount": 122, + "missingKeysCount": 36, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/koKR.ts" }, { "languageTag": "nb-NO", "importName": "nbNO", "localeName": "Norwegian (Bokmål)", - "missingKeysCount": 4, - "totalKeysCount": 122, + "missingKeysCount": 5, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/nbNO.ts" }, { "languageTag": "nn-NO", "importName": "nnNO", "localeName": "Norwegian (Nynorsk)", - "missingKeysCount": 4, - "totalKeysCount": 122, + "missingKeysCount": 5, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/nnNO.ts" }, { "languageTag": "fa-IR", "importName": "faIR", "localeName": "Persian", - "missingKeysCount": 4, - "totalKeysCount": 122, + "missingKeysCount": 5, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/faIR.ts" }, { "languageTag": "pl-PL", "importName": "plPL", "localeName": "Polish", - "missingKeysCount": 11, - "totalKeysCount": 122, + "missingKeysCount": 12, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/plPL.ts" }, { "languageTag": "pt-PT", "importName": "ptPT", "localeName": "Portuguese", - "missingKeysCount": 0, - "totalKeysCount": 122, + "missingKeysCount": 1, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/ptPT.ts" }, { "languageTag": "pt-BR", "importName": "ptBR", "localeName": "Portuguese (Brazil)", - "missingKeysCount": 0, - "totalKeysCount": 122, + "missingKeysCount": 1, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/ptBR.ts" }, { "languageTag": "ro-RO", "importName": "roRO", "localeName": "Romanian", - "missingKeysCount": 8, - "totalKeysCount": 122, + "missingKeysCount": 9, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/roRO.ts" }, { "languageTag": "ru-RU", "importName": "ruRU", "localeName": "Russian", - "missingKeysCount": 4, - "totalKeysCount": 122, + "missingKeysCount": 5, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/ruRU.ts" }, { "languageTag": "sk-SK", "importName": "skSK", "localeName": "Slovak", - "missingKeysCount": 5, - "totalKeysCount": 122, + "missingKeysCount": 6, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/skSK.ts" }, { "languageTag": "es-ES", "importName": "esES", "localeName": "Spanish", - "missingKeysCount": 4, - "totalKeysCount": 122, + "missingKeysCount": 1, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/esES.ts" }, { "languageTag": "sv-SE", "importName": "svSE", "localeName": "Swedish", - "missingKeysCount": 5, - "totalKeysCount": 122, + "missingKeysCount": 1, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/svSE.ts" }, { "languageTag": "tr-TR", "importName": "trTR", "localeName": "Turkish", - "missingKeysCount": 2, - "totalKeysCount": 122, + "missingKeysCount": 3, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/trTR.ts" }, { "languageTag": "uk-UA", "importName": "ukUA", "localeName": "Ukrainian", - "missingKeysCount": 8, - "totalKeysCount": 122, + "missingKeysCount": 9, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/ukUA.ts" }, { "languageTag": "ur-PK", "importName": "urPK", "localeName": "Urdu (Pakistan)", - "missingKeysCount": 8, - "totalKeysCount": 122, + "missingKeysCount": 9, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/urPK.ts" }, { "languageTag": "vi-VN", "importName": "viVN", "localeName": "Vietnamese", - "missingKeysCount": 0, - "totalKeysCount": 122, + "missingKeysCount": 1, + "totalKeysCount": 123, "githubLink": "https://github.com/mui/mui-x/blob/master/packages/x-data-grid/src/locales/viVN.ts" } ] 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 ed0546fe4fa8..4420a0b036ae 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 @@ -226,6 +226,46 @@ All the new Tree Item-related components and utils (introduced in the previous m + } from '@mui/x-tree-view/TreeItemLabelInput'; ``` +## Stop using `publicAPI` methods in the render + +The Tree Items are now memoized to improve the performances of the Tree View components. +If you call a `publicAPI` method in the render of an item, it might not re-render and you might not have the new value. + +```ts +function CustomTreeItem(props) { + const { publicAPI } = useTreeItemUtils(); + + // Invalid + console.log(publicAPI.getItem(props.itemId)); + + // Valid + React.useEffect(() => { + console.log(publicAPI.getItem(props.itemId)); + }); + + // Valid + function handleItemClick() { + console.log(publicAPI.getItem(props.itemId)); + } +} +``` + +If you need to access the tree item model inside the render, you can use the new `useTreeItemModel` hook: + +```diff ++import { useTreeItemModel } from '@mui/x-tree-view/hooks'; + + function CustomTreeItem(props) { +- const { publicAPI } = useTreeItemUtils(); +- const item = publicAPI.getItem(props.itemId); ++ const item = useTreeItemModel(props.itemId); + } +``` + +:::success +If you were using `publicAPI` methods to access other information than the tree item model inside the render, please open an issue so that we can provide a way to do it. +::: + ## Apply the indentation on the item content instead of it's parent's group The indentation of nested Tree Items is now applied on the content of the element. diff --git a/docs/data/tree-view/rich-tree-view/customization/FileExplorer.js b/docs/data/tree-view/rich-tree-view/customization/FileExplorer.js index 304f82faaf74..cfc8d7fe5ccd 100644 --- a/docs/data/tree-view/rich-tree-view/customization/FileExplorer.js +++ b/docs/data/tree-view/rich-tree-view/customization/FileExplorer.js @@ -26,6 +26,7 @@ import { import { TreeItemIcon } from '@mui/x-tree-view/TreeItemIcon'; import { TreeItemProvider } from '@mui/x-tree-view/TreeItemProvider'; import { TreeItemDragAndDropOverlay } from '@mui/x-tree-view/TreeItemDragAndDropOverlay'; +import { useTreeItemModel } from '@mui/x-tree-view/hooks'; const ITEMS = [ { @@ -178,13 +179,6 @@ function CustomLabel({ icon: Icon, expandable, children, ...other }) { ); } -const isExpandable = (reactChildren) => { - if (Array.isArray(reactChildren)) { - return reactChildren.length > 0 && reactChildren.some(isExpandable); - } - return Boolean(reactChildren); -}; - const getIconFromFileType = (fileType) => { switch (fileType) { case 'image': @@ -210,6 +204,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { const { id, itemId, label, disabled, children, ...other } = props; const { + getContextProviderProps, getRootProps, getContentProps, getIconContainerProps, @@ -218,20 +213,19 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { getGroupTransitionProps, getDragAndDropOverlayProps, status, - publicAPI, } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref }); - const item = publicAPI.getItem(itemId); - const expandable = isExpandable(children); + const item = useTreeItemModel(itemId); + let icon; - if (expandable) { + if (status.expandable) { icon = FolderRounded; } else if (item.fileType) { icon = getIconFromFileType(item.fileType); } return ( - + diff --git a/docs/data/tree-view/rich-tree-view/customization/FileExplorer.tsx b/docs/data/tree-view/rich-tree-view/customization/FileExplorer.tsx index 09291542de68..dd76a0417488 100644 --- a/docs/data/tree-view/rich-tree-view/customization/FileExplorer.tsx +++ b/docs/data/tree-view/rich-tree-view/customization/FileExplorer.tsx @@ -26,6 +26,7 @@ import { import { TreeItemIcon } from '@mui/x-tree-view/TreeItemIcon'; import { TreeItemProvider } from '@mui/x-tree-view/TreeItemProvider'; import { TreeItemDragAndDropOverlay } from '@mui/x-tree-view/TreeItemDragAndDropOverlay'; +import { useTreeItemModel } from '@mui/x-tree-view/hooks'; import { TreeViewBaseItem } from '@mui/x-tree-view/models'; type FileType = 'image' | 'pdf' | 'doc' | 'video' | 'folder' | 'pinned' | 'trash'; @@ -204,13 +205,6 @@ function CustomLabel({ ); } -const isExpandable = (reactChildren: React.ReactNode) => { - if (Array.isArray(reactChildren)) { - return reactChildren.length > 0 && reactChildren.some(isExpandable); - } - return Boolean(reactChildren); -}; - const getIconFromFileType = (fileType: FileType) => { switch (fileType) { case 'image': @@ -243,6 +237,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( const { id, itemId, label, disabled, children, ...other } = props; const { + getContextProviderProps, getRootProps, getContentProps, getIconContainerProps, @@ -251,20 +246,19 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( getGroupTransitionProps, getDragAndDropOverlayProps, status, - publicAPI, } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref }); - const item = publicAPI.getItem(itemId); - const expandable = isExpandable(children); + const item = useTreeItemModel(itemId)!; + let icon; - if (expandable) { + if (status.expandable) { icon = FolderRounded; } else if (item.fileType) { icon = getIconFromFileType(item.fileType); } return ( - + diff --git a/docs/data/tree-view/rich-tree-view/customization/HeadlessAPI.js b/docs/data/tree-view/rich-tree-view/customization/HeadlessAPI.js index 172d7a1deaaf..a946acc455c8 100644 --- a/docs/data/tree-view/rich-tree-view/customization/HeadlessAPI.js +++ b/docs/data/tree-view/rich-tree-view/customization/HeadlessAPI.js @@ -40,6 +40,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { const { id, itemId, label, disabled, children, ...other } = props; const { + getContextProviderProps, getRootProps, getContentProps, getIconContainerProps, @@ -51,7 +52,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref }); return ( - + diff --git a/docs/data/tree-view/rich-tree-view/customization/HeadlessAPI.tsx b/docs/data/tree-view/rich-tree-view/customization/HeadlessAPI.tsx index bccd2390df60..3554fd4cc93e 100644 --- a/docs/data/tree-view/rich-tree-view/customization/HeadlessAPI.tsx +++ b/docs/data/tree-view/rich-tree-view/customization/HeadlessAPI.tsx @@ -47,6 +47,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( const { id, itemId, label, disabled, children, ...other } = props; const { + getContextProviderProps, getRootProps, getContentProps, getIconContainerProps, @@ -58,7 +59,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref }); return ( - + diff --git a/docs/data/tree-view/rich-tree-view/editing/CustomLabelInput.js b/docs/data/tree-view/rich-tree-view/editing/CustomLabelInput.js index 552f4de2ce1f..dfbfeec308cb 100644 --- a/docs/data/tree-view/rich-tree-view/editing/CustomLabelInput.js +++ b/docs/data/tree-view/rich-tree-view/editing/CustomLabelInput.js @@ -6,8 +6,8 @@ import IconButton from '@mui/material/IconButton'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; import { TreeItem, TreeItemLabel } from '@mui/x-tree-view/TreeItem'; -import { useTreeItem } from '@mui/x-tree-view/useTreeItem'; -import { useTreeItemUtils } from '@mui/x-tree-view/hooks'; + +import { useTreeItemUtils, useTreeItemModel } from '@mui/x-tree-view/hooks'; const StyledLabelInput = styled('input')(({ theme }) => ({ ...theme.typography.body1, @@ -69,9 +69,11 @@ function Label({ children, ...other }) { } const LabelInput = React.forwardRef(function LabelInput( - { item, handleCancelItemLabelEditing, handleSaveItemLabel, ...props }, + { itemId, handleCancelItemLabelEditing, handleSaveItemLabel, ...props }, ref, ) { + const item = useTreeItemModel(itemId); + const [initialNameValue, setInitialNameValue] = React.useState({ firstName: item.firstName, lastName: item.lastName, @@ -141,7 +143,6 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { itemId: props.itemId, children: props.children, }); - const { publicAPI } = useTreeItem(props); const handleInputBlur = (event) => { event.defaultMuiPrevented = true; @@ -158,7 +159,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { slots={{ label: Label, labelInput: LabelInput }} slotProps={{ labelInput: { - item: publicAPI.getItem(props.itemId), + itemId: props.itemId, onBlur: handleInputBlur, onKeyDown: handleInputKeyDown, handleCancelItemLabelEditing: interactions.handleCancelItemLabelEditing, diff --git a/docs/data/tree-view/rich-tree-view/editing/CustomLabelInput.tsx b/docs/data/tree-view/rich-tree-view/editing/CustomLabelInput.tsx index 8b4d1ad7c143..248f80009c8c 100644 --- a/docs/data/tree-view/rich-tree-view/editing/CustomLabelInput.tsx +++ b/docs/data/tree-view/rich-tree-view/editing/CustomLabelInput.tsx @@ -9,10 +9,9 @@ import { TreeItem, TreeItemLabel, TreeItemProps } from '@mui/x-tree-view/TreeIte import { UseTreeItemLabelInputSlotOwnProps, UseTreeItemLabelSlotOwnProps, - useTreeItem, } from '@mui/x-tree-view/useTreeItem'; -import { useTreeItemUtils } from '@mui/x-tree-view/hooks'; -import { TreeViewBaseItem } from '@mui/x-tree-view/models'; +import { useTreeItemUtils, useTreeItemModel } from '@mui/x-tree-view/hooks'; +import { TreeViewBaseItem, TreeViewItemId } from '@mui/x-tree-view/models'; const StyledLabelInput = styled('input')(({ theme }) => ({ ...theme.typography.body1, @@ -83,18 +82,20 @@ function Label({ children, ...other }: UseTreeItemLabelSlotOwnProps) { interface CustomLabelInputProps extends UseTreeItemLabelInputSlotOwnProps { handleCancelItemLabelEditing: (event: React.SyntheticEvent) => void; handleSaveItemLabel: (event: React.SyntheticEvent, label: string) => void; - item: TreeViewBaseItem; + itemId: TreeViewItemId; } const LabelInput = React.forwardRef(function LabelInput( { - item, + itemId, handleCancelItemLabelEditing, handleSaveItemLabel, ...props }: Omit, ref: React.Ref, ) { + const item = useTreeItemModel(itemId)!; + const [initialNameValue, setInitialNameValue] = React.useState({ firstName: item.firstName, lastName: item.lastName, @@ -167,7 +168,6 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( itemId: props.itemId, children: props.children, }); - const { publicAPI } = useTreeItem(props); const handleInputBlur: UseTreeItemLabelInputSlotOwnProps['onBlur'] = (event) => { event.defaultMuiPrevented = true; @@ -186,7 +186,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( slots={{ label: Label, labelInput: LabelInput }} slotProps={{ labelInput: { - item: publicAPI.getItem(props.itemId), + itemId: props.itemId, onBlur: handleInputBlur, onKeyDown: handleInputKeyDown, handleCancelItemLabelEditing: interactions.handleCancelItemLabelEditing, diff --git a/docs/data/tree-view/rich-tree-view/headless/LogExpandedItems.js b/docs/data/tree-view/rich-tree-view/headless/LogExpandedItems.js index 1b76b3354084..3d6077c4fdac 100644 --- a/docs/data/tree-view/rich-tree-view/headless/LogExpandedItems.js +++ b/docs/data/tree-view/rich-tree-view/headless/LogExpandedItems.js @@ -7,8 +7,11 @@ import { RichTreeViewRoot, RICH_TREE_VIEW_PLUGINS, } from '@mui/x-tree-view/RichTreeView'; -import { TreeItem } from '@mui/x-tree-view/TreeItem'; -import { useTreeView, TreeViewProvider } from '@mui/x-tree-view/internals'; +import { + useTreeView, + TreeViewProvider, + RichTreeViewItems, +} from '@mui/x-tree-view/internals'; const useTreeViewLogExpanded = ({ params, models }) => { const expandedStr = JSON.stringify(models.expandedItems.value); @@ -36,25 +39,15 @@ useTreeViewLogExpanded.params = { const TREE_VIEW_PLUGINS = [...RICH_TREE_VIEW_PLUGINS, useTreeViewLogExpanded]; function TreeView(props) { - const { getRootProps, contextValue, instance } = useTreeView({ + const { getRootProps, contextValue } = useTreeView({ plugins: TREE_VIEW_PLUGINS, props, }); - const itemsToRender = instance.getItemsToRender(); - - const renderItem = ({ children: itemChildren, ...itemProps }) => { - return ( - - {itemChildren?.map(renderItem)} - - ); - }; - return ( - {itemsToRender.map(renderItem)} + ); diff --git a/docs/data/tree-view/rich-tree-view/headless/LogExpandedItems.tsx b/docs/data/tree-view/rich-tree-view/headless/LogExpandedItems.tsx index 5c6942d61458..bab71f713478 100644 --- a/docs/data/tree-view/rich-tree-view/headless/LogExpandedItems.tsx +++ b/docs/data/tree-view/rich-tree-view/headless/LogExpandedItems.tsx @@ -11,7 +11,6 @@ import { RichTreeViewPluginSlots, RichTreeViewPluginSlotProps, } from '@mui/x-tree-view/RichTreeView'; -import { TreeItem } from '@mui/x-tree-view/TreeItem'; import { UseTreeViewExpansionSignature, TreeViewPlugin, @@ -19,6 +18,7 @@ import { useTreeView, TreeViewProvider, ConvertPluginsIntoSignatures, + RichTreeViewItems, } from '@mui/x-tree-view/internals'; interface TreeViewLogExpandedParameters { @@ -86,7 +86,7 @@ type TreeViewPluginSignatures = ConvertPluginsIntoSignatures< function TreeView( props: TreeViewProps, ) { - const { getRootProps, contextValue, instance } = useTreeView< + const { getRootProps, contextValue } = useTreeView< TreeViewPluginSignatures, typeof props >({ @@ -94,23 +94,10 @@ function TreeView( props, }); - const itemsToRender = instance.getItemsToRender(); - - const renderItem = ({ - children: itemChildren, - ...itemProps - }: ReturnType[number]) => { - return ( - - {itemChildren?.map(renderItem)} - - ); - }; - return ( - {itemsToRender.map(renderItem)} + ); diff --git a/docs/data/tree-view/rich-tree-view/headless/headless.md b/docs/data/tree-view/rich-tree-view/headless/headless.md index 21ccf76210a5..0cc8c75d50b6 100644 --- a/docs/data/tree-view/rich-tree-view/headless/headless.md +++ b/docs/data/tree-view/rich-tree-view/headless/headless.md @@ -122,7 +122,7 @@ const useCustomPlugin = ({ models }) => { models.expandedItems.setValue([]); // Check if an item is expanded - const isExpanded = instance.isNodeExpanded('some-item-id'); + const isExpanded = useSelector(selectorIsItemExpanded, 'some-item-id'); }; }; ``` diff --git a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js index 0e7bf9686ca8..9951fed165c1 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js +++ b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.js @@ -23,8 +23,7 @@ import { import { TreeItemIcon } from '@mui/x-tree-view/TreeItemIcon'; import { TreeItemProvider } from '@mui/x-tree-view/TreeItemProvider'; import { TreeItemDragAndDropOverlay } from '@mui/x-tree-view/TreeItemDragAndDropOverlay'; - -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { useTreeItemModel, useTreeViewApiRef } from '@mui/x-tree-view/hooks'; const ITEMS = [ { @@ -164,13 +163,6 @@ function CustomLabel({ icon: Icon, expandable, children, ...other }) { ); } -const isExpandable = (reactChildren) => { - if (Array.isArray(reactChildren)) { - return reactChildren.length > 0 && reactChildren.some(isExpandable); - } - return Boolean(reactChildren); -}; - const getIconFromFileType = (fileType) => { switch (fileType) { case 'image': @@ -194,6 +186,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { const { id, itemId, label, disabled, children, ...other } = props; const { + getContextProviderProps, getRootProps, getContentProps, getIconContainerProps, @@ -202,15 +195,19 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { getGroupTransitionProps, getDragAndDropOverlayProps, status, - publicAPI, } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref }); - const item = publicAPI.getItem(itemId); - const expandable = isExpandable(children); - const icon = getIconFromFileType(item.fileType); + const item = useTreeItemModel(itemId); + + let icon; + if (status.expandable) { + icon = FolderRounded; + } else if (item.fileType) { + icon = getIconFromFileType(item.fileType); + } return ( - + diff --git a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx index ff08427f4691..49d4013853f5 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx +++ b/docs/data/tree-view/rich-tree-view/ordering/FileExplorer.tsx @@ -23,8 +23,8 @@ import { import { TreeItemIcon } from '@mui/x-tree-view/TreeItemIcon'; import { TreeItemProvider } from '@mui/x-tree-view/TreeItemProvider'; import { TreeItemDragAndDropOverlay } from '@mui/x-tree-view/TreeItemDragAndDropOverlay'; +import { useTreeItemModel, useTreeViewApiRef } from '@mui/x-tree-view/hooks'; import { TreeViewBaseItem } from '@mui/x-tree-view/models'; -import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; type FileType = 'image' | 'pdf' | 'doc' | 'video' | 'folder' | 'pinned' | 'trash'; @@ -189,13 +189,6 @@ function CustomLabel({ ); } -const isExpandable = (reactChildren: React.ReactNode) => { - if (Array.isArray(reactChildren)) { - return reactChildren.length > 0 && reactChildren.some(isExpandable); - } - return Boolean(reactChildren); -}; - const getIconFromFileType = (fileType: FileType) => { switch (fileType) { case 'image': @@ -226,6 +219,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( const { id, itemId, label, disabled, children, ...other } = props; const { + getContextProviderProps, getRootProps, getContentProps, getIconContainerProps, @@ -234,15 +228,19 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( getGroupTransitionProps, getDragAndDropOverlayProps, status, - publicAPI, } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref }); - const item = publicAPI.getItem(itemId); - const expandable = isExpandable(children); - const icon = getIconFromFileType(item.fileType); + const item = useTreeItemModel(itemId)!; + + let icon; + if (status.expandable) { + icon = FolderRounded; + } else if (item.fileType) { + icon = getIconFromFileType(item.fileType); + } return ( - + diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.js b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.js index 6abae8742ed1..6d8122853552 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.js +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.js @@ -50,6 +50,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { const { id, itemId, label, disabled, children, ...other } = props; const { + getContextProviderProps, getRootProps, getContentProps, getIconContainerProps, @@ -73,7 +74,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { }; return ( - + diff --git a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx index bdf2378a6dbb..f2d38f4972cc 100644 --- a/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx +++ b/docs/data/tree-view/rich-tree-view/ordering/OnlyReorderFromDragHandle.tsx @@ -57,6 +57,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( const { id, itemId, label, disabled, children, ...other } = props; const { + getContextProviderProps, getRootProps, getContentProps, getIconContainerProps, @@ -84,7 +85,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( }; return ( - + diff --git a/docs/data/tree-view/simple-tree-view/customization/GmailTreeView.js b/docs/data/tree-view/simple-tree-view/customization/GmailTreeView.js index da5c5eebb7f4..c07504f8a92f 100644 --- a/docs/data/tree-view/simple-tree-view/customization/GmailTreeView.js +++ b/docs/data/tree-view/simple-tree-view/customization/GmailTreeView.js @@ -69,6 +69,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { } = props; const { + getContextProviderProps, getRootProps, getContentProps, getIconContainerProps, @@ -84,7 +85,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { }; return ( - + + + diff --git a/docs/data/tree-view/simple-tree-view/customization/HeadlessAPI.tsx b/docs/data/tree-view/simple-tree-view/customization/HeadlessAPI.tsx index 10f47fe2aa3d..67c04b2c4b92 100644 --- a/docs/data/tree-view/simple-tree-view/customization/HeadlessAPI.tsx +++ b/docs/data/tree-view/simple-tree-view/customization/HeadlessAPI.tsx @@ -25,6 +25,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( const { id, itemId, label, disabled, children, ...other } = props; const { + getContextProviderProps, getRootProps, getContentProps, getIconContainerProps, @@ -35,7 +36,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref }); return ( - + diff --git a/docs/data/tree-view/tree-item-customization/CustomTreeItemDemo.js b/docs/data/tree-view/tree-item-customization/CustomTreeItemDemo.js index 9a39125199f4..436d40ea821f 100644 --- a/docs/data/tree-view/tree-item-customization/CustomTreeItemDemo.js +++ b/docs/data/tree-view/tree-item-customization/CustomTreeItemDemo.js @@ -103,12 +103,13 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { getGroupTransitionProps, getDragAndDropOverlayProps, getLabelInputProps, + getContextProviderProps, status, } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref }); return ( - + - + + {status.expandable && ( + {status.expandable && ( , ) { - const { publicAPI } = useTreeItemUtils({ - itemId: props.itemId, - children: props.children, - }); - - const item = publicAPI.getItem(props.itemId); + const item = useTreeItemModel(props.itemId)!; return ( + diff --git a/docs/data/tree-view/tree-item-customization/useTreeItemHookProperties.tsx b/docs/data/tree-view/tree-item-customization/useTreeItemHookProperties.tsx index 8e49e71df547..0fc4072427ac 100644 --- a/docs/data/tree-view/tree-item-customization/useTreeItemHookProperties.tsx +++ b/docs/data/tree-view/tree-item-customization/useTreeItemHookProperties.tsx @@ -22,6 +22,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( ref: React.Ref, ) { const { + getContextProviderProps, getRootProps, getContentProps, getIconContainerProps, @@ -34,7 +35,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref }); return ( - + diff --git a/docs/data/tree-view/tree-item-customization/useTreeItemHookPublicAPI.js b/docs/data/tree-view/tree-item-customization/useTreeItemHookPublicAPI.js index 72886928944d..0c71983ff031 100644 --- a/docs/data/tree-view/tree-item-customization/useTreeItemHookPublicAPI.js +++ b/docs/data/tree-view/tree-item-customization/useTreeItemHookPublicAPI.js @@ -1,14 +1,14 @@ import * as React from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; -import Chip from '@mui/material/Chip'; +import Button from '@mui/material/Button'; import Stack from '@mui/material/Stack'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; import { TreeItem } from '@mui/x-tree-view/TreeItem'; import { useTreeItem } from '@mui/x-tree-view/useTreeItem'; import { MUI_X_PRODUCTS } from './products'; -function CustomLabel({ children, className, numberOfChildren }) { +function CustomLabel({ children, className, selectFirstChildren }) { return ( {children} - - + {!!selectFirstChildren && ( + + )} ); } const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { - const { publicAPI } = useTreeItem(props); + const { publicAPI, status } = useTreeItem(props); - const childrenNumber = publicAPI.getItemOrderedChildrenIds(props.itemId).length; + const selectFirstChildren = status.expanded + ? (event) => { + event.stopPropagation(); + const children = publicAPI.getItemOrderedChildrenIds(props.itemId); + if (children.length > 0) { + publicAPI.selectItem({ + event, + itemId: children[0], + shouldBeSelected: true, + }); + } + } + : undefined; return ( ); diff --git a/docs/data/tree-view/tree-item-customization/useTreeItemHookPublicAPI.tsx b/docs/data/tree-view/tree-item-customization/useTreeItemHookPublicAPI.tsx index 48bf738b2fc8..3698ce968f02 100644 --- a/docs/data/tree-view/tree-item-customization/useTreeItemHookPublicAPI.tsx +++ b/docs/data/tree-view/tree-item-customization/useTreeItemHookPublicAPI.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; -import Chip from '@mui/material/Chip'; +import Button from '@mui/material/Button'; import Stack from '@mui/material/Stack'; import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; import { TreeItem, TreeItemProps } from '@mui/x-tree-view/TreeItem'; @@ -11,10 +11,14 @@ import { MUI_X_PRODUCTS } from './products'; interface CustomLabelProps { children: string; className: string; - numberOfChildren: number; + selectFirstChildren?: (event: React.MouseEvent) => void; } -function CustomLabel({ children, className, numberOfChildren }: CustomLabelProps) { +function CustomLabel({ + children, + className, + selectFirstChildren, +}: CustomLabelProps) { return ( {children} - - + {!!selectFirstChildren && ( + + )} ); } @@ -34,9 +46,21 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( props: TreeItemProps, ref: React.Ref, ) { - const { publicAPI } = useTreeItem(props); + const { publicAPI, status } = useTreeItem(props); - const childrenNumber = publicAPI.getItemOrderedChildrenIds(props.itemId).length; + const selectFirstChildren = status.expanded + ? (event: React.MouseEvent) => { + event.stopPropagation(); + const children = publicAPI.getItemOrderedChildrenIds(props.itemId); + if (children.length > 0) { + publicAPI.selectItem({ + event, + itemId: children[0], + shouldBeSelected: true, + }); + } + } + : undefined; return ( ); diff --git a/docs/data/tree-view/tree-item-customization/useTreeItemHookStatus.js b/docs/data/tree-view/tree-item-customization/useTreeItemHookStatus.js index 5c6e53c47b14..13395175085d 100644 --- a/docs/data/tree-view/tree-item-customization/useTreeItemHookStatus.js +++ b/docs/data/tree-view/tree-item-customization/useTreeItemHookStatus.js @@ -88,6 +88,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( ref, ) { const { + getContextProviderProps, getRootProps, getContentProps, getLabelProps, @@ -98,7 +99,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( } = useTreeItem({ id, itemId, label, disabled, children, rootRef: ref }); return ( - + diff --git a/docs/data/tree-view/tree-item-customization/useTreeItemHookStatus.tsx b/docs/data/tree-view/tree-item-customization/useTreeItemHookStatus.tsx index d03d53ca63d0..783bf24064ca 100644 --- a/docs/data/tree-view/tree-item-customization/useTreeItemHookStatus.tsx +++ b/docs/data/tree-view/tree-item-customization/useTreeItemHookStatus.tsx @@ -91,6 +91,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( ref: React.Ref, ) { const { + getContextProviderProps, getRootProps, getContentProps, getLabelProps, @@ -101,7 +102,7 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem( } = useTreeItem({ id, itemId, label, disabled, children, rootRef: ref }); return ( - + diff --git a/docs/data/tree-view/tree-item-customization/useTreeItemModelHook.js b/docs/data/tree-view/tree-item-customization/useTreeItemModelHook.js new file mode 100644 index 000000000000..eba242636def --- /dev/null +++ b/docs/data/tree-view/tree-item-customization/useTreeItemModelHook.js @@ -0,0 +1,32 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { TreeItem } from '@mui/x-tree-view/TreeItem'; +import { useTreeItemModel } from '@mui/x-tree-view/hooks'; +import { MUI_X_PRODUCTS } from './products'; + +const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { + const item = useTreeItemModel(props.itemId); + + return ( + + ); +}); + +export default function useTreeItemModelHook() { + return ( + + + + ); +} diff --git a/docs/data/tree-view/tree-item-customization/useTreeItemModelHook.tsx b/docs/data/tree-view/tree-item-customization/useTreeItemModelHook.tsx new file mode 100644 index 000000000000..fedf73068fa7 --- /dev/null +++ b/docs/data/tree-view/tree-item-customization/useTreeItemModelHook.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { TreeItem, TreeItemProps } from '@mui/x-tree-view/TreeItem'; +import { useTreeItemModel } from '@mui/x-tree-view/hooks'; +import { MUI_X_PRODUCTS } from './products'; + +type TreeItemWithLabel = { + id: string; + label: string; + isHighlighted?: boolean; +}; + +const CustomTreeItem = React.forwardRef(function CustomTreeItem( + props: TreeItemProps, + ref: React.Ref, +) { + const item = useTreeItemModel(props.itemId)!; + + return ( + + ); +}); + +export default function useTreeItemModelHook() { + return ( + + + + ); +} diff --git a/docs/data/tree-view/tree-item-customization/useTreeItemModelHook.tsx.preview b/docs/data/tree-view/tree-item-customization/useTreeItemModelHook.tsx.preview new file mode 100644 index 000000000000..1333b932705f --- /dev/null +++ b/docs/data/tree-view/tree-item-customization/useTreeItemModelHook.tsx.preview @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/docs/package.json b/docs/package.json index 9961122d1bae..a2aa363bf7bc 100644 --- a/docs/package.json +++ b/docs/package.json @@ -22,7 +22,7 @@ "@babel/core": "^7.26.0", "@babel/runtime": "^7.26.0", "@babel/runtime-corejs2": "^7.26.0", - "@docsearch/react": "^3.7.0", + "@docsearch/react": "^3.8.0", "@emotion/cache": "^11.13.1", "@emotion/react": "^11.13.3", "@emotion/server": "^11.11.0", @@ -46,7 +46,7 @@ "@mui/x-date-pickers-pro": "workspace:*", "@mui/x-tree-view": "workspace:*", "@react-spring/web": "^9.7.5", - "@tanstack/query-core": "^5.59.20", + "@tanstack/query-core": "^5.60.5", "ast-types": "^0.14.2", "autoprefixer": "^10.4.20", "babel-plugin-module-resolver": "^5.0.2", @@ -71,14 +71,14 @@ "lodash": "^4.17.21", "luxon": "^3.5.0", "lz-string": "^1.5.0", - "markdown-to-jsx": "^7.5.0", + "markdown-to-jsx": "^7.6.2", "moment": "^2.30.1", "moment-hijri": "^3.0.0", "moment-jalaali": "^0.10.4", "moment-timezone": "^0.5.46", - "next": "^14.2.17", + "next": "^14.2.18", "nprogress": "^0.2.0", - "postcss": "^8.4.47", + "postcss": "^8.4.49", "prismjs": "^1.29.0", "prop-types": "^15.8.1", "react": "^18.3.1", diff --git a/docs/pages/_app.js b/docs/pages/_app.js index 8f1471a414c7..3bd2fdb0ca7b 100644 --- a/docs/pages/_app.js +++ b/docs/pages/_app.js @@ -222,6 +222,13 @@ function AppWrapper(props) { href: `${languagePrefix}${productIdSubpathMap[id]}/`, }; } + if (version === 'v7') { + // #default-branch-switch + return { + text: version, + href: `https://mui.com${languagePrefix}${productIdSubpathMap[id]}/`, + }; + } return { text: version, href: `https://${version}.mui.com${languagePrefix}${productIdSubpathMap[id]}/`, @@ -232,7 +239,7 @@ function AppWrapper(props) { metadata: '', name: 'MUI X', versions: [ - ...getVersionOptions('introduction', ['next', process.env.LIB_VERSION, 'v6', 'v5']), + ...getVersionOptions('introduction', [process.env.LIB_VERSION, 'v7', 'v6', 'v5']), { text: 'v4', href: `https://v4.mui.com${languagePrefix}/components/data-grid/` }, ], }; @@ -242,7 +249,7 @@ function AppWrapper(props) { metadata: 'MUI X', name: 'Data Grid', versions: [ - ...getVersionOptions('x-data-grid', ['next', process.env.DATA_GRID_VERSION, 'v6', 'v5']), + ...getVersionOptions('x-data-grid', [process.env.DATA_GRID_VERSION, 'v7', 'v6', 'v5']), { text: 'v4', href: `https://v4.mui.com${languagePrefix}/components/data-grid/` }, ], }; @@ -251,7 +258,7 @@ function AppWrapper(props) { metadata: 'MUI X', name: 'Date Pickers', versions: [ - ...getVersionOptions('x-date-pickers', ['next', process.env.DATE_PICKERS_VERSION, 'v6']), + ...getVersionOptions('x-date-pickers', [process.env.DATE_PICKERS_VERSION, 'v7', 'v6']), { text: 'v5', href: `https://v5.mui.com${languagePrefix}/x/react-date-pickers/getting-started/`, @@ -262,14 +269,14 @@ function AppWrapper(props) { productIdentifier = { metadata: 'MUI X', name: 'Charts', - versions: getVersionOptions('x-charts', ['next', process.env.CHARTS_VERSION, 'v6']), + versions: getVersionOptions('x-charts', [process.env.CHARTS_VERSION, 'v7', 'v6']), }; } else if (productId === 'x-tree-view') { productIdentifier = { metadata: 'MUI X', name: 'Tree View', versions: [ - ...getVersionOptions('x-tree-view', ['next', process.env.TREE_VIEW_VERSION]), + ...getVersionOptions('x-tree-view', [process.env.TREE_VIEW_VERSION, 'v7']), { text: 'v6', href: `https://v6.mui.com${languagePrefix}/x/react-tree-view/getting-started`, diff --git a/docs/pages/x/api/charts/bar-chart-pro.json b/docs/pages/x/api/charts/bar-chart-pro.json index dd938b1859a6..acc058881031 100644 --- a/docs/pages/x/api/charts/bar-chart-pro.json +++ b/docs/pages/x/api/charts/bar-chart-pro.json @@ -10,7 +10,7 @@ "description": "{ x?: 'band'
| 'line'
| 'none', y?: 'band'
| 'line'
| 'none' }" }, "seeMoreLink": { - "url": "https://mui.com/x/react-charts/highlighting", + "url": "https://mui.com/x/react-charts/highlighting/", "text": "highlighting docs" } }, diff --git a/docs/pages/x/api/charts/bar-chart.json b/docs/pages/x/api/charts/bar-chart.json index d0c21625f430..d6224e22275c 100644 --- a/docs/pages/x/api/charts/bar-chart.json +++ b/docs/pages/x/api/charts/bar-chart.json @@ -10,7 +10,7 @@ "description": "{ x?: 'band'
| 'line'
| 'none', y?: 'band'
| 'line'
| 'none' }" }, "seeMoreLink": { - "url": "https://mui.com/x/react-charts/highlighting", + "url": "https://mui.com/x/react-charts/highlighting/", "text": "highlighting docs" } }, diff --git a/docs/pages/x/api/charts/bar-element.json b/docs/pages/x/api/charts/bar-element.json index df4e8a407471..ea3557373d07 100644 --- a/docs/pages/x/api/charts/bar-element.json +++ b/docs/pages/x/api/charts/bar-element.json @@ -22,6 +22,18 @@ } ], "classes": [ + { + "key": "faded", + "className": "MuiBarElement-faded", + "description": "Styles applied to the root element if it is faded.", + "isGlobal": false + }, + { + "key": "highlighted", + "className": "MuiBarElement-highlighted", + "description": "Styles applied to the root element if it is highlighted.", + "isGlobal": false + }, { "key": "root", "className": "MuiBarElement-root", diff --git a/docs/pages/x/api/charts/charts-surface.json b/docs/pages/x/api/charts/charts-surface.json index 6e280f8a0119..ae4ebc5a067c 100644 --- a/docs/pages/x/api/charts/charts-surface.json +++ b/docs/pages/x/api/charts/charts-surface.json @@ -1,9 +1,5 @@ { - "props": { - "height": { "type": { "name": "number" }, "required": true }, - "width": { "type": { "name": "number" }, "required": true }, - "disableAxisListener": { "type": { "name": "bool" }, "default": "false" } - }, + "props": { "disableAxisListener": { "type": { "name": "bool" }, "default": "false" } }, "name": "ChartsSurface", "imports": [ "import { ChartsSurface } from '@mui/x-charts/ChartsSurface';", @@ -11,6 +7,8 @@ "import { ChartsSurface } from '@mui/x-charts-pro';" ], "classes": [], + "spread": true, + "themeDefaultProps": null, "muiName": "MuiChartsSurface", "filename": "/packages/x-charts/src/ChartsSurface/ChartsSurface.tsx", "inheritance": null, diff --git a/docs/pages/x/api/charts/line-chart-pro.json b/docs/pages/x/api/charts/line-chart-pro.json index 7b8d440d88ae..7877f00b1b6e 100644 --- a/docs/pages/x/api/charts/line-chart-pro.json +++ b/docs/pages/x/api/charts/line-chart-pro.json @@ -11,7 +11,7 @@ }, "default": "{ x: 'line' }", "seeMoreLink": { - "url": "https://mui.com/x/react-charts/highlighting", + "url": "https://mui.com/x/react-charts/highlighting/", "text": "highlighting docs" } }, diff --git a/docs/pages/x/api/charts/line-chart.json b/docs/pages/x/api/charts/line-chart.json index fec2c41c1357..a1e466ccdc81 100644 --- a/docs/pages/x/api/charts/line-chart.json +++ b/docs/pages/x/api/charts/line-chart.json @@ -11,7 +11,7 @@ }, "default": "{ x: 'line' }", "seeMoreLink": { - "url": "https://mui.com/x/react-charts/highlighting", + "url": "https://mui.com/x/react-charts/highlighting/", "text": "highlighting docs" } }, diff --git a/docs/pages/x/api/charts/scatter-chart-pro.json b/docs/pages/x/api/charts/scatter-chart-pro.json index fec553960c53..4a25cf42ad78 100644 --- a/docs/pages/x/api/charts/scatter-chart-pro.json +++ b/docs/pages/x/api/charts/scatter-chart-pro.json @@ -11,7 +11,7 @@ }, "default": "{ x: 'none', y: 'none' }", "seeMoreLink": { - "url": "https://mui.com/x/react-charts/highlighting", + "url": "https://mui.com/x/react-charts/highlighting/", "text": "highlighting docs" } }, diff --git a/docs/pages/x/api/charts/scatter-chart.json b/docs/pages/x/api/charts/scatter-chart.json index a3ead27efdcf..bdef64f943c6 100644 --- a/docs/pages/x/api/charts/scatter-chart.json +++ b/docs/pages/x/api/charts/scatter-chart.json @@ -11,7 +11,7 @@ }, "default": "{ x: 'none', y: 'none' }", "seeMoreLink": { - "url": "https://mui.com/x/react-charts/highlighting", + "url": "https://mui.com/x/react-charts/highlighting/", "text": "highlighting docs" } }, diff --git a/docs/pages/x/api/data-grid/data-grid-premium.json b/docs/pages/x/api/data-grid/data-grid-premium.json index ded5c1bd3c90..ec134312c970 100644 --- a/docs/pages/x/api/data-grid/data-grid-premium.json +++ b/docs/pages/x/api/data-grid/data-grid-premium.json @@ -1476,6 +1476,12 @@ "description": "Styles applied to the columns management row element.", "isGlobal": false }, + { + "key": "columnsManagementSearchInput", + "className": "MuiDataGridPremium-columnsManagementSearchInput", + "description": "Styles applied to the columns management search input element.", + "isGlobal": false + }, { "key": "container--bottom", "className": "MuiDataGridPremium-container--bottom", diff --git a/docs/pages/x/api/data-grid/data-grid-pro.json b/docs/pages/x/api/data-grid/data-grid-pro.json index 42df3c139311..8fba87d57224 100644 --- a/docs/pages/x/api/data-grid/data-grid-pro.json +++ b/docs/pages/x/api/data-grid/data-grid-pro.json @@ -1390,6 +1390,12 @@ "description": "Styles applied to the columns management row element.", "isGlobal": false }, + { + "key": "columnsManagementSearchInput", + "className": "MuiDataGridPro-columnsManagementSearchInput", + "description": "Styles applied to the columns management search input element.", + "isGlobal": false + }, { "key": "container--bottom", "className": "MuiDataGridPro-container--bottom", diff --git a/docs/pages/x/api/data-grid/data-grid.json b/docs/pages/x/api/data-grid/data-grid.json index 9994e0dc6317..caf8c0b7d035 100644 --- a/docs/pages/x/api/data-grid/data-grid.json +++ b/docs/pages/x/api/data-grid/data-grid.json @@ -1265,6 +1265,12 @@ "description": "Styles applied to the columns management row element.", "isGlobal": false }, + { + "key": "columnsManagementSearchInput", + "className": "MuiDataGrid-columnsManagementSearchInput", + "description": "Styles applied to the columns management search input element.", + "isGlobal": false + }, { "key": "container--bottom", "className": "MuiDataGrid-container--bottom", diff --git a/docs/pages/x/api/tree-view/rich-tree-view-pro.json b/docs/pages/x/api/tree-view/rich-tree-view-pro.json index 2fcdf49e993d..3456c4228964 100644 --- a/docs/pages/x/api/tree-view/rich-tree-view-pro.json +++ b/docs/pages/x/api/tree-view/rich-tree-view-pro.json @@ -161,12 +161,6 @@ "default": "RichTreeViewProRoot", "class": "MuiRichTreeViewPro-root" }, - { - "name": "item", - "description": "Custom component for the item.", - "default": "TreeItem.", - "class": null - }, { "name": "collapseIcon", "description": "The default icon used to collapse the item.", @@ -181,6 +175,12 @@ "name": "endIcon", "description": "The default icon displayed next to an end item.\nThis is applied to all Tree Items and can be overridden by the TreeItem `icon` slot prop.", "class": null + }, + { + "name": "item", + "description": "Custom component to render a Tree Item.", + "default": "TreeItem.", + "class": null } ], "classes": [], diff --git a/docs/pages/x/api/tree-view/rich-tree-view.json b/docs/pages/x/api/tree-view/rich-tree-view.json index c5ca125dfd5d..7b014b540d84 100644 --- a/docs/pages/x/api/tree-view/rich-tree-view.json +++ b/docs/pages/x/api/tree-view/rich-tree-view.json @@ -136,12 +136,6 @@ "default": "RichTreeViewRoot", "class": "MuiRichTreeView-root" }, - { - "name": "item", - "description": "Custom component for the item.", - "default": "TreeItem.", - "class": null - }, { "name": "collapseIcon", "description": "The default icon used to collapse the item.", @@ -156,6 +150,12 @@ "name": "endIcon", "description": "The default icon displayed next to an end item.\nThis is applied to all Tree Items and can be overridden by the TreeItem `icon` slot prop.", "class": null + }, + { + "name": "item", + "description": "Custom component to render a Tree Item.", + "default": "TreeItem.", + "class": null } ], "classes": [], diff --git a/docs/pages/x/api/tree-view/tree-item.json b/docs/pages/x/api/tree-view/tree-item.json index be0a273ec4c0..f1383007b5fe 100644 --- a/docs/pages/x/api/tree-view/tree-item.json +++ b/docs/pages/x/api/tree-view/tree-item.json @@ -1,7 +1,7 @@ { "props": { "itemId": { "type": { "name": "string" }, "required": true }, - "children": { "type": { "name": "node" } }, + "children": { "type": { "name": "any" } }, "classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } }, "disabled": { "type": { "name": "bool" }, "default": "false" }, "id": { "type": { "name": "string" } }, diff --git a/docs/scripts/generateProptypes.ts b/docs/scripts/generateProptypes.ts index b15053b7c1fd..cad6d50efeee 100644 --- a/docs/scripts/generateProptypes.ts +++ b/docs/scripts/generateProptypes.ts @@ -10,7 +10,11 @@ import { import { fixBabelGeneratorIssues, fixLineEndings } from '@mui/internal-docs-utils'; import { createXTypeScriptProjects, XTypeScriptProject } from './createXTypeScriptProjects'; -const COMPONENTS_WITHOUT_PROPTYPES = ['ChartsAxisTooltipContent', 'ChartsItemTooltipContent']; +const COMPONENTS_WITHOUT_PROPTYPES = [ + 'ChartsAxisTooltipContent', + 'ChartsItemTooltipContent', + 'AnimatedBarElement', +]; async function generateProptypes(project: XTypeScriptProject, sourceFile: string) { const isDateObject = (name: string) => { diff --git a/docs/translations/api-docs/charts/bar-element/bar-element.json b/docs/translations/api-docs/charts/bar-element/bar-element.json index af7c71ba194b..cffdd834ffc1 100644 --- a/docs/translations/api-docs/charts/bar-element/bar-element.json +++ b/docs/translations/api-docs/charts/bar-element/bar-element.json @@ -4,6 +4,18 @@ "slotProps": { "description": "The props used for each component slot." }, "slots": { "description": "Overridable component slots." } }, - "classDescriptions": { "root": { "description": "Styles applied to the root element." } }, + "classDescriptions": { + "faded": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the root element", + "conditions": "it is faded" + }, + "highlighted": { + "description": "Styles applied to {{nodeName}} if {{conditions}}.", + "nodeName": "the root element", + "conditions": "it is highlighted" + }, + "root": { "description": "Styles applied to the root element." } + }, "slotDescriptions": { "bar": "The component that renders the bar." } } diff --git a/docs/translations/api-docs/charts/charts-surface/charts-surface.json b/docs/translations/api-docs/charts/charts-surface/charts-surface.json index a82c8a537f11..40b01b75badb 100644 --- a/docs/translations/api-docs/charts/charts-surface/charts-surface.json +++ b/docs/translations/api-docs/charts/charts-surface/charts-surface.json @@ -3,9 +3,7 @@ "propDescriptions": { "disableAxisListener": { "description": "If true, the charts will not listen to the mouse move event. It might break interactive features, but will improve performance." - }, - "height": { "description": "The height of the chart in px." }, - "width": { "description": "The width of the chart in px." } + } }, "classDescriptions": {} } diff --git a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json index 3b9442aa58e3..8879a0ba952c 100644 --- a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json +++ b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json @@ -923,6 +923,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the columns management row element" }, + "columnsManagementSearchInput": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the columns management search input element" + }, "container--bottom": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the bottom container" diff --git a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json index c1d26982ac46..c56adbe4fb0b 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json @@ -861,6 +861,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the columns management row element" }, + "columnsManagementSearchInput": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the columns management search input element" + }, "container--bottom": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the bottom container" diff --git a/docs/translations/api-docs/data-grid/data-grid/data-grid.json b/docs/translations/api-docs/data-grid/data-grid/data-grid.json index d082ef5d495d..77ba99fb4030 100644 --- a/docs/translations/api-docs/data-grid/data-grid/data-grid.json +++ b/docs/translations/api-docs/data-grid/data-grid/data-grid.json @@ -741,6 +741,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the columns management row element" }, + "columnsManagementSearchInput": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the columns management search input element" + }, "container--bottom": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the bottom container" diff --git a/docs/translations/api-docs/tree-view/rich-tree-view-pro/rich-tree-view-pro.json b/docs/translations/api-docs/tree-view/rich-tree-view-pro/rich-tree-view-pro.json index b43dd9784328..97cdea241f1b 100644 --- a/docs/translations/api-docs/tree-view/rich-tree-view-pro/rich-tree-view-pro.json +++ b/docs/translations/api-docs/tree-view/rich-tree-view-pro/rich-tree-view-pro.json @@ -151,7 +151,7 @@ "collapseIcon": "The default icon used to collapse the item.", "endIcon": "The default icon displayed next to an end item. This is applied to all Tree Items and can be overridden by the TreeItem icon slot prop.", "expandIcon": "The default icon used to expand the item.", - "item": "Custom component for the item.", + "item": "Custom component to render a Tree Item.", "root": "Element rendered at the root." } } diff --git a/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json b/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json index a2fdbd15c255..e0b8130a4faa 100644 --- a/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json +++ b/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json @@ -122,7 +122,7 @@ "collapseIcon": "The default icon used to collapse the item.", "endIcon": "The default icon displayed next to an end item. This is applied to all Tree Items and can be overridden by the TreeItem icon slot prop.", "expandIcon": "The default icon used to expand the item.", - "item": "Custom component for the item.", + "item": "Custom component to render a Tree Item.", "root": "Element rendered at the root." } } diff --git a/package.json b/package.json index cab62f88dc8a..ed11dc54eb8c 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "@mui/internal-markdown": "^1.0.20", "@mui/internal-test-utils": "^1.0.20", "@mui/material": "^5.16.7", - "@mui/monorepo": "github:mui/material-ui#123f0de85460c82ed1dc9b8015878c3239a56895", + "@mui/monorepo": "github:mui/material-ui#a0ffee42815b110e14107249f193b7505d1761e5", "@mui/utils": "^5.16.6", "@next/eslint-plugin-next": "15.0.3", "@octokit/plugin-retry": "^7.1.2", @@ -144,13 +144,13 @@ "eslint-import-resolver-webpack": "^0.13.9", "eslint-plugin-filenames": "^1.3.2", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jsdoc": "^50.4.3", + "eslint-plugin-jsdoc": "^50.5.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-material-ui": "workspace:^", "eslint-plugin-mocha": "^10.5.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react": "^7.37.2", - "eslint-plugin-react-compiler": "19.0.0-beta-63b359f-20241101", + "eslint-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-testing-library": "^6.4.0", "fast-glob": "^3.3.2", @@ -201,9 +201,9 @@ "react-is": "^18.3.1", "@types/node": "^20.17.6" }, - "packageManager": "pnpm@9.12.3", + "packageManager": "pnpm@9.13.2", "engines": { - "pnpm": "9.12.3" + "pnpm": "9.13.2" }, "pnpm": { "patchedDependencies": { diff --git a/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx b/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx index 8b76753a1a54..cf06bc9b1cd9 100644 --- a/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx +++ b/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx @@ -126,7 +126,7 @@ BarChartPro.propTypes = { * The configuration of axes highlight. * Default is set to 'band' in the bar direction. * Depends on `layout` prop. - * @see See {@link https://mui.com/x/react-charts/highlighting highlighting docs} for more details. + * @see See {@link https://mui.com/x/react-charts/highlighting/ highlighting docs} for more details. */ axisHighlight: PropTypes.shape({ x: PropTypes.oneOf(['band', 'line', 'none']), @@ -303,12 +303,6 @@ BarChartPro.propTypes = { * @default null */ topAxis: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), - viewBox: PropTypes.shape({ - height: PropTypes.number, - width: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number, - }), /** * The width of the chart in px. If not defined, it takes the width of the parent element. */ diff --git a/packages/x-charts-pro/src/ChartContainerPro/ChartContainerPro.tsx b/packages/x-charts-pro/src/ChartContainerPro/ChartContainerPro.tsx index 4dbbc461d9ee..a007a28485a5 100644 --- a/packages/x-charts-pro/src/ChartContainerPro/ChartContainerPro.tsx +++ b/packages/x-charts-pro/src/ChartContainerPro/ChartContainerPro.tsx @@ -5,6 +5,7 @@ import type {} from '../typeOverloads'; import { Watermark } from '@mui/x-license/Watermark'; import { ChartContainerProps } from '@mui/x-charts/ChartContainer'; import { ResizableContainer } from '@mui/x-charts/internals'; +import { ChartsSurface } from '@mui/x-charts'; import { getReleaseInfo } from '../internals/utils/releaseInfo'; import { ChartDataProviderPro } from '../context/ChartDataProviderPro'; import { ZoomProps } from '../context/ZoomProvider'; @@ -18,14 +19,16 @@ const ChartContainerPro = React.forwardRef(function ChartContainerPro( props: ChartContainerProProps, ref: React.Ref, ) { - const { chartDataProviderProProps, resizableChartContainerProps, hasIntrinsicSize } = + const { chartDataProviderProProps, children, resizableContainerProps, chartsSurfaceProps } = useChartContainerProProps(props, ref); return ( - - {hasIntrinsicSize ? : null} - - + + + {children} + + + ); }); @@ -119,12 +122,6 @@ ChartContainerPro.propTypes = { PropTypes.object, ]), title: PropTypes.string, - viewBox: PropTypes.shape({ - height: PropTypes.number, - width: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number, - }), /** * The width of the chart in px. If not defined, it takes the width of the parent element. */ diff --git a/packages/x-charts-pro/src/ChartContainerPro/useChartContainerProProps.ts b/packages/x-charts-pro/src/ChartContainerPro/useChartContainerProProps.ts index f9ffc08e4c88..aac56a291a32 100644 --- a/packages/x-charts-pro/src/ChartContainerPro/useChartContainerProProps.ts +++ b/packages/x-charts-pro/src/ChartContainerPro/useChartContainerProProps.ts @@ -1,5 +1,6 @@ 'use client'; import { useChartContainerProps, UseChartContainerPropsReturnValue } from '@mui/x-charts/internals'; +import * as React from 'react'; import type { ChartDataProviderProProps } from '../context/ChartDataProviderPro'; import type { ChartContainerProProps } from './ChartContainerPro'; @@ -13,7 +14,7 @@ export type UseChartContainerProPropsReturnValue = Omit< export const useChartContainerProProps = ( props: ChartContainerProProps, ref: React.Ref, -) => { +): UseChartContainerProPropsReturnValue => { const { zoom, onZoomChange, ...baseProps } = props; const chartDataProviderProProps: Pick = { @@ -21,7 +22,7 @@ export const useChartContainerProProps = ( onZoomChange, }; - const { chartDataProviderProps, resizableChartContainerProps, hasIntrinsicSize } = + const { chartDataProviderProps, chartsSurfaceProps, resizableContainerProps, children } = useChartContainerProps(baseProps, ref); return { @@ -29,7 +30,8 @@ export const useChartContainerProProps = ( ...chartDataProviderProps, ...chartDataProviderProProps, }, - resizableChartContainerProps, - hasIntrinsicSize, + resizableContainerProps, + chartsSurfaceProps, + children, }; }; diff --git a/packages/x-charts-pro/src/Heatmap/Heatmap.tsx b/packages/x-charts-pro/src/Heatmap/Heatmap.tsx index 1309081c80ae..fa6afbe1c682 100644 --- a/packages/x-charts-pro/src/Heatmap/Heatmap.tsx +++ b/packages/x-charts-pro/src/Heatmap/Heatmap.tsx @@ -341,12 +341,6 @@ Heatmap.propTypes = { * @default null */ topAxis: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), - viewBox: PropTypes.shape({ - height: PropTypes.number, - width: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number, - }), /** * The width of the chart in px. If not defined, it takes the width of the parent element. */ diff --git a/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx b/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx index 18b3c810139e..45b21e15c2eb 100644 --- a/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx +++ b/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx @@ -199,7 +199,7 @@ LineChartPro.propTypes = { // ---------------------------------------------------------------------- /** * The configuration of axes highlight. - * @see See {@link https://mui.com/x/react-charts/highlighting highlighting docs} for more details. + * @see See {@link https://mui.com/x/react-charts/highlighting/ highlighting docs} for more details. * @default { x: 'line' } */ axisHighlight: PropTypes.shape({ @@ -375,12 +375,6 @@ LineChartPro.propTypes = { * @default null */ topAxis: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), - viewBox: PropTypes.shape({ - height: PropTypes.number, - width: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number, - }), /** * The width of the chart in px. If not defined, it takes the width of the parent element. */ diff --git a/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx b/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx index c5c0485156fd..5e50274d373c 100644 --- a/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx +++ b/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx @@ -76,7 +76,7 @@ ScatterChartPro.propTypes = { // ---------------------------------------------------------------------- /** * The configuration of axes highlight. - * @see See {@link https://mui.com/x/react-charts/highlighting highlighting docs} for more details. + * @see See {@link https://mui.com/x/react-charts/highlighting/ highlighting docs} for more details. * @default { x: 'none', y: 'none' } */ axisHighlight: PropTypes.shape({ @@ -236,12 +236,6 @@ ScatterChartPro.propTypes = { * @default null */ topAxis: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), - viewBox: PropTypes.shape({ - height: PropTypes.number, - width: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number, - }), /** * Defines the maximal distance between a scatter point and the pointer that triggers the interaction. * If `undefined`, the radius is assumed to be infinite. diff --git a/packages/x-charts-pro/src/context/ChartDataProviderPro/ChartDataProviderPro.tsx b/packages/x-charts-pro/src/context/ChartDataProviderPro/ChartDataProviderPro.tsx index dc225d127f86..91ca02d3f8cd 100644 --- a/packages/x-charts-pro/src/context/ChartDataProviderPro/ChartDataProviderPro.tsx +++ b/packages/x-charts-pro/src/context/ChartDataProviderPro/ChartDataProviderPro.tsx @@ -3,15 +3,14 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { ChartDataProviderProps, - ChartsAxesGradients, DrawingAreaProvider, InteractionProvider, PluginProvider, SeriesProvider, AnimationProvider, SvgRefProvider, + SizeProvider, } from '@mui/x-charts/internals'; -import { ChartsSurface } from '@mui/x-charts/ChartsSurface'; import { HighlightedProvider, ZAxisContextProvider } from '@mui/x-charts/context'; import { useLicenseVerifier } from '@mui/x-license/useLicenseVerifier'; import { getReleaseInfo } from '../../internals/utils/releaseInfo'; @@ -23,29 +22,25 @@ const releaseInfo = getReleaseInfo(); export interface ChartDataProviderProProps extends ChartDataProviderProps, ZoomProps {} -const ChartDataProviderPro = React.forwardRef(function ChartDataProviderPro( - props: ChartDataProviderProProps, - ref: React.Ref, -) { +function ChartDataProviderPro(props: ChartDataProviderProProps) { const { zoomProviderProps, - drawingProviderProps, + drawingAreaProviderProps, seriesProviderProps, zAxisContextProps, highlightedProviderProps, cartesianProviderProps, - chartsSurfaceProps, + sizeProviderProps, pluginProviderProps, animationProviderProps, - svgRefProviderProps, children, - } = useChartContainerProProps(props, ref); + } = useChartContainerProProps(props); useLicenseVerifier('x-charts-pro', releaseInfo); return ( - - + + @@ -54,10 +49,7 @@ const ChartDataProviderPro = React.forwardRef(function ChartDataProviderPro( - - - {children} - + {children} @@ -66,10 +58,10 @@ const ChartDataProviderPro = React.forwardRef(function ChartDataProviderPro( - - + + ); -}); +} ChartDataProviderPro.propTypes = { // ----------------------------- Warning -------------------------------- diff --git a/packages/x-charts-pro/src/context/ChartDataProviderPro/useChartDataProviderProProps.ts b/packages/x-charts-pro/src/context/ChartDataProviderPro/useChartDataProviderProProps.ts index 35e227adf5a3..39e6726455b1 100644 --- a/packages/x-charts-pro/src/context/ChartDataProviderPro/useChartDataProviderProProps.ts +++ b/packages/x-charts-pro/src/context/ChartDataProviderPro/useChartDataProviderProProps.ts @@ -3,26 +3,22 @@ import { useChartDataProviderProps } from '@mui/x-charts/internals'; import { ZoomProviderProps } from '../ZoomProvider'; import type { ChartDataProviderProProps } from './ChartDataProviderPro'; -export const useChartContainerProProps = ( - props: ChartDataProviderProProps, - ref: React.Ref, -) => { +export const useChartContainerProProps = (props: ChartDataProviderProProps) => { const { zoom, onZoomChange, ...baseProps } = props; const { children, - drawingProviderProps, + drawingAreaProviderProps, seriesProviderProps, cartesianProviderProps, zAxisContextProps, highlightedProviderProps, - chartsSurfaceProps, + sizeProviderProps, pluginProviderProps, animationProviderProps, - svgRefProviderProps, xAxis, yAxis, - } = useChartDataProviderProps(baseProps, ref); + } = useChartDataProviderProps(baseProps); const zoomProviderProps: Omit = { zoom, @@ -34,14 +30,13 @@ export const useChartContainerProProps = ( return { zoomProviderProps, children, - drawingProviderProps, + drawingAreaProviderProps, pluginProviderProps, seriesProviderProps, cartesianProviderProps, zAxisContextProps, highlightedProviderProps, - chartsSurfaceProps, + sizeProviderProps, animationProviderProps, - svgRefProviderProps, }; }; diff --git a/packages/x-charts-vendor/.babelrc.js b/packages/x-charts-vendor/.babelrc.js index 21146f4cbc44..c009d131c378 100644 --- a/packages/x-charts-vendor/.babelrc.js +++ b/packages/x-charts-vendor/.babelrc.js @@ -43,7 +43,7 @@ module.exports = { // - 'node_modules/d3-interpolate/src/rgb.js' // - 'lib-vendor/d3-interpolate/src/rgb.js' // and have an import transform like: - // - `d3-color` + // - `d3-color` (d3 color is imported by d3-interpolate) // - `../../d3-color` const currentFileVendor = currentFile.replace(/^node_modules/, 'lib-vendor'); const relPathToPkg = path diff --git a/packages/x-charts/package.json b/packages/x-charts/package.json index f4f1fa97de31..f04c575f7cf4 100644 --- a/packages/x-charts/package.json +++ b/packages/x-charts/package.json @@ -46,7 +46,9 @@ "@react-spring/rafz": "^9.7.5", "@react-spring/web": "^9.7.5", "clsx": "^2.1.1", - "prop-types": "^15.8.1" + "prop-types": "^15.8.1", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.0.0" }, "peerDependencies": { "@emotion/react": "^11.9.0", @@ -71,6 +73,7 @@ "@react-spring/core": "^9.7.5", "@react-spring/shared": "^9.7.5", "@types/prop-types": "^15.7.13", + "@types/use-sync-external-store": "^0.0.6", "csstype": "^3.1.3", "rimraf": "^6.0.1" }, diff --git a/packages/x-charts/src/BarChart/AnimatedBarElement.tsx b/packages/x-charts/src/BarChart/AnimatedBarElement.tsx new file mode 100644 index 000000000000..854f64ffcc2a --- /dev/null +++ b/packages/x-charts/src/BarChart/AnimatedBarElement.tsx @@ -0,0 +1,33 @@ +'use client'; +import * as React from 'react'; +import { AnimatedProps, animated } from '@react-spring/web'; +import type { BarElementOwnerState } from './BarElement'; + +export interface BarProps + extends Omit< + React.SVGProps, + 'id' | 'color' | 'ref' | 'x' | 'y' | 'height' | 'width' + >, + AnimatedProps<{ + x?: string | number | undefined; + y?: string | number | undefined; + height?: string | number | undefined; + width?: string | number | undefined; + }> { + ownerState: BarElementOwnerState; +} + +/** + * @ignore - internal component. + */ +export function AnimatedBarElement(props: BarProps) { + const { ownerState, ...other } = props; + + return ( + + ); +} diff --git a/packages/x-charts/src/BarChart/BarChart.tsx b/packages/x-charts/src/BarChart/BarChart.tsx index 2b2b4dde4c0f..9a0fb4abebb2 100644 --- a/packages/x-charts/src/BarChart/BarChart.tsx +++ b/packages/x-charts/src/BarChart/BarChart.tsx @@ -67,7 +67,7 @@ export interface BarChartProps * The configuration of axes highlight. * Default is set to 'band' in the bar direction. * Depends on `layout` prop. - * @see See {@link https://mui.com/x/react-charts/highlighting highlighting docs} for more details. + * @see See {@link https://mui.com/x/react-charts/highlighting/ highlighting docs} for more details. * */ axisHighlight?: ChartsAxisHighlightProps; @@ -150,7 +150,7 @@ BarChart.propTypes = { * The configuration of axes highlight. * Default is set to 'band' in the bar direction. * Depends on `layout` prop. - * @see See {@link https://mui.com/x/react-charts/highlighting highlighting docs} for more details. + * @see See {@link https://mui.com/x/react-charts/highlighting/ highlighting docs} for more details. */ axisHighlight: PropTypes.shape({ x: PropTypes.oneOf(['band', 'line', 'none']), @@ -321,12 +321,6 @@ BarChart.propTypes = { * @default null */ topAxis: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), - viewBox: PropTypes.shape({ - height: PropTypes.number, - width: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number, - }), /** * The width of the chart in px. If not defined, it takes the width of the parent element. */ diff --git a/packages/x-charts/src/BarChart/BarElement.tsx b/packages/x-charts/src/BarChart/BarElement.tsx index ce5f91fcfd2f..f755f388385d 100644 --- a/packages/x-charts/src/BarChart/BarElement.tsx +++ b/packages/x-charts/src/BarChart/BarElement.tsx @@ -4,18 +4,20 @@ import PropTypes from 'prop-types'; import composeClasses from '@mui/utils/composeClasses'; import useSlotProps from '@mui/utils/useSlotProps'; import generateUtilityClass from '@mui/utils/generateUtilityClass'; -import { styled } from '@mui/material/styles'; import generateUtilityClasses from '@mui/utils/generateUtilityClasses'; -import { color as d3Color } from '@mui/x-charts-vendor/d3-color'; -import { AnimatedProps, animated } from '@react-spring/web'; import { SlotComponentPropsFromProps } from '@mui/x-internals/types'; import { useInteractionItemProps } from '../hooks/useInteractionItemProps'; import { SeriesId } from '../models/seriesType/common'; import { useItemHighlighted } from '../context'; +import { AnimatedBarElement, BarProps } from './AnimatedBarElement'; export interface BarElementClasses { /** Styles applied to the root element. */ root: string; + /** Styles applied to the root element if it is highlighted. */ + highlighted: string; + /** Styles applied to the root element if it is faded. */ + faded: string; } export type BarElementClassKey = keyof BarElementClasses; @@ -35,44 +37,19 @@ export function getBarElementUtilityClass(slot: string) { export const barElementClasses: BarElementClasses = generateUtilityClasses('MuiBarElement', [ 'root', + 'highlighted', + 'faded', ]); const useUtilityClasses = (ownerState: BarElementOwnerState) => { - const { classes, id } = ownerState; + const { classes, id, isHighlighted, isFaded } = ownerState; const slots = { - root: ['root', `series-${id}`], + root: ['root', `series-${id}`, isHighlighted && 'highlighted', isFaded && 'faded'], }; return composeClasses(slots, getBarElementUtilityClass, classes); }; -export const BarElementPath = styled(animated.rect, { - name: 'MuiBarElement', - slot: 'Root', - overridesResolver: (_, styles) => styles.root, -})<{ ownerState: BarElementOwnerState }>(({ ownerState }) => ({ - stroke: 'none', - fill: ownerState.isHighlighted - ? d3Color(ownerState.color)!.brighter(0.5).formatHex() - : ownerState.color, - transition: 'opacity 0.2s ease-in, fill 0.2s ease-in', - opacity: (ownerState.isFaded && 0.3) || 1, -})); - -interface BarProps - extends Omit< - React.SVGProps, - 'id' | 'color' | 'ref' | 'x' | 'y' | 'height' | 'width' - >, - AnimatedProps<{ - x?: string | number | undefined; - y?: string | number | undefined; - height?: string | number | undefined; - width?: string | number | undefined; - }> { - ownerState: BarElementOwnerState; -} - export interface BarElementSlots { /** * The component that renders the bar. @@ -124,9 +101,10 @@ function BarElement(props: BarElementProps) { isFaded, isHighlighted, }; + const classes = useUtilityClasses(ownerState); - const Bar = slots?.bar ?? (BarElementPath as React.ElementType); + const Bar = slots?.bar ?? AnimatedBarElement; const barProps = useSlotProps({ elementType: Bar, @@ -137,6 +115,8 @@ function BarElement(props: BarElementProps) { style, onClick, cursor: onClick ? 'pointer' : 'unset', + stroke: 'none', + fill: color, }, className: classes.root, ownerState, diff --git a/packages/x-charts/src/BarChart/BarPlot.tsx b/packages/x-charts/src/BarChart/BarPlot.tsx index e7cbac28c1ec..33924df041db 100644 --- a/packages/x-charts/src/BarChart/BarPlot.tsx +++ b/packages/x-charts/src/BarChart/BarPlot.tsx @@ -2,8 +2,9 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { useTransition } from '@react-spring/web'; +import { styled } from '@mui/material/styles'; import { useCartesianContext } from '../context/CartesianProvider'; -import { BarElement, BarElementSlotProps, BarElementSlots } from './BarElement'; +import { BarElement, barElementClasses, BarElementSlotProps, BarElementSlots } from './BarElement'; import { AxisDefaultized } from '../models/axis'; import { BarItemIdentifier } from '../models'; import getColor from './getColor'; @@ -234,6 +235,16 @@ const enterStyle = ({ x, width, y, height }: AnimationData) => ({ width, }); +const BarPlotRoot = styled('g', { + name: 'MuiBarPlot', + slot: 'Root', + overridesResolver: (_, styles) => styles.root, +})({ + [`& .${barElementClasses.root}`]: { + transition: 'opacity 0.2s ease-in, fill 0.2s ease-in', + }, +}); + /** * Demos: * @@ -271,7 +282,7 @@ function BarPlot(props: BarPlotProps) { }); return ( - + {!withoutBorderRadius && maskTransition((style, { id, hasPositive, hasNegative, layout }) => { return ( @@ -316,7 +327,7 @@ function BarPlot(props: BarPlotProps) { {...other} /> )} - + ); } diff --git a/packages/x-charts/src/ChartContainer/ChartContainer.tsx b/packages/x-charts/src/ChartContainer/ChartContainer.tsx index 54b013ad2235..0f34ea4c88a7 100644 --- a/packages/x-charts/src/ChartContainer/ChartContainer.tsx +++ b/packages/x-charts/src/ChartContainer/ChartContainer.tsx @@ -4,39 +4,23 @@ import PropTypes from 'prop-types'; import { ChartDataProvider, ChartDataProviderProps } from '../context/ChartDataProvider'; import { ResizableContainer } from './ResizableContainer'; import { useChartContainerProps } from './useChartContainerProps'; +import { ChartsSurface, ChartsSurfaceProps } from '../ChartsSurface'; -export interface ChartContainerProps extends Omit { - /** - * The width of the chart in px. If not defined, it takes the width of the parent element. - */ - width?: number; - /** - * The height of the chart in px. If not defined, it takes the height of the parent element. - */ - height?: number; - /** - * The chart will try to wait for the parent container to resolve its size - * before it renders for the first time. - * - * This can be useful in some scenarios where the chart appear to grow after - * the first render, like when used inside a grid. - * - * @default false - */ - resolveSizeBeforeRender?: boolean; -} +export interface ChartContainerProps extends ChartDataProviderProps, ChartsSurfaceProps {} const ChartContainer = React.forwardRef(function ChartContainer( props: ChartContainerProps, ref: React.Ref, ) { - const { hasIntrinsicSize, chartDataProviderProps, resizableChartContainerProps } = + const { chartDataProviderProps, children, resizableContainerProps, chartsSurfaceProps } = useChartContainerProps(props, ref); return ( - - {hasIntrinsicSize ? : null} - + + + {children} + + ); }); @@ -124,12 +108,6 @@ ChartContainer.propTypes = { PropTypes.object, ]), title: PropTypes.string, - viewBox: PropTypes.shape({ - height: PropTypes.number, - width: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number, - }), /** * The width of the chart in px. If not defined, it takes the width of the parent element. */ diff --git a/packages/x-charts/src/ChartContainer/ResizableContainer.tsx b/packages/x-charts/src/ChartContainer/ResizableContainer.tsx index 30c9099a5caf..aacc203af003 100644 --- a/packages/x-charts/src/ChartContainer/ResizableContainer.tsx +++ b/packages/x-charts/src/ChartContainer/ResizableContainer.tsx @@ -1,15 +1,18 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; import { styled } from '@mui/material/styles'; -import type { ChartContainerProps } from './ChartContainer'; +import { useSize } from '../context/SizeProvider'; +import type { SizeContextState } from '../context/SizeProvider'; /** * Wrapping div that take the shape of its parent. * * @ignore - do not document. */ -export const ResizableContainer = styled('div', { +export const ResizableContainerRoot = styled('div', { name: 'MuiResponsiveChart', slot: 'Container', -})<{ ownerState: Pick }>(({ ownerState }) => ({ +})<{ ownerState: Partial> }>(({ ownerState }) => ({ width: ownerState.width ?? '100%', height: ownerState.height ?? '100%', display: 'flex', @@ -24,3 +27,32 @@ export const ResizableContainer = styled('div', { height: '100%', }, })); + +/** + * Wrapping div that take the shape of its parent. + * + * @ignore - do not document. + */ +function ResizableContainer(props: { children: React.ReactNode }) { + const { inHeight, inWidth, hasIntrinsicSize, containerRef } = useSize(); + + return ( + + {hasIntrinsicSize && props.children} + + ); +} + +ResizableContainer.propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the TypeScript types and run "pnpm proptypes" | + // ---------------------------------------------------------------------- + children: PropTypes.node, +} as any; + +export { ResizableContainer }; diff --git a/packages/x-charts/src/ChartContainer/useChartContainerProps.ts b/packages/x-charts/src/ChartContainer/useChartContainerProps.ts index 62930f3985ee..8fa2a647f20d 100644 --- a/packages/x-charts/src/ChartContainer/useChartContainerProps.ts +++ b/packages/x-charts/src/ChartContainer/useChartContainerProps.ts @@ -1,15 +1,14 @@ 'use client'; +import * as React from 'react'; +import { ChartsSurfaceProps } from '../ChartsSurface'; import { ChartDataProviderProps } from '../context/ChartDataProvider'; import type { ChartContainerProps } from './ChartContainer'; -import { useChartContainerDimensions } from './useChartContainerDimensions'; export type UseChartContainerPropsReturnValue = { - hasIntrinsicSize: boolean; chartDataProviderProps: ChartDataProviderProps; - resizableChartContainerProps: { - ownerState: { width: ChartContainerProps['width']; height: ChartContainerProps['height'] }; - ref: React.Ref; - }; + chartsSurfaceProps: ChartsSurfaceProps & { ref: React.Ref }; + resizableContainerProps: any; + children: React.ReactNode; }; export const useChartContainerProps = ( @@ -19,7 +18,6 @@ export const useChartContainerProps = ( const { width, height, - resolveSizeBeforeRender, margin, children, series, @@ -32,7 +30,6 @@ export const useChartContainerProps = ( plugins, sx, title, - viewBox, xAxis, yAxis, zAxis, @@ -40,44 +37,36 @@ export const useChartContainerProps = ( ...other } = props; - const { - containerRef, - width: dWidth, - height: dHeight, - } = useChartContainerDimensions(width, height, resolveSizeBeforeRender); + const resizableContainerProps = other; - const resizableChartContainerProps = { - ...other, - ownerState: { width, height }, - ref: containerRef, + const chartsSurfaceProps: ChartsSurfaceProps & { ref: React.Ref } = { + title, + desc, + sx, + disableAxisListener, + ref, }; - const chartDataProviderProps = { + const chartDataProviderProps: ChartDataProviderProps = { margin, - children, series, colors, dataset, - desc, - disableAxisListener, highlightedItem, onHighlightChange, plugins, - sx, - title, - viewBox, xAxis, yAxis, zAxis, skipAnimation, - width: dWidth, - height: dHeight, - ref, + width, + height, }; return { - hasIntrinsicSize: Boolean(dWidth && dHeight), chartDataProviderProps, - resizableChartContainerProps, + resizableContainerProps, + chartsSurfaceProps, + children, }; }; diff --git a/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlight.tsx b/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlight.tsx index 5712979e916c..a53bfafff95f 100644 --- a/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlight.tsx +++ b/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlight.tsx @@ -2,29 +2,10 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import composeClasses from '@mui/utils/composeClasses'; -import generateUtilityClass from '@mui/utils/generateUtilityClass'; -import generateUtilityClasses from '@mui/utils/generateUtilityClasses'; -import { styled } from '@mui/material/styles'; -import { InteractionContext } from '../context/InteractionProvider'; -import { useCartesianContext } from '../context/CartesianProvider'; -import { getValueToPositionMapper } from '../hooks/useScale'; -import { isBandScale } from '../internals/isBandScale'; - -export interface ChartsAxisHighlightClasses { - /** Styles applied to the root element. */ - root: string; -} - -export type ChartsAxisHighlightClassKey = keyof ChartsAxisHighlightClasses; - -export function getAxisHighlightUtilityClass(slot: string) { - return generateUtilityClass('MuiChartsAxisHighlight', slot); -} - -export const chartsAxisHighlightClasses: ChartsAxisHighlightClasses = generateUtilityClasses( - 'MuiChartsAxisHighlight', - ['root'], -); +import { getAxisHighlightUtilityClass } from './chartsAxisHighlightClasses'; +import ChartsYHighlight from './ChartsYAxisHighlight'; +import ChartsXHighlight from './ChartsXAxisHighlight'; +import { ChartsAxisHighlightProps } from './ChartsAxisHighlight.types'; const useUtilityClasses = () => { const slots = { @@ -34,47 +15,6 @@ const useUtilityClasses = () => { return composeClasses(slots, getAxisHighlightUtilityClass); }; -export const ChartsAxisHighlightPath = styled('path', { - name: 'MuiChartsAxisHighlight', - slot: 'Root', - overridesResolver: (_, styles) => styles.root, -})<{ ownerState: { axisHighlight: AxisHighlight } }>(({ theme }) => ({ - pointerEvents: 'none', - variants: [ - { - props: { - axisHighlight: 'band', - }, - style: { - fill: 'white', - fillOpacity: 0.1, - ...theme.applyStyles('light', { - fill: 'gray', - }), - }, - }, - { - props: { - axisHighlight: 'line', - }, - style: { - strokeDasharray: '5 2', - stroke: '#ffffff', - ...theme.applyStyles('light', { - stroke: '#000000', - }), - }, - }, - ], -})); - -type AxisHighlight = 'none' | 'line' | 'band'; - -export type ChartsAxisHighlightProps = { - x?: AxisHighlight; - y?: AxisHighlight; -}; - /** * Demos: * @@ -86,88 +26,12 @@ export type ChartsAxisHighlightProps = { */ function ChartsAxisHighlight(props: ChartsAxisHighlightProps) { const { x: xAxisHighlight, y: yAxisHighlight } = props; - const { xAxisIds, xAxis, yAxisIds, yAxis } = useCartesianContext(); - const classes = useUtilityClasses(); - - const USED_X_AXIS_ID = xAxisIds[0]; - const USED_Y_AXIS_ID = yAxisIds[0]; - - const xScale = xAxis[USED_X_AXIS_ID].scale; - const yScale = yAxis[USED_Y_AXIS_ID].scale; - - const { axis } = React.useContext(InteractionContext); - - const getXPosition = getValueToPositionMapper(xScale); - const getYPosition = getValueToPositionMapper(yScale); - - const axisX = axis.x; - const axisY = axis.y; - - const isBandScaleX = xAxisHighlight === 'band' && axisX !== null && isBandScale(xScale); - const isBandScaleY = yAxisHighlight === 'band' && axisY !== null && isBandScale(yScale); - - if (process.env.NODE_ENV !== 'production') { - const isXError = isBandScaleX && xScale(axisX.value) === undefined; - const isYError = isBandScaleY && yScale(axisY.value) === undefined; - - if (isXError || isYError) { - console.error( - [ - `MUI X: The position value provided for the axis is not valid for the current scale.`, - `This probably means something is wrong with the data passed to the chart.`, - `The ChartsAxisHighlight component will not be displayed.`, - ].join('\n'), - ); - } - } + const classes = useUtilityClasses(); return ( - {isBandScaleX && xScale(axisX.value) !== undefined && ( - - )} - - {isBandScaleY && yScale(axisY.value) !== undefined && ( - - )} - - {xAxisHighlight === 'line' && axis.x !== null && ( - - )} - - {yAxisHighlight === 'line' && axis.y !== null && ( - - )} + {xAxisHighlight && } + {yAxisHighlight && } ); } diff --git a/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlight.types.ts b/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlight.types.ts new file mode 100644 index 000000000000..de1b75d43438 --- /dev/null +++ b/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlight.types.ts @@ -0,0 +1,6 @@ +export type ChartsAxisHighlightType = 'none' | 'line' | 'band'; + +export type ChartsAxisHighlightProps = { + x?: ChartsAxisHighlightType; + y?: ChartsAxisHighlightType; +}; diff --git a/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlightPath.ts b/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlightPath.ts new file mode 100644 index 000000000000..d015d2b5d15c --- /dev/null +++ b/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlightPath.ts @@ -0,0 +1,37 @@ +'use client'; +import { styled } from '@mui/material/styles'; +import { ChartsAxisHighlightType } from './ChartsAxisHighlight.types'; + +export const ChartsAxisHighlightPath = styled('path', { + name: 'MuiChartsAxisHighlight', + slot: 'Root', + overridesResolver: (_, styles) => styles.root, +})<{ ownerState: { axisHighlight: ChartsAxisHighlightType } }>(({ theme }) => ({ + pointerEvents: 'none', + variants: [ + { + props: { + axisHighlight: 'band', + }, + style: { + fill: 'white', + fillOpacity: 0.1, + ...theme.applyStyles('light', { + fill: 'gray', + }), + }, + }, + { + props: { + axisHighlight: 'line', + }, + style: { + strokeDasharray: '5 2', + stroke: '#ffffff', + ...theme.applyStyles('light', { + stroke: '#000000', + }), + }, + }, + ], +})); diff --git a/packages/x-charts/src/ChartsAxisHighlight/ChartsXAxisHighlight.tsx b/packages/x-charts/src/ChartsAxisHighlight/ChartsXAxisHighlight.tsx new file mode 100644 index 000000000000..d828547af94e --- /dev/null +++ b/packages/x-charts/src/ChartsAxisHighlight/ChartsXAxisHighlight.tsx @@ -0,0 +1,69 @@ +'use client'; +import * as React from 'react'; +import { getValueToPositionMapper, useXScale } from '../hooks/useScale'; +import { isBandScale } from '../internals/isBandScale'; +import { useSelector } from '../internals/useSelector'; +import { useStore } from '../internals/useStore'; +import { selectorChartsInteractionXAxis } from '../context/InteractionSelectors'; +import { useDrawingArea } from '../hooks'; +import { ChartsAxisHighlightType } from './ChartsAxisHighlight.types'; +import { ChartsAxisHighlightClasses } from './chartsAxisHighlightClasses'; +import { ChartsAxisHighlightPath } from './ChartsAxisHighlightPath'; + +/** + * @ignore - internal component. + */ +export default function ChartsXHighlight(props: { + type: ChartsAxisHighlightType; + classes: ChartsAxisHighlightClasses; +}) { + const { type, classes } = props; + + const { top, height } = useDrawingArea(); + + const xScale = useXScale(); + + const store = useStore(); + const axisX = useSelector(store, selectorChartsInteractionXAxis); + + const getXPosition = getValueToPositionMapper(xScale); + + const isBandScaleX = type === 'band' && axisX !== null && isBandScale(xScale); + + if (process.env.NODE_ENV !== 'production') { + const isError = isBandScaleX && xScale(axisX.value) === undefined; + + if (isError) { + console.error( + [ + `MUI X: The position value provided for the axis is not valid for the current scale.`, + `This probably means something is wrong with the data passed to the chart.`, + `The ChartsAxisHighlight component will not be displayed.`, + ].join('\n'), + ); + } + } + + return ( + + {isBandScaleX && xScale(axisX.value) !== undefined && ( + + )} + + {type === 'line' && axisX !== null && ( + + )} + + ); +} diff --git a/packages/x-charts/src/ChartsAxisHighlight/ChartsYAxisHighlight.tsx b/packages/x-charts/src/ChartsAxisHighlight/ChartsYAxisHighlight.tsx new file mode 100644 index 000000000000..ce996fb544df --- /dev/null +++ b/packages/x-charts/src/ChartsAxisHighlight/ChartsYAxisHighlight.tsx @@ -0,0 +1,71 @@ +'use client'; +import * as React from 'react'; +import { getValueToPositionMapper, useYScale } from '../hooks/useScale'; +import { isBandScale } from '../internals/isBandScale'; +import { useSelector } from '../internals/useSelector'; +import { useStore } from '../internals/useStore'; +import { selectorChartsInteractionYAxis } from '../context/InteractionSelectors'; +import { useDrawingArea } from '../hooks'; +import { ChartsAxisHighlightType } from './ChartsAxisHighlight.types'; +import { ChartsAxisHighlightClasses } from './chartsAxisHighlightClasses'; +import { ChartsAxisHighlightPath } from './ChartsAxisHighlightPath'; + +/** + * @ignore - internal component. + */ +export default function ChartsYHighlight(props: { + type: ChartsAxisHighlightType; + classes: ChartsAxisHighlightClasses; +}) { + const { type, classes } = props; + + const { left, width } = useDrawingArea(); + + const yScale = useYScale(); + + const store = useStore(); + const axisY = useSelector(store, selectorChartsInteractionYAxis); + + const getYPosition = getValueToPositionMapper(yScale); + + const isBandScaleY = type === 'band' && axisY !== null && isBandScale(yScale); + + if (process.env.NODE_ENV !== 'production') { + const isError = isBandScaleY && yScale(axisY.value) === undefined; + + if (isError) { + console.error( + [ + `MUI X: The position value provided for the axis is not valid for the current scale.`, + `This probably means something is wrong with the data passed to the chart.`, + `The ChartsAxisHighlight component will not be displayed.`, + ].join('\n'), + ); + } + } + + return ( + + {isBandScaleY && yScale(axisY.value) !== undefined && ( + + )} + + {type === 'line' && axisY !== null && ( + + )} + + ); +} diff --git a/packages/x-charts/src/ChartsAxisHighlight/chartsAxisHighlightClasses.ts b/packages/x-charts/src/ChartsAxisHighlight/chartsAxisHighlightClasses.ts new file mode 100644 index 000000000000..7d62c32d82fa --- /dev/null +++ b/packages/x-charts/src/ChartsAxisHighlight/chartsAxisHighlightClasses.ts @@ -0,0 +1,18 @@ +import generateUtilityClass from '@mui/utils/generateUtilityClass'; +import generateUtilityClasses from '@mui/utils/generateUtilityClasses'; + +export interface ChartsAxisHighlightClasses { + /** Styles applied to the root element. */ + root: string; +} + +export type ChartsAxisHighlightClassKey = keyof ChartsAxisHighlightClasses; + +export function getAxisHighlightUtilityClass(slot: string) { + return generateUtilityClass('MuiChartsAxisHighlight', slot); +} + +export const chartsAxisHighlightClasses: ChartsAxisHighlightClasses = generateUtilityClasses( + 'MuiChartsAxisHighlight', + ['root'], +); diff --git a/packages/x-charts/src/ChartsAxisHighlight/index.ts b/packages/x-charts/src/ChartsAxisHighlight/index.ts index 14c071e360be..653afd1cfd87 100644 --- a/packages/x-charts/src/ChartsAxisHighlight/index.ts +++ b/packages/x-charts/src/ChartsAxisHighlight/index.ts @@ -1 +1,4 @@ export * from './ChartsAxisHighlight'; +export * from './chartsAxisHighlightClasses'; +export * from './ChartsAxisHighlight.types'; +export * from './ChartsAxisHighlightPath'; diff --git a/packages/x-charts/src/ChartsGrid/ChartsHorizontalGrid.tsx b/packages/x-charts/src/ChartsGrid/ChartsHorizontalGrid.tsx index 85ab5f0dadfc..648a9de08cb6 100644 --- a/packages/x-charts/src/ChartsGrid/ChartsHorizontalGrid.tsx +++ b/packages/x-charts/src/ChartsGrid/ChartsHorizontalGrid.tsx @@ -23,9 +23,9 @@ export function ChartsGridHorizontal(props: ChartsGridHorizontalProps) { return ( - {yTicks.map(({ formattedValue, offset }) => ( + {yTicks.map(({ value, offset }) => ( - {xTicks.map(({ formattedValue, offset }) => ( + {xTicks.map(({ value, offset }) => ( { @@ -39,9 +40,10 @@ function ChartsOnAxisClickHandler(props: ChartsOnAxisClickHandlerProps) { const handleMouseClick = (event: MouseEvent) => { event.preventDefault(); - const isXaxis = axis.x && axis.x.index !== -1; + const { x: axisX, y: axisY } = store.value.interaction.axis; + const isXaxis = axisX && axisX.index !== -1; const USED_AXIS_ID = isXaxis ? xAxisIds[0] : yAxisIds[0]; - const dataIndex = isXaxis ? axis.x && axis.x.index : axis.y && axis.y.index; + const dataIndex = isXaxis ? axisX && axisX.index : axisY && axisY.index; if (dataIndex == null) { return; @@ -72,7 +74,7 @@ function ChartsOnAxisClickHandler(props: ChartsOnAxisClickHandlerProps) { return () => { element.removeEventListener('click', handleMouseClick); }; - }, [axis.x, axis.y, onAxisClick, series, svgRef, xAxis, xAxisIds, yAxis, yAxisIds]); + }, [onAxisClick, series, store, svgRef, xAxis, xAxisIds, yAxis, yAxisIds]); // eslint-disable-next-line react/jsx-no-useless-fragment return ; diff --git a/packages/x-charts/src/ChartsSurface/ChartsSurface.test.tsx b/packages/x-charts/src/ChartsSurface/ChartsSurface.test.tsx new file mode 100644 index 000000000000..8a61ef8212f6 --- /dev/null +++ b/packages/x-charts/src/ChartsSurface/ChartsSurface.test.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { createRenderer } from '@mui/internal-test-utils'; +import { ChartsSurface } from '@mui/x-charts/ChartsSurface'; +import { expect } from 'chai'; +import { SvgRefProvider } from '../context/SvgRefProvider'; +import { SizeProvider } from '../context/SizeProvider'; + +describe('', () => { + // JSDOM doesn't implement SVGElement + if (/jsdom/.test(window.navigator.userAgent)) { + return; + } + + const { render } = createRenderer(); + + it('should pass ref when it is added directly to component', () => { + const ref = React.createRef(); + + render( + + + + + + + , + ); + + expect(ref.current instanceof SVGElement, 'ref is a SVGElement').to.equal(true); + expect( + ref.current?.lastElementChild instanceof SVGRectElement, + 'ref last child is a SVGRectElement', + ).to.equal(true); + }); +}); diff --git a/packages/x-charts/src/ChartsSurface/ChartsSurface.tsx b/packages/x-charts/src/ChartsSurface/ChartsSurface.tsx index 97f4b89868cf..9939f798d1b3 100644 --- a/packages/x-charts/src/ChartsSurface/ChartsSurface.tsx +++ b/packages/x-charts/src/ChartsSurface/ChartsSurface.tsx @@ -2,24 +2,13 @@ import { styled, SxProps, Theme, useThemeProps } from '@mui/material/styles'; import PropTypes from 'prop-types'; import * as React from 'react'; +import useForkRef from '@mui/utils/useForkRef'; import { useAxisEvents } from '../hooks/useAxisEvents'; +import { ChartsAxesGradients } from '../internals/components/ChartsAxesGradients'; +import { useDrawingArea } from '../hooks'; +import { useSurfaceRef } from '../context/SvgRefProvider'; -type ViewBox = { - x?: number; - y?: number; - width?: number; - height?: number; -}; export interface ChartsSurfaceProps { - /** - * The width of the chart in px. - */ - width: number; - /** - * The height of the chart in px. - */ - height: number; - viewBox?: ViewBox; className?: string; title?: string; desc?: string; @@ -33,7 +22,7 @@ export interface ChartsSurfaceProps { disableAxisListener?: boolean; } -const ChartChartsSurfaceStyles = styled('svg', { +const ChartsSurfaceStyles = styled('svg', { name: 'MuiChartsSurface', slot: 'Root', })(() => ({ @@ -46,35 +35,39 @@ const ChartsSurface = React.forwardRef(functi inProps: ChartsSurfaceProps, ref: React.Ref, ) { - const props = useThemeProps({ props: inProps, name: 'MuiChartsSurface' }); - const { - children, - width, - height, - viewBox, - disableAxisListener = false, - className, - title, - desc, - ...other - } = props; - const svgView = { width, height, x: 0, y: 0, ...viewBox }; + const { width, height, left, right, top, bottom } = useDrawingArea(); + const surfaceRef = useSurfaceRef(); + const handleRef = useForkRef(surfaceRef, ref); + const themeProps = useThemeProps({ props: inProps, name: 'MuiChartsSurface' }); + + const { children, disableAxisListener = false, className, title, desc, ...other } = themeProps; + + const svgWidth = width + left + right; + const svgHeight = height + top + bottom; + + const svgView = { + width: svgWidth, + height: svgHeight, + x: 0, + y: 0, + }; useAxisEvents(disableAxisListener); return ( - - {title} - {desc} + {title && {title}} + {desc && {desc}} + {children} - + ); }); @@ -92,26 +85,12 @@ ChartsSurface.propTypes = { * @default false */ disableAxisListener: PropTypes.bool, - /** - * The height of the chart in px. - */ - height: PropTypes.number.isRequired, sx: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), PropTypes.func, PropTypes.object, ]), title: PropTypes.string, - viewBox: PropTypes.shape({ - height: PropTypes.number, - width: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number, - }), - /** - * The width of the chart in px. - */ - width: PropTypes.number.isRequired, } as any; export { ChartsSurface }; diff --git a/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx b/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx index 28b6f6f833e0..ed4f1a6608b4 100644 --- a/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx +++ b/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { SxProps, Theme } from '@mui/material/styles'; import useSlotProps from '@mui/utils/useSlotProps'; -import { AxisInteractionData } from '../context/InteractionProvider'; +import { AxisInteractionData } from '../internals/plugins/models'; import { useCartesianContext } from '../context/CartesianProvider'; import { ChartSeriesDefaultized, ChartSeriesType } from '../models/seriesType/config'; import { AxisDefaultized } from '../models/axis'; diff --git a/packages/x-charts/src/ChartsTooltip/ChartsItemTooltipContent.tsx b/packages/x-charts/src/ChartsTooltip/ChartsItemTooltipContent.tsx index 30140400a948..8d374db974f7 100644 --- a/packages/x-charts/src/ChartsTooltip/ChartsItemTooltipContent.tsx +++ b/packages/x-charts/src/ChartsTooltip/ChartsItemTooltipContent.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { SxProps, Theme } from '@mui/material/styles'; import useSlotProps from '@mui/utils/useSlotProps'; -import { ItemInteractionData } from '../context/InteractionProvider'; +import { ItemInteractionData } from '../internals/plugins/models'; import { ChartSeriesDefaultized, ChartSeriesType } from '../models/seriesType/config'; import { ChartsTooltipClasses } from './chartsTooltipClasses'; import { DefaultChartsItemTooltipContent } from './DefaultChartsItemTooltipContent'; diff --git a/packages/x-charts/src/ChartsTooltip/ChartsTooltip.tsx b/packages/x-charts/src/ChartsTooltip/ChartsTooltip.tsx index 560a83ccc9a4..544dac733866 100644 --- a/packages/x-charts/src/ChartsTooltip/ChartsTooltip.tsx +++ b/packages/x-charts/src/ChartsTooltip/ChartsTooltip.tsx @@ -7,17 +7,19 @@ import { styled, useThemeProps, SxProps, Theme } from '@mui/material/styles'; import Popper, { PopperProps as BasePopperProps } from '@mui/material/Popper'; import NoSsr from '@mui/material/NoSsr'; import useSlotProps from '@mui/utils/useSlotProps'; -import { - AxisInteractionData, - InteractionContext, - ItemInteractionData, -} from '../context/InteractionProvider'; import { useSvgRef } from '../hooks/useSvgRef'; import { getTooltipHasData, TriggerOptions, usePointerType } from './utils'; import { ChartSeriesType } from '../models/seriesType/config'; import { ChartsItemContentProps, ChartsItemTooltipContent } from './ChartsItemTooltipContent'; import { ChartsAxisContentProps, ChartsAxisTooltipContent } from './ChartsAxisTooltipContent'; import { ChartsTooltipClasses, getChartsTooltipUtilityClass } from './chartsTooltipClasses'; +import { + selectorChartsInteractionItem, + selectorChartsInteractionAxis, +} from '../context/InteractionSelectors'; +import { ItemInteractionData, AxisInteractionData } from '../internals/plugins/models'; +import { useSelector } from '../internals/useSelector'; +import { useStore } from '../internals/useStore'; export type PopperProps = BasePopperProps & { /** @@ -137,7 +139,9 @@ function ChartsTooltip(inProps: ChartsTooltipProps const positionRef = useLazyRef(() => ({ x: 0, y: 0 })); - const { item, axis } = React.useContext(InteractionContext); + const store = useStore(); + const item = useSelector(store, selectorChartsInteractionItem); + const axis = useSelector(store, selectorChartsInteractionAxis); const displayedData = trigger === 'item' ? item : axis; diff --git a/packages/x-charts/src/ChartsTooltip/useAxisTooltip.tsx b/packages/x-charts/src/ChartsTooltip/useAxisTooltip.tsx index 109665b90e5d..aff76dca6187 100644 --- a/packages/x-charts/src/ChartsTooltip/useAxisTooltip.tsx +++ b/packages/x-charts/src/ChartsTooltip/useAxisTooltip.tsx @@ -1,6 +1,5 @@ 'use client'; import * as React from 'react'; -import { AxisInteractionData, InteractionContext } from '../context/InteractionProvider'; import { useSeries } from '../hooks/useSeries'; import { useCartesianContext } from '../context/CartesianProvider'; import { ZAxisContext } from '../context/ZAxisContextProvider'; @@ -10,6 +9,15 @@ import { CartesianChartSeriesType, ChartsSeriesConfig } from '../models/seriesTy import { getLabel } from '../internals/getLabel'; import { isCartesianSeriesType } from '../internals/isCartesian'; import { utcFormatter } from './utils'; +import { + selectorChartsInteractionAxis, + selectorChartsInteractionXAxis, + selectorChartsInteractionYAxis, +} from '../context/InteractionSelectors'; +import { useXAxis, useYAxis } from '../hooks'; +import { useSelector } from '../internals/useSelector'; +import { useStore } from '../internals/useStore'; +import { AxisInteractionData } from '../internals/plugins/models'; export interface UseAxisTooltipReturnValue< SeriesT extends CartesianChartSeriesType = CartesianChartSeriesType, @@ -30,26 +38,35 @@ interface SeriesItem { } export function useAxisTooltip(): null | UseAxisTooltipReturnValue { - const { axis } = React.useContext(InteractionContext); + const defaultXAxis = useXAxis(); + const defaultYAxis = useYAxis(); + + const xAxisHasData = defaultXAxis.data !== undefined && defaultXAxis.data.length !== 0; + + const store = useStore(); + + // This line will be removed in v8 because it degrade perfs for no reason except avoiding breaking change. + const axis = useSelector(store, selectorChartsInteractionAxis); + const axisData = useSelector( + store, + xAxisHasData ? selectorChartsInteractionXAxis : selectorChartsInteractionYAxis, + ); + const series = useSeries(); - const { xAxis, yAxis, xAxisIds, yAxisIds } = useCartesianContext(); + const { xAxis, yAxis } = useCartesianContext(); + const { zAxis, zAxisIds } = React.useContext(ZAxisContext); const colorProcessors = useColorProcessor(); - // By default use the x-axis - const isXaxis = axis.x !== null && axis.x.index !== -1; - - const axisData = isXaxis ? axis.x && axis.x : axis.y && axis.y; - if (axisData === null) { return null; } const { index: dataIndex, value: axisValue } = axisData; - const USED_AXIS_ID = isXaxis ? xAxisIds[0] : yAxisIds[0]; - const usedAxis = isXaxis ? xAxis[USED_AXIS_ID] : yAxis[USED_AXIS_ID]; + const USED_AXIS_ID = xAxisHasData ? defaultXAxis.id : defaultYAxis.id; + const usedAxis = xAxisHasData ? defaultXAxis : defaultYAxis; const relevantSeries = Object.keys(series) .filter(isCartesianSeriesType) @@ -64,20 +81,20 @@ export function useAxisTooltip(): null | UseAxisTooltipReturnValue { const providedXAxisId = seriesToAdd.xAxisId; const providedYAxisId = seriesToAdd.yAxisId; - const axisKey = isXaxis ? providedXAxisId : providedYAxisId; + const axisKey = xAxisHasData ? providedXAxisId : providedYAxisId; // Test if the series uses the default axis if (axisKey === undefined || axisKey === USED_AXIS_ID) { - const xAxisId = providedXAxisId ?? xAxisIds[0]; - const yAxisId = providedYAxisId ?? yAxisIds[0]; - const zAxisId = (seriesToAdd as any).zAxisId ?? zAxisIds[0]; + const xAxisId = providedXAxisId ?? defaultXAxis.id; + const yAxisId = providedYAxisId ?? defaultYAxis.id; + const zAxisId = 'zAxisId' in seriesToAdd ? seriesToAdd.zAxisId : zAxisIds[0]; const color = colorProcessors[seriesType]?.( seriesToAdd, xAxis[xAxisId], yAxis[yAxisId], - zAxisId && zAxis[zAxisId], + zAxisId ? zAxis[zAxisId] : undefined, )(dataIndex) ?? ''; const value = seriesToAdd.data[dataIndex] ?? null; @@ -107,7 +124,7 @@ export function useAxisTooltip(): null | UseAxisTooltipReturnValue { const axisFormattedValue = axisFormatter(axisValue, { location: 'tooltip' }); return { - identifier: axis as AxisInteractionData, + identifier: axis, seriesItems: relevantSeries, axisValue, axisFormattedValue, diff --git a/packages/x-charts/src/ChartsTooltip/useItemTooltip.tsx b/packages/x-charts/src/ChartsTooltip/useItemTooltip.tsx index a3ab97c287b1..96023ae1b272 100644 --- a/packages/x-charts/src/ChartsTooltip/useItemTooltip.tsx +++ b/packages/x-charts/src/ChartsTooltip/useItemTooltip.tsx @@ -1,6 +1,5 @@ 'use client'; import * as React from 'react'; -import { InteractionContext, ItemInteractionData } from '../context/InteractionProvider'; import { useSeries } from '../hooks/useSeries'; import { useCartesianContext } from '../context/CartesianProvider'; import { ZAxisContext } from '../context/ZAxisContextProvider'; @@ -12,6 +11,10 @@ import { } from '../models/seriesType/config'; import { getLabel } from '../internals/getLabel'; import { CommonSeriesType } from '../models/seriesType/common'; +import { selectorChartsInteractionItem } from '../context/InteractionSelectors'; +import { useSelector } from '../internals/useSelector'; +import { useStore } from '../internals/useStore'; +import { ItemInteractionData } from '../internals/plugins/models'; export interface UseItemTooltipReturnValue { identifier: ItemInteractionData; @@ -22,7 +25,8 @@ export interface UseItemTooltipReturnValue { } export function useItemTooltip(): null | UseItemTooltipReturnValue { - const { item } = React.useContext(InteractionContext); + const stroe = useStore(); + const item = useSelector(stroe, selectorChartsInteractionItem); const series = useSeries(); const { xAxis, yAxis, xAxisIds, yAxisIds } = useCartesianContext(); diff --git a/packages/x-charts/src/ChartsTooltip/utils.tsx b/packages/x-charts/src/ChartsTooltip/utils.tsx index 227c7f58e24e..6964b5ef3e45 100644 --- a/packages/x-charts/src/ChartsTooltip/utils.tsx +++ b/packages/x-charts/src/ChartsTooltip/utils.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { AxisInteractionData, ItemInteractionData } from '../context/InteractionProvider'; +import { AxisInteractionData, ItemInteractionData } from '../internals/plugins/models'; import { ChartSeriesType } from '../models/seriesType/config'; import { useSvgRef } from '../hooks'; diff --git a/packages/x-charts/src/ChartsVoronoiHandler/ChartsVoronoiHandler.tsx b/packages/x-charts/src/ChartsVoronoiHandler/ChartsVoronoiHandler.tsx index ad1dea102fc6..f0169fb95025 100644 --- a/packages/x-charts/src/ChartsVoronoiHandler/ChartsVoronoiHandler.tsx +++ b/packages/x-charts/src/ChartsVoronoiHandler/ChartsVoronoiHandler.tsx @@ -3,9 +3,9 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { Delaunay } from '@mui/x-charts-vendor/d3-delaunay'; import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; -import { InteractionContext } from '../context/InteractionProvider'; import { useCartesianContext } from '../context/CartesianProvider'; import { getValueToPositionMapper } from '../hooks/useScale'; +import { useStore } from '../internals/useStore'; import { getSVGPoint } from '../internals/getSVGPoint'; import { ScatterItemIdentifier } from '../models'; import { SeriesId } from '../models/seriesType/common'; @@ -34,7 +34,7 @@ function ChartsVoronoiHandler(props: ChartsVoronoiHandlerProps) { const svgRef = useSvgRef(); const drawingArea = useDrawingArea(); const { xAxis, yAxis, xAxisIds, yAxisIds } = useCartesianContext(); - const { dispatch } = React.useContext(InteractionContext); + const store = useStore(); const { series, seriesOrder } = useScatterSeries() ?? {}; const voronoiRef = React.useRef>({}); @@ -47,11 +47,18 @@ function ChartsVoronoiHandler(props: ChartsVoronoiHandlerProps) { const defaultYAxisId = yAxisIds[0]; useEnhancedEffect(() => { - dispatch({ type: 'updateVoronoiUsage', useVoronoiInteraction: true }); + store.update((prev) => ({ + ...prev, + interaction: { ...prev.interaction, useVoronoiInteraction: true }, + })); + return () => { - dispatch({ type: 'updateVoronoiUsage', useVoronoiInteraction: false }); + store.update((prev) => ({ + ...prev, + interaction: { ...prev.interaction, useVoronoiInteraction: false }, + })); }; - }, [dispatch]); + }, [store]); useEnhancedEffect(() => { // This effect generate and store the Delaunay object that's used to map coordinate to closest point. @@ -153,7 +160,10 @@ function ChartsVoronoiHandler(props: ChartsVoronoiHandlerProps) { } const handleMouseLeave = () => { - dispatch({ type: 'exitChart' }); + store.update((prev) => ({ + ...prev, + interaction: { ...prev.interaction, axis: { x: null, y: null }, item: null }, + })); clearHighlighted(); }; @@ -161,19 +171,29 @@ function ChartsVoronoiHandler(props: ChartsVoronoiHandlerProps) { const closestPoint = getClosestPoint(event); if (closestPoint === 'outside-chart') { - dispatch({ type: 'exitChart' }); + store.update((prev) => ({ + ...prev, + interaction: { ...prev.interaction, axis: { x: null, y: null }, item: null }, + })); clearHighlighted(); return; } if (closestPoint === 'outside-voronoi-max-radius' || closestPoint === 'no-point-found') { - dispatch({ type: 'leaveItem', data: { type: 'scatter' } }); + store.update((prev) => ({ + ...prev, + interaction: { ...prev.interaction, item: null }, + })); clearHighlighted(); return; } const { seriesId, dataIndex } = closestPoint; - dispatch({ type: 'enterItem', data: { type: 'scatter', seriesId, dataIndex } }); + store.update((prev) => ({ + ...prev, + interaction: { ...prev.interaction, item: { type: 'scatter', seriesId, dataIndex } }, + })); + setHighlighted({ seriesId, dataIndex, @@ -205,7 +225,6 @@ function ChartsVoronoiHandler(props: ChartsVoronoiHandlerProps) { }; }, [ svgRef, - dispatch, yAxis, xAxis, voronoiMaxRadius, @@ -213,6 +232,7 @@ function ChartsVoronoiHandler(props: ChartsVoronoiHandlerProps) { setHighlighted, clearHighlighted, drawingArea, + store, ]); // eslint-disable-next-line react/jsx-no-useless-fragment diff --git a/packages/x-charts/src/Gauge/Gauge.tsx b/packages/x-charts/src/Gauge/Gauge.tsx index 01a4fd2827e1..c169d1251f9e 100644 --- a/packages/x-charts/src/Gauge/Gauge.tsx +++ b/packages/x-charts/src/Gauge/Gauge.tsx @@ -135,12 +135,6 @@ Gauge.propTypes = { * @default 0 */ valueMin: PropTypes.number, - viewBox: PropTypes.shape({ - height: PropTypes.number, - width: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number, - }), /** * The width of the chart in px. If not defined, it takes the width of the parent element. */ diff --git a/packages/x-charts/src/Gauge/GaugeContainer.tsx b/packages/x-charts/src/Gauge/GaugeContainer.tsx index e25066c0e618..6126367e056c 100644 --- a/packages/x-charts/src/Gauge/GaugeContainer.tsx +++ b/packages/x-charts/src/Gauge/GaugeContainer.tsx @@ -2,16 +2,15 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { styled } from '@mui/material/styles'; -import useForkRef from '@mui/utils/useForkRef'; -import { useChartContainerDimensions } from '../ChartContainer/useChartContainerDimensions'; import { ChartsSurface, ChartsSurfaceProps } from '../ChartsSurface'; import { DrawingAreaProvider, DrawingAreaProviderProps } from '../context/DrawingAreaProvider'; import { GaugeProvider, GaugeProviderProps } from './GaugeProvider'; +import { SizeProvider, useSize } from '../context/SizeProvider'; import { SvgRefProvider } from '../context/SvgRefProvider'; export interface GaugeContainerProps extends Omit, - Omit, + Pick, Omit { /** * The width of the chart in px. If not defined, it takes the width of the parent element. @@ -24,7 +23,7 @@ export interface GaugeContainerProps children?: React.ReactNode; } -const ResizableContainer = styled('div', { +const ResizableContainerRoot = styled('div', { name: 'MuiGauge', slot: 'Container', })<{ ownerState: Pick }>(({ ownerState, theme }) => ({ @@ -46,6 +45,20 @@ const ResizableContainer = styled('div', { }, })); +function ResizableContainer(props: any) { + const { inHeight, inWidth, hasIntrinsicSize, containerRef } = useSize(); + + return ( + + {hasIntrinsicSize && props.children} + + ); +} + const GaugeContainer = React.forwardRef(function GaugeContainer( props: GaugeContainerProps, ref: React.Ref, @@ -69,56 +82,44 @@ const GaugeContainer = React.forwardRef(function GaugeContainer( children, ...other } = props; - const { containerRef, width, height } = useChartContainerDimensions(inWidth, inHeight); - - const svgRef = React.useRef(null); - const chartSurfaceRef = useForkRef(ref, svgRef); return ( - - {width && height ? ( - - + + + - - - - - ) : null} - + + + + + ); }); @@ -215,12 +216,6 @@ GaugeContainer.propTypes = { * @default 0 */ valueMin: PropTypes.number, - viewBox: PropTypes.shape({ - height: PropTypes.number, - width: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number, - }), /** * The width of the chart in px. If not defined, it takes the width of the parent element. */ diff --git a/packages/x-charts/src/LineChart/AnimatedArea.tsx b/packages/x-charts/src/LineChart/AnimatedArea.tsx index c88c8be9d585..4d1d3cbe7cbd 100644 --- a/packages/x-charts/src/LineChart/AnimatedArea.tsx +++ b/packages/x-charts/src/LineChart/AnimatedArea.tsx @@ -1,27 +1,11 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import { styled } from '@mui/material/styles'; import { animated, useTransition } from '@react-spring/web'; -import { color as d3Color } from '@mui/x-charts-vendor/d3-color'; import type { AreaElementOwnerState } from './AreaElement'; import { useStringInterpolator } from '../internals/useStringInterpolator'; import { AppearingMask } from './AppearingMask'; -export const AreaElementPath = styled(animated.path, { - name: 'MuiAreaElement', - slot: 'Root', - overridesResolver: (_, styles) => styles.root, -})<{ ownerState: AreaElementOwnerState }>(({ ownerState }) => ({ - stroke: 'none', - fill: - (ownerState.gradientId && `url(#${ownerState.gradientId})`) || - (ownerState.isHighlighted && d3Color(ownerState.color)!.brighter(1).formatHex()) || - d3Color(ownerState.color)!.brighter(0.5).formatHex(), - transition: 'opacity 0.2s ease-in, fill 0.2s ease-in', - opacity: ownerState.isFaded ? 0.3 : 1, -})); - export interface AnimatedAreaProps extends React.ComponentPropsWithoutRef<'path'> { ownerState: AreaElementOwnerState; d: string; @@ -58,7 +42,21 @@ function AnimatedArea(props: AnimatedAreaProps) { return ( {transitionChange((style, interpolator) => ( - + ))} ); diff --git a/packages/x-charts/src/LineChart/AnimatedLine.tsx b/packages/x-charts/src/LineChart/AnimatedLine.tsx index 6c2eeb435102..8564435da881 100644 --- a/packages/x-charts/src/LineChart/AnimatedLine.tsx +++ b/packages/x-charts/src/LineChart/AnimatedLine.tsx @@ -2,28 +2,10 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { animated, useTransition } from '@react-spring/web'; -import { color as d3Color } from '@mui/x-charts-vendor/d3-color'; -import { styled } from '@mui/material/styles'; import type { LineElementOwnerState } from './LineElement'; import { useStringInterpolator } from '../internals/useStringInterpolator'; import { AppearingMask } from './AppearingMask'; -export const LineElementPath = styled(animated.path, { - name: 'MuiLineElement', - slot: 'Root', - overridesResolver: (_, styles) => styles.root, -})<{ ownerState: LineElementOwnerState }>(({ ownerState }) => ({ - strokeWidth: 2, - strokeLinejoin: 'round', - fill: 'none', - stroke: - (ownerState.gradientId && `url(#${ownerState.gradientId})`) || - (ownerState.isHighlighted && d3Color(ownerState.color)!.brighter(0.5).formatHex()) || - ownerState.color, - transition: 'opacity 0.2s ease-in, stroke 0.2s ease-in', - opacity: ownerState.isFaded ? 0.3 : 1, -})); - export interface AnimatedLineProps extends React.ComponentPropsWithoutRef<'path'> { ownerState: LineElementOwnerState; d: string; @@ -60,7 +42,16 @@ function AnimatedLine(props: AnimatedLineProps) { return ( {transitionChange((style, interpolator) => ( - + ))} ); diff --git a/packages/x-charts/src/LineChart/AreaPlot.tsx b/packages/x-charts/src/LineChart/AreaPlot.tsx index 7db6cbaf1c82..2a4f2d6f3280 100644 --- a/packages/x-charts/src/LineChart/AreaPlot.tsx +++ b/packages/x-charts/src/LineChart/AreaPlot.tsx @@ -1,10 +1,12 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; import { area as d3Area } from '@mui/x-charts-vendor/d3-shape'; import { useCartesianContext } from '../context/CartesianProvider'; import { AreaElement, + areaElementClasses, AreaElementProps, AreaElementSlotProps, AreaElementSlots, @@ -36,6 +38,16 @@ export interface AreaPlotProps ) => void; } +const AreaPlotRoot = styled('g', { + name: 'MuiAreaPlot', + slot: 'Root', + overridesResolver: (_, styles) => styles.root, +})({ + [`& .${areaElementClasses.root}`]: { + transition: 'opacity 0.2s ease-in, fill 0.2s ease-in', + }, +}); + const useAggregatedData = () => { const seriesData = useLineSeries(); const axisData = useCartesianContext(); @@ -154,7 +166,7 @@ function AreaPlot(props: AreaPlotProps) { const completedData = useAggregatedData(); return ( - + {completedData.map( ({ d, seriesId, color, area, gradientUsed }) => !!area && ( @@ -171,7 +183,7 @@ function AreaPlot(props: AreaPlotProps) { /> ), )} - + ); } diff --git a/packages/x-charts/src/LineChart/CircleMarkElement.tsx b/packages/x-charts/src/LineChart/CircleMarkElement.tsx index 584c15bab4d8..ddc1608cc44c 100644 --- a/packages/x-charts/src/LineChart/CircleMarkElement.tsx +++ b/packages/x-charts/src/LineChart/CircleMarkElement.tsx @@ -4,10 +4,12 @@ import PropTypes from 'prop-types'; import { useTheme } from '@mui/material/styles'; import { warnOnce } from '@mui/x-internals/warning'; import { animated, useSpring } from '@react-spring/web'; -import { InteractionContext } from '../context/InteractionProvider'; import { useInteractionItemProps } from '../hooks/useInteractionItemProps'; import { useItemHighlighted } from '../context'; import { MarkElementOwnerState, useUtilityClasses } from './markElementClasses'; +import { useSelector } from '../internals/useSelector'; +import { selectorChartsInteractionXAxis } from '../context/InteractionSelectors'; +import { useStore } from '../internals/useStore'; export type CircleMarkElementProps = Omit & Omit, 'ref' | 'id'> & { @@ -66,13 +68,14 @@ function CircleMarkElement(props: CircleMarkElementProps) { const { isFaded, isHighlighted } = useItemHighlighted({ seriesId: id, }); - const { axis } = React.useContext(InteractionContext); + const store = useStore(); + const xAxisIdentifier = useSelector(store, selectorChartsInteractionXAxis); const position = useSpring({ to: { x, y }, immediate: skipAnimation }); const ownerState = { id, classes: innerClasses, - isHighlighted: axis.x?.index === dataIndex || isHighlighted, + isHighlighted: xAxisIdentifier?.index === dataIndex || isHighlighted, isFaded, color, }; diff --git a/packages/x-charts/src/LineChart/LineChart.tsx b/packages/x-charts/src/LineChart/LineChart.tsx index 3c1ae7202add..8da4a8e59f1d 100644 --- a/packages/x-charts/src/LineChart/LineChart.tsx +++ b/packages/x-charts/src/LineChart/LineChart.tsx @@ -78,7 +78,7 @@ export interface LineChartProps grid?: Pick; /** * The configuration of axes highlight. - * @see See {@link https://mui.com/x/react-charts/highlighting highlighting docs} for more details. + * @see See {@link https://mui.com/x/react-charts/highlighting/ highlighting docs} for more details. * @default { x: 'line' } */ axisHighlight?: ChartsAxisHighlightProps; @@ -187,7 +187,7 @@ LineChart.propTypes = { // ---------------------------------------------------------------------- /** * The configuration of axes highlight. - * @see See {@link https://mui.com/x/react-charts/highlighting highlighting docs} for more details. + * @see See {@link https://mui.com/x/react-charts/highlighting/ highlighting docs} for more details. * @default { x: 'line' } */ axisHighlight: PropTypes.shape({ @@ -357,12 +357,6 @@ LineChart.propTypes = { * @default null */ topAxis: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), - viewBox: PropTypes.shape({ - height: PropTypes.number, - width: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number, - }), /** * The width of the chart in px. If not defined, it takes the width of the parent element. */ diff --git a/packages/x-charts/src/LineChart/LineHighlightPlot.tsx b/packages/x-charts/src/LineChart/LineHighlightPlot.tsx index a5911052eb10..7c6e30437bd0 100644 --- a/packages/x-charts/src/LineChart/LineHighlightPlot.tsx +++ b/packages/x-charts/src/LineChart/LineHighlightPlot.tsx @@ -5,11 +5,13 @@ import { SlotComponentPropsFromProps } from '@mui/x-internals/types'; import { useCartesianContext } from '../context/CartesianProvider'; import { LineHighlightElement, LineHighlightElementProps } from './LineHighlightElement'; import { getValueToPositionMapper } from '../hooks/useScale'; -import { InteractionContext } from '../context/InteractionProvider'; import { DEFAULT_X_AXIS_KEY } from '../constants'; import getColor from './getColor'; import { useLineSeries } from '../hooks/useSeries'; import { useDrawingArea } from '../hooks/useDrawingArea'; +import { selectorChartsInteractionXAxis } from '../context/InteractionSelectors'; +import { useSelector } from '../internals/useSelector'; +import { useStore } from '../internals/useStore'; export interface LineHighlightPlotSlots { lineHighlight?: React.JSXElementConstructor; @@ -48,9 +50,11 @@ function LineHighlightPlot(props: LineHighlightPlotProps) { const seriesData = useLineSeries(); const axisData = useCartesianContext(); const drawingArea = useDrawingArea(); - const { axis } = React.useContext(InteractionContext); + const store = useStore(); + const xAxisIdentifier = useSelector(store, selectorChartsInteractionXAxis); + + const highlightedIndex = xAxisIdentifier?.index; - const highlightedIndex = axis.x?.index; if (highlightedIndex === undefined) { return null; } diff --git a/packages/x-charts/src/LineChart/LinePlot.tsx b/packages/x-charts/src/LineChart/LinePlot.tsx index 75ada74aa047..b0986bee8d67 100644 --- a/packages/x-charts/src/LineChart/LinePlot.tsx +++ b/packages/x-charts/src/LineChart/LinePlot.tsx @@ -1,10 +1,12 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; import { line as d3Line } from '@mui/x-charts-vendor/d3-shape'; import { useCartesianContext } from '../context/CartesianProvider'; import { LineElement, + lineElementClasses, LineElementProps, LineElementSlotProps, LineElementSlots, @@ -36,6 +38,16 @@ export interface LinePlotProps ) => void; } +const LinePlotRoot = styled('g', { + name: 'MuiAreaPlot', + slot: 'Root', + overridesResolver: (_, styles) => styles.root, +})({ + [`& .${lineElementClasses.root}`]: { + transition: 'opacity 0.2s ease-in, fill 0.2s ease-in', + }, +}); + const useAggregatedData = () => { const seriesData = useLineSeries(); const axisData = useCartesianContext(); @@ -131,7 +143,7 @@ function LinePlot(props: LinePlotProps) { const getGradientId = useChartGradient(); const completedData = useAggregatedData(); return ( - + {completedData.map(({ d, seriesId, color, gradientUsed }) => { return ( ); })} - + ); } diff --git a/packages/x-charts/src/LineChart/MarkElement.tsx b/packages/x-charts/src/LineChart/MarkElement.tsx index 93a77ca25e25..a3d9d6f93857 100644 --- a/packages/x-charts/src/LineChart/MarkElement.tsx +++ b/packages/x-charts/src/LineChart/MarkElement.tsx @@ -5,10 +5,12 @@ import { styled } from '@mui/material/styles'; import { symbol as d3Symbol, symbolsFill as d3SymbolsFill } from '@mui/x-charts-vendor/d3-shape'; import { animated, to, useSpring } from '@react-spring/web'; import { getSymbol } from '../internals/getSymbol'; -import { InteractionContext } from '../context/InteractionProvider'; import { useInteractionItemProps } from '../hooks/useInteractionItemProps'; import { useItemHighlighted } from '../context'; import { MarkElementOwnerState, useUtilityClasses } from './markElementClasses'; +import { selectorChartsInteractionXAxis } from '../context/InteractionSelectors'; +import { useSelector } from '../internals/useSelector'; +import { useStore } from '../internals/useStore'; const MarkElementPath = styled(animated.path, { name: 'MuiMarkElement', @@ -65,13 +67,15 @@ function MarkElement(props: MarkElementProps) { const { isFaded, isHighlighted } = useItemHighlighted({ seriesId: id, }); - const { axis } = React.useContext(InteractionContext); + + const store = useStore(); + const xAxisIdentifier = useSelector(store, selectorChartsInteractionXAxis); const position = useSpring({ to: { x, y }, immediate: skipAnimation }); const ownerState = { id, classes: innerClasses, - isHighlighted: axis.x?.index === dataIndex || isHighlighted, + isHighlighted: xAxisIdentifier?.index === dataIndex || isHighlighted, isFaded, color, }; diff --git a/packages/x-charts/src/PieChart/PieChart.tsx b/packages/x-charts/src/PieChart/PieChart.tsx index 3e712091a1c5..28c41b7bfd44 100644 --- a/packages/x-charts/src/PieChart/PieChart.tsx +++ b/packages/x-charts/src/PieChart/PieChart.tsx @@ -259,12 +259,6 @@ PieChart.propTypes = { slots: PropTypes.object, trigger: PropTypes.oneOf(['axis', 'item', 'none']), }), - viewBox: PropTypes.shape({ - height: PropTypes.number, - width: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number, - }), /** * The width of the chart in px. If not defined, it takes the width of the parent element. */ diff --git a/packages/x-charts/src/ScatterChart/Scatter.tsx b/packages/x-charts/src/ScatterChart/Scatter.tsx index 74aea129dc00..b6a95e703587 100644 --- a/packages/x-charts/src/ScatterChart/Scatter.tsx +++ b/packages/x-charts/src/ScatterChart/Scatter.tsx @@ -8,10 +8,12 @@ import { } from '../models/seriesType/scatter'; import { getValueToPositionMapper } from '../hooks/useScale'; import { useInteractionItemProps } from '../hooks/useInteractionItemProps'; -import { InteractionContext } from '../context/InteractionProvider'; import { D3Scale } from '../models/axis'; import { useHighlighted } from '../context'; import { useDrawingArea } from '../hooks/useDrawingArea'; +import { selectorChartsInteractionIsVoronoiEnabled } from '../context/InteractionSelectors'; +import { useSelector } from '../internals/useSelector'; +import { useStore } from '../internals/useStore'; export interface ScatterProps { series: DefaultizedScatterSeriesType; @@ -46,9 +48,10 @@ function Scatter(props: ScatterProps) { const drawingArea = useDrawingArea(); - const { useVoronoiInteraction } = React.useContext(InteractionContext); + const store = useStore(); + const isVoronoiEnabled = useSelector(store, selectorChartsInteractionIsVoronoiEnabled); - const skipInteractionHandlers = useVoronoiInteraction || series.disableHover; + const skipInteractionHandlers = isVoronoiEnabled || series.disableHover; const getInteractionItemProps = useInteractionItemProps(skipInteractionHandlers); const { isFaded, isHighlighted } = useHighlighted(); diff --git a/packages/x-charts/src/ScatterChart/ScatterChart.tsx b/packages/x-charts/src/ScatterChart/ScatterChart.tsx index 6ccdfd6d69b6..951ed860ef70 100644 --- a/packages/x-charts/src/ScatterChart/ScatterChart.tsx +++ b/packages/x-charts/src/ScatterChart/ScatterChart.tsx @@ -67,7 +67,7 @@ export interface ScatterChartProps tooltip?: ChartsTooltipProps<'scatter'>; /** * The configuration of axes highlight. - * @see See {@link https://mui.com/x/react-charts/highlighting highlighting docs} for more details. + * @see See {@link https://mui.com/x/react-charts/highlighting/ highlighting docs} for more details. * @default { x: 'none', y: 'none' } */ axisHighlight?: ChartsAxisHighlightProps; @@ -157,7 +157,7 @@ ScatterChart.propTypes = { // ---------------------------------------------------------------------- /** * The configuration of axes highlight. - * @see See {@link https://mui.com/x/react-charts/highlighting highlighting docs} for more details. + * @see See {@link https://mui.com/x/react-charts/highlighting/ highlighting docs} for more details. * @default { x: 'none', y: 'none' } */ axisHighlight: PropTypes.shape({ @@ -311,12 +311,6 @@ ScatterChart.propTypes = { * @default null */ topAxis: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), - viewBox: PropTypes.shape({ - height: PropTypes.number, - width: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number, - }), /** * Defines the maximal distance between a scatter point and the pointer that triggers the interaction. * If `undefined`, the radius is assumed to be infinite. diff --git a/packages/x-charts/src/SparkLineChart/SparkLineChart.tsx b/packages/x-charts/src/SparkLineChart/SparkLineChart.tsx index fb75f3df563d..c08f0a2942b4 100644 --- a/packages/x-charts/src/SparkLineChart/SparkLineChart.tsx +++ b/packages/x-charts/src/SparkLineChart/SparkLineChart.tsx @@ -375,12 +375,6 @@ SparkLineChart.propTypes = { * @default (value: number | null) => (value === null ? '' : value.toString()) */ valueFormatter: PropTypes.func, - viewBox: PropTypes.shape({ - height: PropTypes.number, - width: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number, - }), /** * The width of the chart in px. If not defined, it takes the width of the parent element. */ diff --git a/packages/x-charts/src/context/ChartDataProvider/ChartDataProvider.test.tsx b/packages/x-charts/src/context/ChartDataProvider/ChartDataProvider.test.tsx index ac81b3cfc7d0..ca0355c21029 100644 --- a/packages/x-charts/src/context/ChartDataProvider/ChartDataProvider.test.tsx +++ b/packages/x-charts/src/context/ChartDataProvider/ChartDataProvider.test.tsx @@ -13,6 +13,10 @@ describe('', () => { testComponentPropWith: 'div', refInstanceof: window.SVGSVGElement, skip: [ + 'mergeClassName', + 'propsSpread', + 'rootClass', + 'refForwarding', 'componentProp', 'componentsProp', 'slotPropsProp', diff --git a/packages/x-charts/src/context/ChartDataProvider/ChartDataProvider.tsx b/packages/x-charts/src/context/ChartDataProvider/ChartDataProvider.tsx index 38e2727388e0..2f4e5e5d1e34 100644 --- a/packages/x-charts/src/context/ChartDataProvider/ChartDataProvider.tsx +++ b/packages/x-charts/src/context/ChartDataProvider/ChartDataProvider.tsx @@ -5,25 +5,20 @@ import { MakeOptional } from '@mui/x-internals/types'; import { DrawingAreaProvider, DrawingAreaProviderProps } from '../DrawingAreaProvider'; import { SeriesProvider, SeriesProviderProps } from '../SeriesProvider'; import { InteractionProvider } from '../InteractionProvider'; -import { ChartsSurface, ChartsSurfaceProps } from '../../ChartsSurface'; import { CartesianProvider, CartesianProviderProps } from '../CartesianProvider'; -import { ChartsAxesGradients } from '../../internals/components/ChartsAxesGradients'; -import { - HighlightedProvider, - HighlightedProviderProps, - ZAxisContextProvider, - ZAxisContextProviderProps, -} from '..'; import { PluginProvider, PluginProviderProps } from '../PluginProvider'; import { useChartDataProviderProps } from './useChartDataProviderProps'; import { AxisConfig, ChartsXAxisProps, ChartsYAxisProps, ScaleName } from '../../models/axis'; import { AnimationProvider, AnimationProviderProps } from '../AnimationProvider'; +import { ZAxisContextProvider, ZAxisContextProviderProps } from '../ZAxisContextProvider'; +import { HighlightedProvider, HighlightedProviderProps } from '../HighlightedProvider'; +import { SizeProvider, SizeProviderProps } from '../SizeProvider'; import { SvgRefProvider } from '../SvgRefProvider'; export type ChartDataProviderProps = Omit< - ChartsSurfaceProps & + SizeProviderProps & Omit & - Omit & + Pick & Pick & ZAxisContextProviderProps & HighlightedProviderProps & @@ -46,26 +41,22 @@ export type ChartDataProviderProps = Omit< children?: React.ReactNode; }; -const ChartDataProvider = React.forwardRef(function ChartDataProvider( - props: ChartDataProviderProps, - ref: React.Ref, -) { +function ChartDataProvider(props: ChartDataProviderProps) { const { children, - drawingProviderProps, + drawingAreaProviderProps, seriesProviderProps, cartesianProviderProps, zAxisContextProps, highlightedProviderProps, - chartsSurfaceProps, pluginProviderProps, animationProviderProps, - svgRefProviderProps, - } = useChartDataProviderProps(props, ref); + sizeProviderProps, + } = useChartDataProviderProps(props); return ( - - + + @@ -73,10 +64,7 @@ const ChartDataProvider = React.forwardRef(function ChartDataProvider( - - - {children} - + {children} @@ -84,10 +72,10 @@ const ChartDataProvider = React.forwardRef(function ChartDataProvider( - - + + ); -}); +} ChartDataProvider.propTypes = { // ----------------------------- Warning -------------------------------- @@ -95,7 +83,6 @@ ChartDataProvider.propTypes = { // | To update them edit the TypeScript types and run "pnpm proptypes" | // ---------------------------------------------------------------------- children: PropTypes.node, - className: PropTypes.string, /** * Color palette used to colorize multiple series. * @default blueberryTwilightPalette @@ -105,17 +92,10 @@ ChartDataProvider.propTypes = { * An array of objects that can be used to populate series and axes data using their `dataKey` property. */ dataset: PropTypes.arrayOf(PropTypes.object), - desc: PropTypes.string, /** - * If `true`, the charts will not listen to the mouse move event. - * It might break interactive features, but will improve performance. - * @default false - */ - disableAxisListener: PropTypes.bool, - /** - * The height of the chart in px. + * The height of the chart in px. If not defined, it takes the height of the parent element. */ - height: PropTypes.number.isRequired, + height: PropTypes.number, /** * The item currently highlighted. Turns highlighting into a controlled prop. */ @@ -146,6 +126,16 @@ ChartDataProvider.propTypes = { * If not provided, the container supports line, bar, scatter and pie charts. */ plugins: PropTypes.arrayOf(PropTypes.object), + /** + * The chart will try to wait for the parent container to resolve its size + * before it renders for the first time. + * + * This can be useful in some scenarios where the chart appear to grow after + * the first render, like when used inside a grid. + * + * @default false + */ + resolveSizeBeforeRender: PropTypes.bool, /** * The array of series to display. * Each type of series has its own specificity. @@ -157,22 +147,10 @@ ChartDataProvider.propTypes = { * If unset or `false`, the animations respects the user's `prefers-reduced-motion` setting. */ skipAnimation: PropTypes.bool, - sx: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), - PropTypes.func, - PropTypes.object, - ]), - title: PropTypes.string, - viewBox: PropTypes.shape({ - height: PropTypes.number, - width: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number, - }), /** - * The width of the chart in px. + * The width of the chart in px. If not defined, it takes the width of the parent element. */ - width: PropTypes.number.isRequired, + width: PropTypes.number, /** * The configuration of the x-axes. * If not provided, a default axis config is used. diff --git a/packages/x-charts/src/context/ChartDataProvider/useChartDataProviderProps.ts b/packages/x-charts/src/context/ChartDataProvider/useChartDataProviderProps.ts index fa0fdb4d17d0..a3c77fe7801f 100644 --- a/packages/x-charts/src/context/ChartDataProvider/useChartDataProviderProps.ts +++ b/packages/x-charts/src/context/ChartDataProvider/useChartDataProviderProps.ts @@ -1,22 +1,16 @@ 'use client'; -import * as React from 'react'; -import useForkRef from '@mui/utils/useForkRef'; import type { DrawingAreaProviderProps } from '../DrawingAreaProvider'; import type { CartesianProviderProps } from '../CartesianProvider'; import type { SeriesProviderProps } from '../SeriesProvider'; import type { ZAxisContextProviderProps } from '../ZAxisContextProvider'; import type { ChartDataProviderProps } from './ChartDataProvider'; -import { HighlightedProviderProps } from '..'; -import { ChartsSurfaceProps } from '../../ChartsSurface'; +import { HighlightedProviderProps } from '../HighlightedProvider'; import { useDefaultizeAxis } from './useDefaultizeAxis'; import { PluginProviderProps } from '../PluginProvider'; import { AnimationProviderProps } from '../AnimationProvider'; -import { SvgRefProviderProps } from '../SvgRefProvider'; +import { SizeProviderProps } from '../SizeProvider'; -export const useChartDataProviderProps = ( - props: ChartDataProviderProps, - ref: React.Ref, -) => { +export const useChartDataProviderProps = (props: ChartDataProviderProps) => { const { width, height, @@ -27,29 +21,17 @@ export const useChartDataProviderProps = ( zAxis, colors, dataset, - sx, - title, - desc, - disableAxisListener, highlightedItem, onHighlightChange, plugins, children, skipAnimation, - ...other + resolveSizeBeforeRender, } = props; - const svgRef = React.useRef(null); - const chartSurfaceRef = useForkRef(ref, svgRef); const [defaultizedXAxis, defaultizedYAxis] = useDefaultizeAxis(xAxis, yAxis, dataset); - const svgRefProviderProps: Omit = { - svgRef, - }; - - const drawingProviderProps: Omit = { - width, - height, + const drawingAreaProviderProps: Omit = { margin, }; @@ -83,29 +65,23 @@ export const useChartDataProviderProps = ( onHighlightChange, }; - const chartsSurfaceProps: ChartsSurfaceProps & { ref: any } = { - ...other, + const sizeProviderProps: Omit = { width, height, - ref: chartSurfaceRef, - sx, - title, - desc, - disableAxisListener, + resolveSizeBeforeRender, }; return { children, - drawingProviderProps, + drawingAreaProviderProps, seriesProviderProps, cartesianProviderProps, zAxisContextProps, highlightedProviderProps, - chartsSurfaceProps, pluginProviderProps, animationProviderProps, - svgRefProviderProps, xAxis: defaultizedXAxis, yAxis: defaultizedYAxis, + sizeProviderProps, }; }; diff --git a/packages/x-charts/src/context/DrawingAreaProvider/DrawingArea.types.ts b/packages/x-charts/src/context/DrawingAreaProvider/DrawingArea.types.ts index 98469323d98c..58ab5b5a7d3f 100644 --- a/packages/x-charts/src/context/DrawingAreaProvider/DrawingArea.types.ts +++ b/packages/x-charts/src/context/DrawingAreaProvider/DrawingArea.types.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { LayoutConfig } from '../../models'; -export interface DrawingAreaProviderProps extends LayoutConfig { +export interface DrawingAreaProviderProps extends Pick { children: React.ReactNode; } diff --git a/packages/x-charts/src/context/DrawingAreaProvider/DrawingAreaProvider.tsx b/packages/x-charts/src/context/DrawingAreaProvider/DrawingAreaProvider.tsx index 6357c5125929..1735812357df 100644 --- a/packages/x-charts/src/context/DrawingAreaProvider/DrawingAreaProvider.tsx +++ b/packages/x-charts/src/context/DrawingAreaProvider/DrawingAreaProvider.tsx @@ -4,9 +4,11 @@ import useId from '@mui/utils/useId'; import useChartDimensions from '../../hooks/useChartDimensions'; import { DrawingAreaProviderProps, DrawingAreaState } from './DrawingArea.types'; import { DrawingAreaContext } from './DrawingAreaContext'; +import { useSize } from '../SizeProvider'; export function DrawingAreaProvider(props: DrawingAreaProviderProps) { - const { width, height, margin, children } = props; + const { margin, children } = props; + const { width, height } = useSize(); const drawingArea = useChartDimensions(width, height, margin); const chartId = useId(); diff --git a/packages/x-charts/src/context/InteractionProvider.tsx b/packages/x-charts/src/context/InteractionProvider.tsx index 80c4658bfc1f..f31eac1c155d 100644 --- a/packages/x-charts/src/context/InteractionProvider.tsx +++ b/packages/x-charts/src/context/InteractionProvider.tsx @@ -1,132 +1,19 @@ 'use client'; import * as React from 'react'; -import { ChartItemIdentifier, ChartSeriesType } from '../models/seriesType/config'; +import { useCharts } from '../internals/useCharts'; +import { ChartStore } from '../internals/plugins/utils/ChartStore'; -export interface InteractionProviderProps { - children: React.ReactNode; -} - -export type ItemInteractionData = ChartItemIdentifier; - -export type AxisInteractionData = { - x: null | { - value: number | Date | string; - // Set to -1 if no index. - index: number; - }; - y: null | { - value: number | Date | string; - // Set to -1 if no index. - index: number; - }; -}; - -type InteractionActions = - | { - type: 'enterItem'; - data: ItemInteractionData; - } - | { - type: 'leaveItem'; - data: Partial>; - } - | { - type: 'exitChart'; - } - | { - type: 'updateVoronoiUsage'; - useVoronoiInteraction: boolean; - } - | { - type: 'updateAxis'; - data: AxisInteractionData; - }; - -type InteractionState = { - /** - * The item currently interacting. - */ - item: null | ItemInteractionData; - /** - * The x- and y-axes currently interacting. - */ - axis: AxisInteractionData; - /** - * Set to `true` when `VoronoiHandler` is active. - * Used to prevent collision with mouseEnter events. - */ - useVoronoiInteraction: boolean; - dispatch: React.Dispatch; -}; - -export const InteractionContext = React.createContext({ - item: null, - axis: { x: null, y: null }, - useVoronoiInteraction: false, - dispatch: () => null, -}); +export const ChartsContext = React.createContext<{ store: ChartStore } | null>(null); if (process.env.NODE_ENV !== 'production') { - InteractionContext.displayName = 'InteractionContext'; + ChartsContext.displayName = 'ChartsContext'; } -const dataReducer: React.Reducer, InteractionActions> = ( - prevState, - action, -) => { - switch (action.type) { - case 'enterItem': - return { ...prevState, item: action.data }; - - case 'exitChart': - if (prevState.item === null && prevState.axis.x === null && prevState.axis.y === null) { - return prevState; - } - return { ...prevState, axis: { x: null, y: null }, item: null }; - - case 'updateVoronoiUsage': - return { ...prevState, useVoronoiInteraction: action.useVoronoiInteraction }; - - case 'leaveItem': - if ( - prevState.item === null || - (Object.keys(action.data) as (keyof ItemInteractionData)[]).some( - (key) => action.data[key] !== prevState.item![key], - ) - ) { - // The item is already something else - return prevState; - } - return { ...prevState, item: null }; - - case 'updateAxis': - if (action.data.x === prevState.axis.x && action.data.y === prevState.axis.y) { - return prevState; - } - return { ...prevState, axis: action.data }; - - default: - return prevState; - } -}; - -function InteractionProvider(props: InteractionProviderProps) { +function InteractionProvider(props: React.PropsWithChildren) { const { children } = props; - const [data, dispatch] = React.useReducer(dataReducer, { - item: null, - axis: { x: null, y: null }, - useVoronoiInteraction: false, - }); - - const value = React.useMemo( - () => ({ - ...data, - dispatch, - }), - [data], - ); - return {children}; + const { contextValue } = useCharts(); + return {children}; } export { InteractionProvider }; diff --git a/packages/x-charts/src/context/InteractionSelectors.ts b/packages/x-charts/src/context/InteractionSelectors.ts new file mode 100644 index 000000000000..c911baeae216 --- /dev/null +++ b/packages/x-charts/src/context/InteractionSelectors.ts @@ -0,0 +1,46 @@ +import { ChartState } from '../internals/plugins/models'; +import { createSelector } from '../internals/plugins/utils/selectors'; + +function selectInteraction(state: ChartState) { + return state.interaction; +} + +export const selectorChartsInteractionItem = createSelector( + selectInteraction, + (interaction) => interaction.item, +); + +export const selectorChartsInteractionAxis = createSelector( + selectInteraction, + (interaction) => interaction.axis, +); + +export const selectorChartsInteractionXAxis = createSelector( + selectInteraction, + (interaction) => interaction.axis.x, +); + +export const selectorChartsInteractionYAxis = createSelector( + selectInteraction, + (interaction) => interaction.axis.y, +); + +export const selectorChartsInteractionItemIsDefined = createSelector( + selectorChartsInteractionItem, + (item) => item !== null, +); + +export const selectorChartsInteractionXAxisIsDefined = createSelector( + selectorChartsInteractionXAxis, + (x) => x !== null, +); + +export const selectorChartsInteractionYAxisIsDefined = createSelector( + selectorChartsInteractionYAxis, + (y) => y !== null, +); + +export const selectorChartsInteractionIsVoronoiEnabled = createSelector( + selectInteraction, + (interaction) => interaction.isVoronoiEnabled, +); diff --git a/packages/x-charts/src/context/SizeProvider/Size.types.ts b/packages/x-charts/src/context/SizeProvider/Size.types.ts new file mode 100644 index 000000000000..ea93098aa2cc --- /dev/null +++ b/packages/x-charts/src/context/SizeProvider/Size.types.ts @@ -0,0 +1,42 @@ +import * as React from 'react'; + +export interface SizeProviderProps { + /** + * The width of the chart in px. If not defined, it takes the width of the parent element. + */ + width?: number; + /** + * The height of the chart in px. If not defined, it takes the height of the parent element. + */ + height?: number; + /** + * The chart will try to wait for the parent container to resolve its size + * before it renders for the first time. + * + * This can be useful in some scenarios where the chart appear to grow after + * the first render, like when used inside a grid. + * + * @default false + */ + resolveSizeBeforeRender?: boolean; + children: React.ReactNode; +} + +export interface SizeContextState extends Required> { + /** + * The ref of the container element that the chart is rendered in. + */ + containerRef: React.RefObject; + /** + * If the chart has a defined size. + */ + hasIntrinsicSize: boolean; + /** + * The input height of the chart in px. + */ + inHeight?: number; + /** + * The input width of the chart in px. + */ + inWidth?: number; +} diff --git a/packages/x-charts/src/context/SizeProvider/SizeContext.ts b/packages/x-charts/src/context/SizeProvider/SizeContext.ts new file mode 100644 index 000000000000..f9c4b22b7a9c --- /dev/null +++ b/packages/x-charts/src/context/SizeProvider/SizeContext.ts @@ -0,0 +1,18 @@ +import * as React from 'react'; + +import { Initializable } from '../context.types'; +import { SizeContextState } from './Size.types'; + +export const SizeContext = React.createContext>({ + isInitialized: false, + data: { + hasIntrinsicSize: false, + containerRef: null as any, + height: 0, + width: 0, + }, +}); + +if (process.env.NODE_ENV !== 'production') { + SizeContext.displayName = 'SizeContext'; +} diff --git a/packages/x-charts/src/context/SizeProvider/SizeProvider.tsx b/packages/x-charts/src/context/SizeProvider/SizeProvider.tsx new file mode 100644 index 000000000000..7b704a92931b --- /dev/null +++ b/packages/x-charts/src/context/SizeProvider/SizeProvider.tsx @@ -0,0 +1,32 @@ +'use client'; +import * as React from 'react'; +import { SizeContext } from './SizeContext'; +import { SizeProviderProps } from './Size.types'; +import { useChartContainerDimensions } from './useChartContainerDimensions'; + +/** + * The size provider. + * + * This differs from the DrawingProvider in that it provides the full size of the container. + * + * This provider is also responsible for resolving the size of the container before rendering and if the parent size changes. + */ +function SizeProvider(props: SizeProviderProps) { + const dimensions = useChartContainerDimensions( + props.width, + props.height, + props.resolveSizeBeforeRender, + ); + + const value = React.useMemo( + () => ({ + isInitialized: true, + data: dimensions, + }), + [dimensions], + ); + + return {props.children}; +} + +export { SizeProvider }; diff --git a/packages/x-charts/src/context/SizeProvider/index.ts b/packages/x-charts/src/context/SizeProvider/index.ts new file mode 100644 index 000000000000..dfeb24e47f33 --- /dev/null +++ b/packages/x-charts/src/context/SizeProvider/index.ts @@ -0,0 +1,4 @@ +export * from './SizeProvider'; +export * from './SizeContext'; +export * from './useSize'; +export * from './Size.types'; diff --git a/packages/x-charts/src/ChartContainer/useChartContainerDimensions.ts b/packages/x-charts/src/context/SizeProvider/useChartContainerDimensions.ts similarity index 93% rename from packages/x-charts/src/ChartContainer/useChartContainerDimensions.ts rename to packages/x-charts/src/context/SizeProvider/useChartContainerDimensions.ts index 56c5f6c602f5..e22bd75bdadf 100644 --- a/packages/x-charts/src/ChartContainer/useChartContainerDimensions.ts +++ b/packages/x-charts/src/context/SizeProvider/useChartContainerDimensions.ts @@ -112,5 +112,15 @@ export const useChartContainerDimensions = ( } } - return { containerRef: rootRef, width: inWidth ?? width, height: inHeight ?? height }; + const finalWidth = inWidth ?? width; + const finalHeight = inHeight ?? height; + + return { + containerRef: rootRef, + width: finalWidth, + height: finalHeight, + hasIntrinsicSize: Boolean(finalWidth && finalHeight), + inWidth, + inHeight, + }; }; diff --git a/packages/x-charts/src/context/SizeProvider/useSize.ts b/packages/x-charts/src/context/SizeProvider/useSize.ts new file mode 100644 index 000000000000..4e7ebb951300 --- /dev/null +++ b/packages/x-charts/src/context/SizeProvider/useSize.ts @@ -0,0 +1,12 @@ +'use client'; +import * as React from 'react'; +import { SizeContext } from './SizeContext'; +import { SizeContextState } from './Size.types'; + +/** + * Returns the size of the chart. And the ref of the container element that the chart is rendered in. + */ +export const useSize = (): SizeContextState => { + const { data } = React.useContext(SizeContext); + return data; +}; diff --git a/packages/x-charts/src/context/SvgRefProvider/SvgRef.types.ts b/packages/x-charts/src/context/SvgRefProvider/SvgRef.types.ts index 562fb46a3466..92449792410f 100644 --- a/packages/x-charts/src/context/SvgRefProvider/SvgRef.types.ts +++ b/packages/x-charts/src/context/SvgRefProvider/SvgRef.types.ts @@ -2,7 +2,9 @@ import * as React from 'react'; export interface SvgRefProviderProps { children: React.ReactNode; - svgRef: React.Ref; } -export type SvgRefState = React.Ref; +export type SvgRefState = { + svgRef: React.Ref; + surfaceRef: React.Ref; +}; diff --git a/packages/x-charts/src/context/SvgRefProvider/SvgRefContext.tsx b/packages/x-charts/src/context/SvgRefProvider/SvgRefContext.tsx index c126e233db9d..139761daee62 100644 --- a/packages/x-charts/src/context/SvgRefProvider/SvgRefContext.tsx +++ b/packages/x-charts/src/context/SvgRefProvider/SvgRefContext.tsx @@ -5,7 +5,10 @@ import { SvgRefState } from './SvgRef.types'; export const SvgRefContext = React.createContext>({ isInitialized: false, - data: { current: null }, + data: { + svgRef: { current: null }, + surfaceRef: { current: null }, + }, }); if (process.env.NODE_ENV !== 'production') { diff --git a/packages/x-charts/src/context/SvgRefProvider/SvgRefProvider.tsx b/packages/x-charts/src/context/SvgRefProvider/SvgRefProvider.tsx index accbeead0d2b..fec512d29a82 100644 --- a/packages/x-charts/src/context/SvgRefProvider/SvgRefProvider.tsx +++ b/packages/x-charts/src/context/SvgRefProvider/SvgRefProvider.tsx @@ -1,12 +1,18 @@ 'use client'; import * as React from 'react'; +import useForkRef from '@mui/utils/useForkRef'; import { SvgRefProviderProps } from './SvgRef.types'; import { SvgRefContext } from './SvgRefContext'; export function SvgRefProvider(props: SvgRefProviderProps) { - const { svgRef, children } = props; + const { children } = props; + const svgRef = React.useRef(null); + const surfaceRef = useForkRef(svgRef); - const refValue = React.useMemo(() => ({ isInitialized: true, data: svgRef }), [svgRef]); + const refValue = React.useMemo( + () => ({ isInitialized: true, data: { svgRef, surfaceRef } }), + [svgRef, surfaceRef], + ); return {children}; } diff --git a/packages/x-charts/src/context/SvgRefProvider/index.ts b/packages/x-charts/src/context/SvgRefProvider/index.ts index a4d07c773d5a..f6d75d4f23d6 100644 --- a/packages/x-charts/src/context/SvgRefProvider/index.ts +++ b/packages/x-charts/src/context/SvgRefProvider/index.ts @@ -1,3 +1,4 @@ export * from './SvgRef.types'; export * from './SvgRefProvider'; export * from './SvgRefContext'; +export * from './useSurfaceRef'; diff --git a/packages/x-charts/src/context/SvgRefProvider/useSurfaceRef.ts b/packages/x-charts/src/context/SvgRefProvider/useSurfaceRef.ts new file mode 100644 index 000000000000..5c05ae4d1200 --- /dev/null +++ b/packages/x-charts/src/context/SvgRefProvider/useSurfaceRef.ts @@ -0,0 +1,18 @@ +'use client'; +import * as React from 'react'; +import { SvgRefContext } from './SvgRefContext'; + +export function useSurfaceRef(): React.MutableRefObject { + const { isInitialized, data } = React.useContext(SvgRefContext); + + if (!isInitialized) { + throw new Error( + [ + 'MUI X: Could not find the svg ref context.', + 'It looks like you rendered your component outside of a ChartsContainer parent component.', + ].join('\n'), + ); + } + + return data.surfaceRef as React.MutableRefObject; +} diff --git a/packages/x-charts/src/context/index.ts b/packages/x-charts/src/context/index.ts index a5cb93cdb90d..c95a0ef9fb67 100644 --- a/packages/x-charts/src/context/index.ts +++ b/packages/x-charts/src/context/index.ts @@ -1,3 +1,5 @@ export * from './HighlightedProvider'; export { ZAxisContextProvider } from './ZAxisContextProvider'; export type { ZAxisContextProviderProps } from './ZAxisContextProvider'; +export { ChartDataProvider } from './ChartDataProvider'; +export type { ChartDataProviderProps } from './ChartDataProvider'; diff --git a/packages/x-charts/src/hooks/useAxisEvents.ts b/packages/x-charts/src/hooks/useAxisEvents.ts index f3973f3fb393..2860ba00e32e 100644 --- a/packages/x-charts/src/hooks/useAxisEvents.ts +++ b/packages/x-charts/src/hooks/useAxisEvents.ts @@ -1,12 +1,12 @@ 'use client'; import * as React from 'react'; -import { InteractionContext } from '../context/InteractionProvider'; import { useCartesianContext } from '../context/CartesianProvider'; import { isBandScale } from '../internals/isBandScale'; import { AxisDefaultized } from '../models/axis'; import { getSVGPoint } from '../internals/getSVGPoint'; import { useSvgRef } from './useSvgRef'; import { useDrawingArea } from './useDrawingArea'; +import { useStore } from '../internals/useStore'; function getAsANumber(value: number | Date) { return value instanceof Date ? value.getTime() : value; @@ -15,7 +15,8 @@ export const useAxisEvents = (disableAxisListener: boolean) => { const svgRef = useSvgRef(); const drawingArea = useDrawingArea(); const { xAxis, yAxis, xAxisIds, yAxisIds } = useCartesianContext(); - const { dispatch } = React.useContext(InteractionContext); + + const store = useStore(disableAxisListener); const usedXAxis = xAxisIds[0]; const usedYAxis = yAxisIds[0]; @@ -29,7 +30,7 @@ export const useAxisEvents = (disableAxisListener: boolean) => { React.useEffect(() => { const element = svgRef.current; - if (element === null || disableAxisListener) { + if (element === null || disableAxisListener || !store) { return () => {}; } @@ -100,7 +101,11 @@ export const useAxisEvents = (disableAxisListener: boolean) => { x: -1, y: -1, }; - dispatch({ type: 'exitChart' }); + + store.update((prev) => ({ + ...prev, + interaction: { item: null, axis: { x: null, y: null } }, + })); }; const handleMove = (event: MouseEvent | TouchEvent) => { @@ -112,7 +117,10 @@ export const useAxisEvents = (disableAxisListener: boolean) => { if (!drawingArea.isPointInside(svgPoint, { targetElement: event.target as SVGElement })) { if (mousePosition.current.isInChart) { - dispatch({ type: 'exitChart' }); + store.update((prev) => ({ + ...prev, + interaction: { item: null, axis: { x: null, y: null } }, + })); mousePosition.current.isInChart = false; } return; @@ -121,7 +129,24 @@ export const useAxisEvents = (disableAxisListener: boolean) => { const newStateX = getNewAxisState(xAxis[usedXAxis], svgPoint.x); const newStateY = getNewAxisState(yAxis[usedYAxis], svgPoint.y); - dispatch({ type: 'updateAxis', data: { x: newStateX, y: newStateY } }); + store.update((prev) => ({ + ...prev, + interaction: { + ...prev.interaction, + axis: { + // A bit verbose, but prevent losing the x value if only y got modified. + ...prev.interaction.axis, + ...(prev.interaction.axis.x?.index !== newStateX?.index || + prev.interaction.axis.x?.value !== newStateX?.value + ? { x: newStateX } + : {}), + ...(prev.interaction.axis.y?.index !== newStateY?.index || + prev.interaction.axis.y?.value !== newStateY?.value + ? { y: newStateY } + : {}), + }, + }, + })); }; const handleDown = (event: PointerEvent) => { @@ -147,5 +172,5 @@ export const useAxisEvents = (disableAxisListener: boolean) => { element.removeEventListener('pointercancel', handleOut); element.removeEventListener('pointerleave', handleOut); }; - }, [svgRef, dispatch, usedYAxis, yAxis, usedXAxis, xAxis, disableAxisListener, drawingArea]); + }, [svgRef, store, usedYAxis, yAxis, usedXAxis, xAxis, disableAxisListener, drawingArea]); }; diff --git a/packages/x-charts/src/hooks/useInteractionItemProps.ts b/packages/x-charts/src/hooks/useInteractionItemProps.ts index 49a6e5e70c90..7474425fe650 100644 --- a/packages/x-charts/src/hooks/useInteractionItemProps.ts +++ b/packages/x-charts/src/hooks/useInteractionItemProps.ts @@ -1,11 +1,11 @@ 'use client'; import * as React from 'react'; -import { InteractionContext } from '../context/InteractionProvider'; import { SeriesItemIdentifier } from '../models'; import { useHighlighted } from '../context'; +import { useStore } from '../internals/useStore'; export const useInteractionItemProps = (skip?: boolean) => { - const { dispatch: dispatchInteraction } = React.useContext(InteractionContext); + const store = useStore(); const { setHighlighted, clearHighlighted } = useHighlighted(); if (skip) { @@ -18,10 +18,13 @@ export const useInteractionItemProps = (skip?: boolean) => { } }; const onPointerEnter = () => { - dispatchInteraction({ - type: 'enterItem', - data, - }); + store.update((prev) => ({ + ...prev, + interaction: { + ...prev.interaction, + item: data, + }, + })); setHighlighted({ seriesId: data.seriesId, dataIndex: data.dataIndex, @@ -29,7 +32,26 @@ export const useInteractionItemProps = (skip?: boolean) => { }; const onPointerLeave = (event: React.PointerEvent) => { event.currentTarget.releasePointerCapture(event.pointerId); - dispatchInteraction({ type: 'leaveItem', data }); + + store.update((prev) => { + const prevItem = prev.interaction.item; + if ( + prevItem === null || + Object.keys(data).some( + (key) => data[key as keyof typeof data] !== prevItem[key as keyof typeof prevItem], + ) + ) { + // The item is already something else, no need to clean it. + return prev; + } + return { + ...prev, + interaction: { + ...prev.interaction, + item: null, + }, + }; + }); clearHighlighted(); }; return { diff --git a/packages/x-charts/src/hooks/useSvgRef.test.tsx b/packages/x-charts/src/hooks/useSvgRef.test.tsx index 87ad0d2d3bc5..e6d16c2beab9 100644 --- a/packages/x-charts/src/hooks/useSvgRef.test.tsx +++ b/packages/x-charts/src/hooks/useSvgRef.test.tsx @@ -2,13 +2,22 @@ import * as React from 'react'; import { expect } from 'chai'; import { ErrorBoundary, createRenderer, screen } from '@mui/internal-test-utils'; import { useSvgRef } from './useSvgRef'; -import { SvgRefProvider } from '../context/SvgRefProvider'; +import { SvgRefProvider, useSurfaceRef } from '../context/SvgRefProvider'; function UseSvgRef() { const ref = useSvgRef(); return
{ref.current?.id}
; } +function UseSurfaceRef({ children }: any) { + const ref = useSurfaceRef(); + return ( + + {children} + + ); +} + describe('useSvgRef', () => { const { render } = createRenderer(); @@ -41,14 +50,12 @@ describe('useSvgRef', () => { it('should not throw an error when parent context is present', async () => { function RenderDrawingProvider() { - const ref = React.useRef(null); - return ( - - + + - - + + ); } diff --git a/packages/x-charts/src/hooks/useSvgRef.ts b/packages/x-charts/src/hooks/useSvgRef.ts index 7c13970c7b65..ef1e3fe842ac 100644 --- a/packages/x-charts/src/hooks/useSvgRef.ts +++ b/packages/x-charts/src/hooks/useSvgRef.ts @@ -14,5 +14,5 @@ export function useSvgRef(): React.MutableRefObject { ); } - return data as React.MutableRefObject; + return data.svgRef as React.MutableRefObject; } diff --git a/packages/x-charts/src/internals/components/ChartsAxesGradients/ChartsAxesGradients.tsx b/packages/x-charts/src/internals/components/ChartsAxesGradients/ChartsAxesGradients.tsx index 4a3f02cb2371..68d6958c233f 100644 --- a/packages/x-charts/src/internals/components/ChartsAxesGradients/ChartsAxesGradients.tsx +++ b/packages/x-charts/src/internals/components/ChartsAxesGradients/ChartsAxesGradients.tsx @@ -22,76 +22,79 @@ export function ChartsAxesGradients() { const getGradientId = useChartGradient(); const { xAxisIds, xAxis, yAxisIds, yAxis } = useCartesianContext(); + const filteredYAxisIds = yAxisIds.filter((axisId) => yAxis[axisId].colorMap !== undefined); + const filteredXAxisIds = xAxisIds.filter((axisId) => xAxis[axisId].colorMap !== undefined); + + if (filteredYAxisIds.length === 0 && filteredXAxisIds.length === 0) { + return null; + } + return ( - {yAxisIds - .filter((axisId) => yAxis[axisId].colorMap !== undefined) - .map((axisId) => { - const gradientId = getGradientId(axisId, 'y'); - const { colorMap, scale, colorScale, reverse } = yAxis[axisId]; - if (colorMap?.type === 'piecewise') { - return ( - - ); - } - if (colorMap?.type === 'continuous') { - return ( - - ); - } - return null; - })} - {xAxisIds - .filter((axisId) => xAxis[axisId].colorMap !== undefined) - .map((axisId) => { - const gradientId = getGradientId(axisId, 'x'); - const { colorMap, scale, reverse, colorScale } = xAxis[axisId]; - if (colorMap?.type === 'piecewise') { - return ( - - ); - } - if (colorMap?.type === 'continuous') { - return ( - - ); - } - return null; - })} + {filteredYAxisIds.map((axisId) => { + const gradientId = getGradientId(axisId, 'y'); + const { colorMap, scale, colorScale, reverse } = yAxis[axisId]; + if (colorMap?.type === 'piecewise') { + return ( + + ); + } + if (colorMap?.type === 'continuous') { + return ( + + ); + } + return null; + })} + {filteredXAxisIds.map((axisId) => { + const gradientId = getGradientId(axisId, 'x'); + const { colorMap, scale, reverse, colorScale } = xAxis[axisId]; + if (colorMap?.type === 'piecewise') { + return ( + + ); + } + if (colorMap?.type === 'continuous') { + return ( + + ); + } + return null; + })} ); } diff --git a/packages/x-charts/src/internals/index.ts b/packages/x-charts/src/internals/index.ts index fd233b5553d2..c96a644c9b58 100644 --- a/packages/x-charts/src/internals/index.ts +++ b/packages/x-charts/src/internals/index.ts @@ -1,7 +1,6 @@ // Components export * from './components/ChartsAxesGradients'; -export * from '../ChartContainer/useChartContainerDimensions'; export * from '../ChartContainer/ResizableContainer'; // hooks @@ -36,6 +35,7 @@ export * from '../context/AnimationProvider'; export type * from '../context/context.types'; export { getAxisExtremum } from '../context/CartesianProvider/getAxisExtremum'; export * from '../context/ChartDataProvider'; +export * from '../context/SizeProvider'; export * from '../context/SvgRefProvider'; // series configuration diff --git a/packages/x-charts/src/internals/plugins/models/index.ts b/packages/x-charts/src/internals/plugins/models/index.ts new file mode 100644 index 000000000000..ba5ae5bd5f5f --- /dev/null +++ b/packages/x-charts/src/internals/plugins/models/index.ts @@ -0,0 +1,39 @@ +import { ChartItemIdentifier, ChartSeriesType } from '../../../models/seriesType/config'; + +export type ItemInteractionData = ChartItemIdentifier; + +export type AxisInteractionData = { + x: null | { + value: number | Date | string; + // Set to -1 if no index. + index: number; + }; + y: null | { + value: number | Date | string; + // Set to -1 if no index. + index: number; + }; +}; + +type InteractionState = { + /** + * The item currently interacting. + */ + item: null | ItemInteractionData; + /** + * The x- and y-axes currently interacting. + */ + axis: AxisInteractionData; + /** + * Set to `true` when `VoronoiHandler` is active. + * Used to prevent collision with mouseEnter events. + */ + isVoronoiEnabled?: boolean; +}; + +export type ChartStateCacheKey = { id: number }; + +export type ChartState = { + interaction: InteractionState; + cacheKey: ChartStateCacheKey; +}; diff --git a/packages/x-charts/src/internals/plugins/utils/ChartStore.ts b/packages/x-charts/src/internals/plugins/utils/ChartStore.ts new file mode 100644 index 000000000000..f4cdbc2324d9 --- /dev/null +++ b/packages/x-charts/src/internals/plugins/utils/ChartStore.ts @@ -0,0 +1,35 @@ +import type { ChartState } from '../models'; // For now this is fixed. Will need to support generic if we add plugins + +type Listener = (value: T) => void; + +export type StoreUpdater = (prevState: ChartState) => ChartState; + +export class ChartStore { + public value: ChartState; + + private listeners: Set>; + + constructor(value: ChartState) { + this.value = value; + this.listeners = new Set(); + } + + public subscribe = (fn: Listener) => { + this.listeners.add(fn); + return () => { + this.listeners.delete(fn); + }; + }; + + public getSnapshot = () => { + return this.value; + }; + + public update = (updater: StoreUpdater) => { + const newState = updater(this.value); + if (newState !== this.value) { + this.value = newState; + this.listeners.forEach((l) => l(newState)); + } + }; +} diff --git a/packages/x-charts/src/internals/plugins/utils/selectors.ts b/packages/x-charts/src/internals/plugins/utils/selectors.ts new file mode 100644 index 000000000000..a91ac4a1a0fa --- /dev/null +++ b/packages/x-charts/src/internals/plugins/utils/selectors.ts @@ -0,0 +1,50 @@ +import { lruMemoize, createSelectorCreator, CreateSelectorFunction } from 'reselect'; +import { ChartState, ChartStateCacheKey } from '../models'; + +const reselectCreateSelector = createSelectorCreator({ + memoize: lruMemoize, + memoizeOptions: { + maxSize: 1, + equalityCheck: Object.is, + }, +}); + +const cache = new WeakMap< + ChartStateCacheKey, + Map, any> +>(); + +export type ChartsRootSelector = (state: ChartState) => ChartState[keyof ChartState]; + +export type ChartsSelector = (state: TState, args: TArgs) => TResult; + +/** + * Method wrapping reselect's createSelector to provide caching for chart instances. + * + */ +export const createSelector = ((...createSelectorArgs: any) => { + const selector: ChartsSelector = (state, selectorArgs) => { + const cacheKey = state.cacheKey; + + // If there is no cache for the current chart instance, create one. + let cacheForCurrentChartInstance = cache.get(cacheKey); + if (!cacheForCurrentChartInstance) { + cacheForCurrentChartInstance = new Map(); + cache.set(cacheKey, cacheForCurrentChartInstance); + } + + // If there is a cached selector, execute it. + const cachedSelector = cacheForCurrentChartInstance.get(createSelectorArgs); + if (cachedSelector) { + return cachedSelector(state, selectorArgs); + } + + // Otherwise, create a new selector and cache it and execute it. + const fn = reselectCreateSelector(...createSelectorArgs); + cacheForCurrentChartInstance.set(createSelectorArgs, fn); + + return fn(state, selectorArgs); + }; + + return selector; +}) as unknown as CreateSelectorFunction; diff --git a/packages/x-charts/src/internals/useCharts.ts b/packages/x-charts/src/internals/useCharts.ts new file mode 100644 index 000000000000..dddafa853ba8 --- /dev/null +++ b/packages/x-charts/src/internals/useCharts.ts @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { ChartStore } from './plugins/utils/ChartStore'; +import { ChartState } from './plugins/models'; + +let globalId = 0; + +export function useCharts() { + const storeRef = React.useRef(null); + if (storeRef.current == null) { + // eslint-disable-next-line react-compiler/react-compiler + globalId += 1; + const initialState: ChartState = { + interaction: { + item: null, + axis: { x: null, y: null }, + }, + cacheKey: { id: globalId }, + }; + storeRef.current = new ChartStore(initialState); + } + + const contextValue = React.useMemo(() => ({ store: storeRef.current as ChartStore }), []); + + return { contextValue }; +} diff --git a/packages/x-charts/src/internals/useSelector.ts b/packages/x-charts/src/internals/useSelector.ts new file mode 100644 index 000000000000..b4940ae5c8d3 --- /dev/null +++ b/packages/x-charts/src/internals/useSelector.ts @@ -0,0 +1,23 @@ +import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector'; +import { ChartState } from './plugins/models'; +import { ChartsSelector } from './plugins/utils/selectors'; +import { ChartStore } from './plugins/utils/ChartStore'; + +const defaultCompare = Object.is; + +export const useSelector = ( + store: ChartStore, + selector: ChartsSelector, + args: TArgs = undefined as TArgs, + equals: (a: TValue, b: TValue) => boolean = defaultCompare, +): TValue => { + const selectorWithArgs = (state: ChartState) => selector(state, args); + + return useSyncExternalStoreWithSelector( + store.subscribe, + store.getSnapshot, + store.getSnapshot, + selectorWithArgs, + equals, + ); +}; diff --git a/packages/x-charts/src/internals/useStore.ts b/packages/x-charts/src/internals/useStore.ts new file mode 100644 index 000000000000..17e6d3b6316f --- /dev/null +++ b/packages/x-charts/src/internals/useStore.ts @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { ChartsContext } from '../context/InteractionProvider'; +import { ChartStore } from './plugins/utils/ChartStore'; + +export function useStore(skipError?: boolean): ChartStore { + const charts = React.useContext(ChartsContext); + + if (skipError) { + // TODO: Remove once store is used by all charts. + // TODO: Remove once store is used by all charts. + // This line is only for `useAxisEvents` which is in the surface of the Gauge. + // But the Gauge don't have store yet because it does not need the interaction provider. + // Will be fixed when every thing move to the store since every component will have access to it. + // @ts-ignore + return charts?.store; + } + if (!charts) { + throw new Error( + [ + 'MUI X: Could not find the charts context.', + 'It looks like you rendered your component outside of a ChartsContainer parent component.', + ].join('\n'), + ); + } + + return charts.store; +} diff --git a/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx b/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx index fdf3990d7767..ad5d41c588c7 100644 --- a/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx +++ b/packages/x-data-grid-pro/src/tests/rowSelection.DataGridPro.test.tsx @@ -11,6 +11,7 @@ import { GridRowSelectionModel, GridRowsProp, GridColDef, + GridFilterModel, } from '@mui/x-data-grid-pro'; import { getBasicGridData } from '@mui/x-data-grid-generator'; @@ -194,6 +195,69 @@ describe(' - Row selection', () => { expect(apiRef.current.getSelectedRows()).to.have.keys([1]); }); + // Context: https://github.com/mui/mui-x/issues/15045 + it('should not throw when using `isRowSelectable` and `keepNonExistentRowsSelected`', () => { + function TestDataGrid() { + const [gridRows, setRows] = React.useState(rows); + const onFilterChange = React.useCallback( + (filterModel: GridFilterModel) => { + if (filterModel.items?.length === 0) { + return; + } + + const filteredRows = rows.filter((row) => { + return row.jobTitle.includes(filterModel.items[0].value); + }); + setRows(filteredRows); + }, + [setRows], + ); + return ( + true} + rows={gridRows} + onFilterModelChange={onFilterChange} + keepNonExistentRowsSelected + /> + ); + } + render(); + + // Select `Thomas` + fireEvent.click( + screen.getAllByRole('checkbox', { + name: /select row/i, + })[1], + ); + + expect(apiRef.current.getSelectedRows()).to.have.length(1); + expect(Array.from(apiRef.current.getSelectedRows())[0][0]).to.equal(1); + + act(() => { + apiRef.current.setFilterModel({ + items: [{ field: 'jobTitle', value: 'Head of Human Resources', operator: 'contains' }], + }); + }); + + expect(apiRef.current.getSelectedRows()).to.have.length(1); + expect(Array.from(apiRef.current.getSelectedRows())[0][0]).to.equal(1); + }); + + // Context: https://github.com/mui/mui-x/issues/15068 + it('should not call `onRowSelectionModelChange` when adding a new row', () => { + const onRowSelectionModelChange = spy(); + const { setProps } = render( + , + ); + + act(() => { + setProps({ rows: [...rows, { id: 15, hierarchy: ['New'], jobTitle: 'Test Job' }] }); + }); + + expect(onRowSelectionModelChange.callCount).to.equal(0); + }); + it('should put the parent into indeterminate if some but not all the children are selected', () => { render(); diff --git a/packages/x-data-grid/src/components/cell/GridCell.tsx b/packages/x-data-grid/src/components/cell/GridCell.tsx index 3760b113a261..f81ba9e89e3d 100644 --- a/packages/x-data-grid/src/components/cell/GridCell.tsx +++ b/packages/x-data-grid/src/components/cell/GridCell.tsx @@ -368,6 +368,10 @@ const GridCell = React.forwardRef(function GridCe if (rowSpan > 1) { cellStyle.height = `calc(var(--height) * ${rowSpan})`; cellStyle.zIndex = 5; + + if (isLeftPinned || isRightPinned) { + cellStyle.zIndex = 6; + } } return cellStyle; diff --git a/packages/x-data-grid/src/components/columnsManagement/GridColumnsManagement.tsx b/packages/x-data-grid/src/components/columnsManagement/GridColumnsManagement.tsx index 431736337d34..a5c15703ecb1 100644 --- a/packages/x-data-grid/src/components/columnsManagement/GridColumnsManagement.tsx +++ b/packages/x-data-grid/src/components/columnsManagement/GridColumnsManagement.tsx @@ -4,6 +4,8 @@ import PropTypes from 'prop-types'; import composeClasses from '@mui/utils/composeClasses'; import FormControlLabel from '@mui/material/FormControlLabel'; import { styled } from '@mui/material/styles'; +import TextField, { TextFieldProps } from '@mui/material/TextField'; +import { inputBaseClasses } from '@mui/material/InputBase'; import { gridColumnDefinitionsSelector, gridColumnVisibilityModelSelector, @@ -24,6 +26,7 @@ export interface GridColumnsManagementProps { */ sort?: 'asc' | 'desc'; searchPredicate?: (column: GridColDef, searchValue: string) => boolean; + searchInputProps?: Partial; /** * If `true`, the column search field will be focused automatically. * If `false`, the first column switch input will be focused automatically. @@ -66,6 +69,7 @@ const useUtilityClasses = (ownerState: OwnerState) => { const slots = { root: ['columnsManagement'], header: ['columnsManagementHeader'], + searchInput: ['columnsManagementSearchInput'], footer: ['columnsManagementFooter'], row: ['columnsManagementRow'], }; @@ -95,6 +99,7 @@ function GridColumnsManagement(props: GridColumnsManagementProps) { disableResetButton = false, toggleAllMode = 'all', getTogglableColumns, + searchInputProps, } = props; const isResetDisabled = React.useMemo( @@ -207,27 +212,59 @@ function GridColumnsManagement(props: GridColumnsManagementProps) { } return false; }; + const handleSearchReset = React.useCallback(() => { + setSearchValue(''); + searchInputRef.current!.focus(); + }, []); return ( - ), - sx: { pl: 1.5 }, + endAdornment: ( + + + + ), + }} + inputProps={{ + 'aria-label': apiRef.current.getLocaleText('columnsManagementSearchTitle'), }} + autoComplete="off" fullWidth {...rootProps.slotProps?.baseTextField} + {...searchInputProps} /> @@ -321,6 +358,7 @@ GridColumnsManagement.propTypes = { * @returns {GridColDef['field'][]} The list of togglable columns' field names. */ getTogglableColumns: PropTypes.func, + searchInputProps: PropTypes.object, searchPredicate: PropTypes.func, sort: PropTypes.oneOf(['asc', 'desc']), /** @@ -354,6 +392,23 @@ const GridColumnsManagementHeader = styled('div', { padding: theme.spacing(1.5, 3), })); +const SearchInput = styled(TextField, { + name: 'MuiDataGrid', + slot: 'ColumnsManagementSearchInput', + overridesResolver: (props, styles) => styles.columnsManagementSearchInput, +})<{ ownerState: OwnerState }>(({ theme }) => ({ + [`& .${inputBaseClasses.root}`]: { + padding: theme.spacing(0, 1.5, 0, 1.5), + }, + [`& .${inputBaseClasses.input}::-webkit-search-decoration, + & .${inputBaseClasses.input}::-webkit-search-cancel-button, + & .${inputBaseClasses.input}::-webkit-search-results-button, + & .${inputBaseClasses.input}::-webkit-search-results-decoration`]: { + /* clears the 'X' icon from Chrome */ + display: 'none', + }, +})); + const GridColumnsManagementFooter = styled('div', { name: 'MuiDataGrid', slot: 'ColumnsManagementFooter', diff --git a/packages/x-data-grid/src/constants/gridClasses.ts b/packages/x-data-grid/src/constants/gridClasses.ts index 06ce8915846a..9082fae28e0e 100644 --- a/packages/x-data-grid/src/constants/gridClasses.ts +++ b/packages/x-data-grid/src/constants/gridClasses.ts @@ -242,6 +242,10 @@ export interface GridClasses { * Styles applied to the columns management header element. */ columnsManagementHeader: string; + /** + * Styles applied to the columns management search input element. + */ + columnsManagementSearchInput: string; /** * Styles applied to the columns management footer element. */ @@ -720,6 +724,7 @@ export const gridClasses = generateUtilityClasses('MuiDataGrid', [ 'columnsManagement', 'columnsManagementRow', 'columnsManagementHeader', + 'columnsManagementSearchInput', 'columnsManagementFooter', 'container--top', 'container--bottom', diff --git a/packages/x-data-grid/src/constants/localeTextConstants.ts b/packages/x-data-grid/src/constants/localeTextConstants.ts index c0d29a97a0e9..395d73a7355b 100644 --- a/packages/x-data-grid/src/constants/localeTextConstants.ts +++ b/packages/x-data-grid/src/constants/localeTextConstants.ts @@ -41,6 +41,7 @@ export const GRID_DEFAULT_LOCALE_TEXT: GridLocaleText = { columnsManagementNoColumns: 'No columns', columnsManagementShowHideAllText: 'Show/Hide All', columnsManagementReset: 'Reset', + columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Add filter', diff --git a/packages/x-data-grid/src/locales/arSD.ts b/packages/x-data-grid/src/locales/arSD.ts index 5d3ebd5993e5..b9aadc4d34bb 100644 --- a/packages/x-data-grid/src/locales/arSD.ts +++ b/packages/x-data-grid/src/locales/arSD.ts @@ -43,6 +43,7 @@ const arSDGrid: Partial = { // columnsManagementNoColumns: 'No columns', // columnsManagementShowHideAllText: 'Show/Hide All', // columnsManagementReset: 'Reset', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'إضافة مرشِح', diff --git a/packages/x-data-grid/src/locales/beBY.ts b/packages/x-data-grid/src/locales/beBY.ts index bba9d37e0fbd..e11cb810e4cf 100644 --- a/packages/x-data-grid/src/locales/beBY.ts +++ b/packages/x-data-grid/src/locales/beBY.ts @@ -66,6 +66,7 @@ const beBYGrid: Partial = { // columnsManagementNoColumns: 'No columns', // columnsManagementShowHideAllText: 'Show/Hide All', // columnsManagementReset: 'Reset', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Дадаць фільтр', diff --git a/packages/x-data-grid/src/locales/bgBG.ts b/packages/x-data-grid/src/locales/bgBG.ts index 3c360f174e45..a27adce61ffa 100644 --- a/packages/x-data-grid/src/locales/bgBG.ts +++ b/packages/x-data-grid/src/locales/bgBG.ts @@ -42,6 +42,7 @@ const bgBGGrid: Partial = { columnsManagementNoColumns: 'Няма колони', columnsManagementShowHideAllText: 'Покажи/Скрий Всичко', columnsManagementReset: 'Нулирай', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Добави Филтър', diff --git a/packages/x-data-grid/src/locales/csCZ.ts b/packages/x-data-grid/src/locales/csCZ.ts index c5b26a3ce10e..18611554aa82 100644 --- a/packages/x-data-grid/src/locales/csCZ.ts +++ b/packages/x-data-grid/src/locales/csCZ.ts @@ -50,6 +50,7 @@ const csCZGrid: Partial = { columnsManagementNoColumns: 'Žádné sloupce', columnsManagementShowHideAllText: 'Zobrazit/skrýt vše', columnsManagementReset: 'Resetovat', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Přidat filtr', diff --git a/packages/x-data-grid/src/locales/daDK.ts b/packages/x-data-grid/src/locales/daDK.ts index dc305634c30e..d6094d42ce26 100644 --- a/packages/x-data-grid/src/locales/daDK.ts +++ b/packages/x-data-grid/src/locales/daDK.ts @@ -43,6 +43,7 @@ const daDKGrid: Partial = { columnsManagementNoColumns: 'Ingen søjler', columnsManagementShowHideAllText: 'Vis/Skjul Alle', columnsManagementReset: 'Nulstil', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Tilføj filter', diff --git a/packages/x-data-grid/src/locales/deDE.ts b/packages/x-data-grid/src/locales/deDE.ts index 31a9350cb5e1..48c894e57ecd 100644 --- a/packages/x-data-grid/src/locales/deDE.ts +++ b/packages/x-data-grid/src/locales/deDE.ts @@ -43,6 +43,7 @@ const deDEGrid: Partial = { columnsManagementNoColumns: 'Keine Spalten', columnsManagementShowHideAllText: 'Alle anzeigen/verbergen', columnsManagementReset: 'Zurücksetzen', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Filter hinzufügen', diff --git a/packages/x-data-grid/src/locales/elGR.ts b/packages/x-data-grid/src/locales/elGR.ts index dbd6bdc36f3d..ad0c394d99ef 100644 --- a/packages/x-data-grid/src/locales/elGR.ts +++ b/packages/x-data-grid/src/locales/elGR.ts @@ -43,6 +43,7 @@ const elGRGrid: Partial = { // columnsManagementNoColumns: 'No columns', // columnsManagementShowHideAllText: 'Show/Hide All', // columnsManagementReset: 'Reset', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Προσθήκη φίλτρου', diff --git a/packages/x-data-grid/src/locales/esES.ts b/packages/x-data-grid/src/locales/esES.ts index 8547eb2602ea..87d6133d30a2 100644 --- a/packages/x-data-grid/src/locales/esES.ts +++ b/packages/x-data-grid/src/locales/esES.ts @@ -43,6 +43,7 @@ const esESGrid: Partial = { columnsManagementNoColumns: 'Sin columnas', columnsManagementShowHideAllText: 'Mostrar/Ocultar todas', columnsManagementReset: 'Restablecer', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Agregar filtro', @@ -58,9 +59,9 @@ const esESGrid: Partial = { // Filter operators text filterOperatorContains: 'contiene', - // filterOperatorDoesNotContain: 'does not contain', + filterOperatorDoesNotContain: 'no contiene', filterOperatorEquals: 'es igual', - // filterOperatorDoesNotEqual: 'does not equal', + filterOperatorDoesNotEqual: 'es diferente a', filterOperatorStartsWith: 'comienza con', filterOperatorEndsWith: 'termina con', filterOperatorIs: 'es', @@ -81,9 +82,9 @@ const esESGrid: Partial = { // Header filter operators text headerFilterOperatorContains: 'Contiene', - // headerFilterOperatorDoesNotContain: 'Does not contain', + headerFilterOperatorDoesNotContain: 'No contiene', headerFilterOperatorEquals: 'Es igual a', - // headerFilterOperatorDoesNotEqual: 'Does not equal', + headerFilterOperatorDoesNotEqual: 'Es diferente a', headerFilterOperatorStartsWith: 'Comienza con', headerFilterOperatorEndsWith: 'Termina con', headerFilterOperatorIs: 'Es', diff --git a/packages/x-data-grid/src/locales/faIR.ts b/packages/x-data-grid/src/locales/faIR.ts index 41dbdd4ca402..6fc24b6f734e 100644 --- a/packages/x-data-grid/src/locales/faIR.ts +++ b/packages/x-data-grid/src/locales/faIR.ts @@ -43,6 +43,7 @@ const faIRGrid: Partial = { columnsManagementNoColumns: 'بدون سطر', columnsManagementShowHideAllText: 'نمایش/مخفی کردن همه', columnsManagementReset: 'بازنشانی', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'افزودن فیلتر', diff --git a/packages/x-data-grid/src/locales/fiFI.ts b/packages/x-data-grid/src/locales/fiFI.ts index d8fee01123fb..00391b62b18c 100644 --- a/packages/x-data-grid/src/locales/fiFI.ts +++ b/packages/x-data-grid/src/locales/fiFI.ts @@ -43,6 +43,7 @@ const fiFIGrid: Partial = { columnsManagementNoColumns: 'Ei sarakkeita näytettäväksi', columnsManagementShowHideAllText: 'Näytä/Piilota kaikki', columnsManagementReset: 'Palauta', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Lisää suodatin', diff --git a/packages/x-data-grid/src/locales/frFR.ts b/packages/x-data-grid/src/locales/frFR.ts index 96abb7fc40e9..e6928a00e57a 100644 --- a/packages/x-data-grid/src/locales/frFR.ts +++ b/packages/x-data-grid/src/locales/frFR.ts @@ -43,6 +43,7 @@ const frFRGrid: Partial = { columnsManagementNoColumns: 'Pas de colonnes', columnsManagementShowHideAllText: 'Afficher/masquer toutes', columnsManagementReset: 'Réinitialiser', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Ajouter un filtre', diff --git a/packages/x-data-grid/src/locales/heIL.ts b/packages/x-data-grid/src/locales/heIL.ts index 61556403a76a..7612d2a8cc39 100644 --- a/packages/x-data-grid/src/locales/heIL.ts +++ b/packages/x-data-grid/src/locales/heIL.ts @@ -43,6 +43,7 @@ const heILGrid: Partial = { columnsManagementNoColumns: 'אין עמודות', columnsManagementShowHideAllText: 'הצג/הסתר הכל', columnsManagementReset: 'אתחול', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'הוסף מסנן', diff --git a/packages/x-data-grid/src/locales/hrHR.ts b/packages/x-data-grid/src/locales/hrHR.ts index 26493db593d9..5999823e9d72 100644 --- a/packages/x-data-grid/src/locales/hrHR.ts +++ b/packages/x-data-grid/src/locales/hrHR.ts @@ -50,6 +50,7 @@ const hrHRGrid: Partial = { columnsManagementNoColumns: 'Nema stupaca', columnsManagementShowHideAllText: 'Prikaži/Sakrij sve', columnsManagementReset: 'Ponovno namjesti', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Dodaj filter', diff --git a/packages/x-data-grid/src/locales/huHU.ts b/packages/x-data-grid/src/locales/huHU.ts index f65c09cdba73..9d9d78e8a7f6 100644 --- a/packages/x-data-grid/src/locales/huHU.ts +++ b/packages/x-data-grid/src/locales/huHU.ts @@ -42,6 +42,7 @@ const huHUGrid: Partial = { columnsManagementNoColumns: 'Nincsenek oszlopok', columnsManagementShowHideAllText: 'Összes', columnsManagementReset: 'Visszavon', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Szűrő hozzáadása', diff --git a/packages/x-data-grid/src/locales/isIS.ts b/packages/x-data-grid/src/locales/isIS.ts index 307231873d9d..70f9c45d6119 100644 --- a/packages/x-data-grid/src/locales/isIS.ts +++ b/packages/x-data-grid/src/locales/isIS.ts @@ -43,6 +43,7 @@ const isISGrid: Partial = { // columnsManagementNoColumns: 'No columns', // columnsManagementShowHideAllText: 'Show/Hide All', // columnsManagementReset: 'Reset', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Bæta síu', diff --git a/packages/x-data-grid/src/locales/itIT.ts b/packages/x-data-grid/src/locales/itIT.ts index 2058096d6165..7291525a07b8 100644 --- a/packages/x-data-grid/src/locales/itIT.ts +++ b/packages/x-data-grid/src/locales/itIT.ts @@ -43,6 +43,7 @@ const itITGrid: Partial = { columnsManagementNoColumns: 'Nessuna colonna', columnsManagementShowHideAllText: 'Mostra/Nascondi Tutto', columnsManagementReset: 'Resetta', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Aggiungi un filtro', diff --git a/packages/x-data-grid/src/locales/jaJP.ts b/packages/x-data-grid/src/locales/jaJP.ts index 00f626c51187..fd2f69883cf8 100644 --- a/packages/x-data-grid/src/locales/jaJP.ts +++ b/packages/x-data-grid/src/locales/jaJP.ts @@ -42,6 +42,7 @@ const jaJPGrid: Partial = { columnsManagementNoColumns: 'カラムなし', columnsManagementShowHideAllText: 'すべて表示/非表示', columnsManagementReset: 'リセット', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'フィルター追加', diff --git a/packages/x-data-grid/src/locales/koKR.ts b/packages/x-data-grid/src/locales/koKR.ts index 671f6abd0520..bacd936d8fd3 100644 --- a/packages/x-data-grid/src/locales/koKR.ts +++ b/packages/x-data-grid/src/locales/koKR.ts @@ -42,6 +42,7 @@ const koKRGrid: Partial = { // columnsManagementNoColumns: 'No columns', // columnsManagementShowHideAllText: 'Show/Hide All', // columnsManagementReset: 'Reset', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: '필터 추가', diff --git a/packages/x-data-grid/src/locales/nbNO.ts b/packages/x-data-grid/src/locales/nbNO.ts index 9fa25b0cc4f8..70bd33fc7704 100644 --- a/packages/x-data-grid/src/locales/nbNO.ts +++ b/packages/x-data-grid/src/locales/nbNO.ts @@ -43,6 +43,7 @@ const nbNOGrid: Partial = { columnsManagementNoColumns: 'Ingen kolonner', columnsManagementShowHideAllText: 'Vis/skjul alle', columnsManagementReset: 'Nullstill', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Legg til filter', diff --git a/packages/x-data-grid/src/locales/nlNL.ts b/packages/x-data-grid/src/locales/nlNL.ts index ccc1f5378ab4..4f247c94b2d8 100644 --- a/packages/x-data-grid/src/locales/nlNL.ts +++ b/packages/x-data-grid/src/locales/nlNL.ts @@ -43,6 +43,7 @@ const nlNLGrid: Partial = { columnsManagementNoColumns: 'Geen kolommen', columnsManagementShowHideAllText: 'Toon/Verberg Alle', columnsManagementReset: 'Reset', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Filter toevoegen', diff --git a/packages/x-data-grid/src/locales/nnNO.ts b/packages/x-data-grid/src/locales/nnNO.ts index 58595a0a4456..e97a9692adb5 100644 --- a/packages/x-data-grid/src/locales/nnNO.ts +++ b/packages/x-data-grid/src/locales/nnNO.ts @@ -43,6 +43,7 @@ const nnNOGrid: Partial = { columnsManagementNoColumns: 'Ingen kolonner', columnsManagementShowHideAllText: 'Vis/skjul alle', columnsManagementReset: 'Nullstill', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Legg til filter', diff --git a/packages/x-data-grid/src/locales/plPL.ts b/packages/x-data-grid/src/locales/plPL.ts index af001de70617..772e84dd64e5 100644 --- a/packages/x-data-grid/src/locales/plPL.ts +++ b/packages/x-data-grid/src/locales/plPL.ts @@ -42,6 +42,7 @@ const plPLGrid: Partial = { columnsManagementNoColumns: 'Brak kolumn', columnsManagementShowHideAllText: 'Wyświetl/Ukryj wszystkie', columnsManagementReset: 'Resetuj', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Dodaj filtr', diff --git a/packages/x-data-grid/src/locales/ptBR.ts b/packages/x-data-grid/src/locales/ptBR.ts index 4dbd7b806024..a921b980bd90 100644 --- a/packages/x-data-grid/src/locales/ptBR.ts +++ b/packages/x-data-grid/src/locales/ptBR.ts @@ -43,6 +43,7 @@ const ptBRGrid: Partial = { columnsManagementNoColumns: 'Nenhuma coluna', columnsManagementShowHideAllText: 'Mostrar/Ocultar Todas', columnsManagementReset: 'Redefinir', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Adicionar filtro', diff --git a/packages/x-data-grid/src/locales/ptPT.ts b/packages/x-data-grid/src/locales/ptPT.ts index 1024917ed9c3..88fa81bdc1c0 100644 --- a/packages/x-data-grid/src/locales/ptPT.ts +++ b/packages/x-data-grid/src/locales/ptPT.ts @@ -43,6 +43,7 @@ const ptPTGrid: Partial = { columnsManagementNoColumns: 'Sem colunas', columnsManagementShowHideAllText: 'Mostrar/Ocultar Todas', columnsManagementReset: 'Repor', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Adicionar filtro', diff --git a/packages/x-data-grid/src/locales/roRO.ts b/packages/x-data-grid/src/locales/roRO.ts index 2a2db174eeac..cc49a4f52e6d 100644 --- a/packages/x-data-grid/src/locales/roRO.ts +++ b/packages/x-data-grid/src/locales/roRO.ts @@ -43,6 +43,7 @@ const roROGrid: Partial = { // columnsManagementNoColumns: 'No columns', // columnsManagementShowHideAllText: 'Show/Hide All', // columnsManagementReset: 'Reset', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Adăugare filtru', diff --git a/packages/x-data-grid/src/locales/ruRU.ts b/packages/x-data-grid/src/locales/ruRU.ts index 54a7b8514962..61a1613e8ecd 100644 --- a/packages/x-data-grid/src/locales/ruRU.ts +++ b/packages/x-data-grid/src/locales/ruRU.ts @@ -67,6 +67,7 @@ const ruRUGrid: Partial = { columnsManagementNoColumns: 'Нет столбцов', columnsManagementShowHideAllText: 'Показать/Скрыть Всё', columnsManagementReset: 'Сбросить', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Добавить фильтр', diff --git a/packages/x-data-grid/src/locales/skSK.ts b/packages/x-data-grid/src/locales/skSK.ts index f1d2f3be0513..bca93172c221 100644 --- a/packages/x-data-grid/src/locales/skSK.ts +++ b/packages/x-data-grid/src/locales/skSK.ts @@ -50,6 +50,7 @@ const skSKGrid: Partial = { columnsManagementNoColumns: 'Žiadne stĺpce', columnsManagementShowHideAllText: 'Zobraziť/Skryť všetko', // columnsManagementReset: 'Reset', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Pridať filter', diff --git a/packages/x-data-grid/src/locales/svSE.ts b/packages/x-data-grid/src/locales/svSE.ts index 516d93b53f01..46c5a6f089c5 100644 --- a/packages/x-data-grid/src/locales/svSE.ts +++ b/packages/x-data-grid/src/locales/svSE.ts @@ -42,7 +42,8 @@ const svSEGrid: Partial = { columnsManagementSearchTitle: 'Sök', columnsManagementNoColumns: 'Inga kolumner', columnsManagementShowHideAllText: 'Visa/Dölj alla', - // columnsManagementReset: 'Reset', + columnsManagementReset: 'Återställ', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Lägg till filter', @@ -58,9 +59,9 @@ const svSEGrid: Partial = { // Filter operators text filterOperatorContains: 'innehåller', - // filterOperatorDoesNotContain: 'does not contain', + filterOperatorDoesNotContain: 'innehåller inte', filterOperatorEquals: 'lika med', - // filterOperatorDoesNotEqual: 'does not equal', + filterOperatorDoesNotEqual: 'inte lika med', filterOperatorStartsWith: 'börjar med', filterOperatorEndsWith: 'slutar med', filterOperatorIs: 'är', @@ -81,9 +82,9 @@ const svSEGrid: Partial = { // Header filter operators text headerFilterOperatorContains: 'Innehåller', - // headerFilterOperatorDoesNotContain: 'Does not contain', + headerFilterOperatorDoesNotContain: 'Innehåller inte', headerFilterOperatorEquals: 'Lika med', - // headerFilterOperatorDoesNotEqual: 'Does not equal', + headerFilterOperatorDoesNotEqual: 'Inte lika med', headerFilterOperatorStartsWith: 'Börjar med', headerFilterOperatorEndsWith: 'Slutar med', headerFilterOperatorIs: 'Är', diff --git a/packages/x-data-grid/src/locales/trTR.ts b/packages/x-data-grid/src/locales/trTR.ts index aac3da7f9772..07dd3ee608ec 100644 --- a/packages/x-data-grid/src/locales/trTR.ts +++ b/packages/x-data-grid/src/locales/trTR.ts @@ -42,6 +42,7 @@ const trTRGrid: Partial = { columnsManagementNoColumns: 'Kolon yok', columnsManagementShowHideAllText: 'Hepsini Göster/Gizle', columnsManagementReset: 'Sıfırla', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Filtre Ekle', diff --git a/packages/x-data-grid/src/locales/ukUA.ts b/packages/x-data-grid/src/locales/ukUA.ts index 456bf8b00f06..f04fa67e9b5d 100644 --- a/packages/x-data-grid/src/locales/ukUA.ts +++ b/packages/x-data-grid/src/locales/ukUA.ts @@ -67,6 +67,7 @@ const ukUAGrid: Partial = { // columnsManagementNoColumns: 'No columns', // columnsManagementShowHideAllText: 'Show/Hide All', // columnsManagementReset: 'Reset', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Додати фільтр', diff --git a/packages/x-data-grid/src/locales/urPK.ts b/packages/x-data-grid/src/locales/urPK.ts index 2001e0ea29d1..2ea1572123ca 100644 --- a/packages/x-data-grid/src/locales/urPK.ts +++ b/packages/x-data-grid/src/locales/urPK.ts @@ -43,6 +43,7 @@ const urPKGrid: Partial = { // columnsManagementNoColumns: 'No columns', // columnsManagementShowHideAllText: 'Show/Hide All', // columnsManagementReset: 'Reset', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'نیا فلٹر', diff --git a/packages/x-data-grid/src/locales/viVN.ts b/packages/x-data-grid/src/locales/viVN.ts index a76c4783b593..354f9db689d3 100644 --- a/packages/x-data-grid/src/locales/viVN.ts +++ b/packages/x-data-grid/src/locales/viVN.ts @@ -43,6 +43,7 @@ const viVNGrid: Partial = { columnsManagementNoColumns: 'Không có cột', columnsManagementShowHideAllText: 'Hiện/Ẩn Tất cả', columnsManagementReset: 'Đặt lại', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: 'Thêm bộ lọc', diff --git a/packages/x-data-grid/src/locales/zhCN.ts b/packages/x-data-grid/src/locales/zhCN.ts index 0de5f7988f01..74574df253b8 100644 --- a/packages/x-data-grid/src/locales/zhCN.ts +++ b/packages/x-data-grid/src/locales/zhCN.ts @@ -42,6 +42,7 @@ const zhCNGrid: Partial = { columnsManagementNoColumns: '没有列', columnsManagementShowHideAllText: '显示/隐藏所有', columnsManagementReset: '重置', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: '添加筛选器', @@ -57,9 +58,9 @@ const zhCNGrid: Partial = { // Filter operators text filterOperatorContains: '包含', - // filterOperatorDoesNotContain: 'does not contain', + filterOperatorDoesNotContain: '不包含', filterOperatorEquals: '等于', - // filterOperatorDoesNotEqual: 'does not equal', + filterOperatorDoesNotEqual: '不等于', filterOperatorStartsWith: '开始于', filterOperatorEndsWith: '结束于', filterOperatorIs: '是', @@ -80,9 +81,9 @@ const zhCNGrid: Partial = { // Header filter operators text headerFilterOperatorContains: '包含', - // headerFilterOperatorDoesNotContain: 'Does not contain', + headerFilterOperatorDoesNotContain: '不包含', headerFilterOperatorEquals: '等于', - // headerFilterOperatorDoesNotEqual: 'Does not equal', + headerFilterOperatorDoesNotEqual: '不等于', headerFilterOperatorStartsWith: '开始于', headerFilterOperatorEndsWith: '结束于', headerFilterOperatorIs: '是', diff --git a/packages/x-data-grid/src/locales/zhHK.ts b/packages/x-data-grid/src/locales/zhHK.ts index 3f53e6b7d9ee..0e73634398a0 100644 --- a/packages/x-data-grid/src/locales/zhHK.ts +++ b/packages/x-data-grid/src/locales/zhHK.ts @@ -43,6 +43,7 @@ const zhHKGrid: Partial = { // columnsManagementNoColumns: 'No columns', // columnsManagementShowHideAllText: 'Show/Hide All', // columnsManagementReset: 'Reset', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: '新增過濾器', diff --git a/packages/x-data-grid/src/locales/zhTW.ts b/packages/x-data-grid/src/locales/zhTW.ts index facba1f16343..0052bc700964 100644 --- a/packages/x-data-grid/src/locales/zhTW.ts +++ b/packages/x-data-grid/src/locales/zhTW.ts @@ -42,6 +42,7 @@ const zhTWGrid: Partial = { // columnsManagementNoColumns: 'No columns', // columnsManagementShowHideAllText: 'Show/Hide All', // columnsManagementReset: 'Reset', + // columnsManagementDeleteIconLabel: 'Clear', // Filter panel text filterPanelAddFilter: '增加篩選器', diff --git a/packages/x-data-grid/src/models/api/gridLocaleTextApi.ts b/packages/x-data-grid/src/models/api/gridLocaleTextApi.ts index b2ce738ff721..9573191b4756 100644 --- a/packages/x-data-grid/src/models/api/gridLocaleTextApi.ts +++ b/packages/x-data-grid/src/models/api/gridLocaleTextApi.ts @@ -53,6 +53,7 @@ export interface GridLocaleText { columnsManagementNoColumns: string; columnsManagementShowHideAllText: string; columnsManagementReset: string; + columnsManagementDeleteIconLabel: string; // Filter panel text filterPanelAddFilter: React.ReactNode; diff --git a/packages/x-data-grid/src/tests/rowSelection.DataGrid.test.tsx b/packages/x-data-grid/src/tests/rowSelection.DataGrid.test.tsx index 4ad235c405e8..e549ffd11300 100644 --- a/packages/x-data-grid/src/tests/rowSelection.DataGrid.test.tsx +++ b/packages/x-data-grid/src/tests/rowSelection.DataGrid.test.tsx @@ -18,6 +18,7 @@ import { useGridApiRef, GridApi, GridPreferencePanelsValue, + GridRowSelectionModel, } from '@mui/x-data-grid'; import { getCell, @@ -65,6 +66,29 @@ describe(' - Row selection', () => { ); } + // Context: https://github.com/mui/mui-x/issues/15079 + it('should not call `onRowSelectionModelChange` twice when using filterMode="server"', () => { + const onRowSelectionModelChange = spy(); + function TestDataGrid() { + const [, setRowSelectionModel] = React.useState([]); + const handleRowSelectionModelChange = React.useCallback((model: GridRowSelectionModel) => { + setRowSelectionModel(model); + onRowSelectionModelChange(model); + }, []); + return ( + row.id} + checkboxSelection + onRowSelectionModelChange={handleRowSelectionModelChange} + filterMode="server" + /> + ); + } + render(); + fireEvent.click(getCell(0, 0).querySelector('input')!); + expect(onRowSelectionModelChange.callCount).to.equal(1); + }); + describe('prop: checkboxSelection = false (single selection)', () => { it('should select one row at a time on click WITHOUT ctrl or meta pressed', () => { render(); diff --git a/packages/x-data-grid/src/tests/toolbar.DataGrid.test.tsx b/packages/x-data-grid/src/tests/toolbar.DataGrid.test.tsx index 92cea1015317..5f35b8c50009 100644 --- a/packages/x-data-grid/src/tests/toolbar.DataGrid.test.tsx +++ b/packages/x-data-grid/src/tests/toolbar.DataGrid.test.tsx @@ -154,7 +154,7 @@ describe(' - Toolbar', () => { fireEvent.click(screen.getByText('Columns')); - const searchInput = document.querySelector('input[type="text"]')!; + const searchInput = document.querySelector('input[type="search"]')!; fireEvent.change(searchInput, { target: { value: 'test' } }); expect(document.querySelector('[role="tooltip"] [name="id"]')).not.to.equal(null); diff --git a/packages/x-tree-view-pro/package.json b/packages/x-tree-view-pro/package.json index 47c3fbf73f0b..eb4d82f852e6 100644 --- a/packages/x-tree-view-pro/package.json +++ b/packages/x-tree-view-pro/package.json @@ -51,7 +51,9 @@ "@types/react-transition-group": "^4.4.11", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" + "react-transition-group": "^4.4.5", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@emotion/react": "^11.9.0", @@ -74,6 +76,7 @@ "@mui/material": "^5.16.7", "@mui/system": "^5.16.7", "@types/prop-types": "^15.7.13", + "@types/use-sync-external-store": "^0.0.6", "rimraf": "^6.0.1" }, "engines": { diff --git a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx index 5f48c869de88..74be110c7958 100644 --- a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx +++ b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx @@ -75,14 +75,13 @@ const RichTreeViewPro = React.forwardRef(function RichTreeViewPro< } } - const { getRootProps, contextValue, instance } = useTreeView< - RichTreeViewProPluginSignatures, - typeof props - >({ - plugins: RICH_TREE_VIEW_PRO_PLUGINS, - rootRef: ref, - props, - }); + const { getRootProps, contextValue } = useTreeView( + { + plugins: RICH_TREE_VIEW_PRO_PLUGINS, + rootRef: ref, + props, + }, + ); const { slots, slotProps } = props; const classes = useUtilityClasses(props); @@ -99,11 +98,7 @@ const RichTreeViewPro = React.forwardRef(function RichTreeViewPro< return ( - + diff --git a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.types.ts b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.types.ts index ff865e8fe213..b2d4a02f071f 100644 --- a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.types.ts +++ b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.types.ts @@ -2,9 +2,12 @@ import * as React from 'react'; import { Theme } from '@mui/material/styles'; import { SxProps } from '@mui/system'; import { SlotComponentProps } from '@mui/utils'; -import { TreeItem, TreeItemProps } from '@mui/x-tree-view/TreeItem'; -import { TreeViewItemId } from '@mui/x-tree-view/models'; -import { TreeViewPublicAPI, TreeViewExperimentalFeatures } from '@mui/x-tree-view/internals'; +import { + TreeViewPublicAPI, + TreeViewExperimentalFeatures, + RichTreeViewItemsSlots, + RichTreeViewItemsSlotProps, +} from '@mui/x-tree-view/internals'; import { RichTreeViewProClasses } from './richTreeViewProClasses'; import { RichTreeViewProPluginParameters, @@ -13,28 +16,18 @@ import { RichTreeViewProPluginSignatures, } from './RichTreeViewPro.plugins'; -interface RichTreeViewItemProSlotOwnerState { - itemId: TreeViewItemId; - label: string; -} - -export interface RichTreeViewProSlots extends RichTreeViewProPluginSlots { +export interface RichTreeViewProSlots extends RichTreeViewProPluginSlots, RichTreeViewItemsSlots { /** * Element rendered at the root. * @default RichTreeViewProRoot */ root?: React.ElementType; - /** - * Custom component for the item. - * @default TreeItem. - */ - item?: React.JSXElementConstructor; } export interface RichTreeViewProSlotProps - extends RichTreeViewProPluginSlotProps { + extends RichTreeViewProPluginSlotProps, + RichTreeViewItemsSlotProps { root?: SlotComponentProps<'ul', {}, RichTreeViewProProps>; - item?: SlotComponentProps; } export type RichTreeViewProApiRef = React.MutableRefObject< diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.itemPlugin.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.itemPlugin.ts index 999d7b2b3e3f..1906cef2d7ea 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.itemPlugin.ts +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.itemPlugin.ts @@ -5,6 +5,7 @@ import { useTreeViewContext, UseTreeViewItemsSignature, isTargetInDescendants, + useSelector, } from '@mui/x-tree-view/internals'; import { UseTreeItemDragAndDropOverlaySlotPropsFromItemsReordering, @@ -13,16 +14,27 @@ import { TreeViewItemItemReorderingValidActions, UseTreeItemContentSlotPropsFromItemsReordering, } from './useTreeViewItemsReordering.types'; +import { + selectorItemsReorderingDraggedItemProperties, + selectorItemsReorderingIsValidTarget, +} from './useTreeViewItemsReordering.selectors'; export const isAndroid = () => navigator.userAgent.toLowerCase().includes('android'); export const useTreeViewItemsReorderingItemPlugin: TreeViewItemPlugin = ({ props }) => { - const { itemsReordering, instance } = + const { instance, store, itemsReordering } = useTreeViewContext<[UseTreeViewItemsSignature, UseTreeViewItemsReorderingSignature]>(); const { itemId } = props; const validActionsRef = React.useRef(null); + const draggedItemProperties = useSelector( + store, + selectorItemsReorderingDraggedItemProperties, + itemId, + ); + const isValidTarget = useSelector(store, selectorItemsReorderingIsValidTarget, itemId); + return { propsEnhancers: { root: ({ @@ -30,8 +42,10 @@ export const useTreeViewItemsReorderingItemPlugin: TreeViewItemPlugin = ({ props contentRefObject, externalEventHandlers, }): UseTreeItemRootSlotPropsFromItemsReordering => { - const draggable = instance.canItemBeDragged(itemId); - if (!draggable) { + if ( + !itemsReordering.enabled || + (itemsReordering.isItemReorderable && !itemsReordering.isItemReorderable(itemId)) + ) { return {}; } @@ -92,8 +106,7 @@ export const useTreeViewItemsReorderingItemPlugin: TreeViewItemPlugin = ({ props externalEventHandlers, contentRefObject, }): UseTreeItemContentSlotPropsFromItemsReordering => { - const currentDrag = itemsReordering.currentDrag; - if (!currentDrag || currentDrag.draggedItemId === itemId) { + if (!isValidTarget) { return {}; } @@ -131,20 +144,15 @@ export const useTreeViewItemsReorderingItemPlugin: TreeViewItemPlugin = ({ props }; }, dragAndDropOverlay: (): UseTreeItemDragAndDropOverlaySlotPropsFromItemsReordering => { - const currentDrag = itemsReordering.currentDrag; - if (!currentDrag || currentDrag.targetItemId !== itemId || currentDrag.action == null) { + if (!draggedItemProperties) { return {}; } - const targetDepth = - currentDrag.newPosition?.parentId == null - ? 0 - : // The depth is always defined because drag&drop is only usable with Rich Tree View components. - instance.getItemMeta(currentDrag.newPosition.parentId).depth! + 1; - return { - action: currentDrag.action, - style: { '--TreeView-targetDepth': targetDepth } as React.CSSProperties, + action: draggedItemProperties.action, + style: { + '--TreeView-targetDepth': draggedItemProperties.targetDepth, + } as React.CSSProperties, }; }, }, diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.selectors.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.selectors.ts new file mode 100644 index 000000000000..8e59419f0259 --- /dev/null +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.selectors.ts @@ -0,0 +1,53 @@ +import { createSelector, TreeViewState, selectorItemMetaLookup } from '@mui/x-tree-view/internals'; +import { UseTreeViewItemsReorderingSignature } from './useTreeViewItemsReordering.types'; + +/** + * Get the items reordering state. + * @param {TreeViewState<[UseTreeViewItemsReorderingSignature]>} state The state of the tree view. + * @returns {TreeViewItemsReorderingState | null} The items reordering state. + */ +export const selectorItemsReordering = ( + state: TreeViewState<[UseTreeViewItemsReorderingSignature]>, +) => state.itemsReordering; + +/** + * Get the properties of the dragged item. + * @param {TreeViewState<[UseTreeViewItemsSignature, UseTreeViewItemsReorderingSignature]>} state The state of the tree view. + * @param {string} itemId The id of the item. + * @returns {TreeViewItemDraggedItemProperties | null} The properties of the dragged item if the current item is being dragged, `null` otherwise. + */ +export const selectorItemsReorderingDraggedItemProperties = createSelector( + [selectorItemsReordering, selectorItemMetaLookup, (_, itemId: string) => itemId], + (itemsReordering, itemMetaLookup, itemId) => { + if ( + !itemsReordering || + itemsReordering.targetItemId !== itemId || + itemsReordering.action == null + ) { + return null; + } + + const targetDepth = + itemsReordering.newPosition?.parentId == null + ? 0 + : // The depth is always defined because drag&drop is only usable with Rich Tree View components. + itemMetaLookup[itemId].depth! + 1; + + return { + newPosition: itemsReordering.newPosition, + action: itemsReordering.action, + targetDepth, + }; + }, +); + +/** + * Check if the current item is a valid target for the dragged item. + * @param {TreeViewState<[UseTreeViewItemsReorderingSignature]>} state The state of the tree view. + * @param {string} itemId The id of the item. + * @returns {boolean} `true` if the current item is a valid target for the dragged item, `false` otherwise. + */ +export const selectorItemsReorderingIsValidTarget = createSelector( + [selectorItemsReordering, (_, itemId: string) => itemId], + (itemsReordering, itemId) => itemsReordering && itemsReordering.draggedItemId !== itemId, +); diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx index f414c28fa431..cfd604e7ac70 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx @@ -123,7 +123,7 @@ describeTreeView< fireEvent.keyDown(view.getItemRoot('2'), { key: 'Enter' }); expect(view.getItemIdTree()).to.deep.equal([ - { id: '1', children: [] }, + { id: '1' }, { id: '2', children: [{ id: '1.1' }] }, ]); }); diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts index 45b8568da050..c096a1320ebd 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts @@ -1,5 +1,10 @@ import * as React from 'react'; -import { TreeViewPlugin } from '@mui/x-tree-view/internals'; +import { + TreeViewPlugin, + selectorItemIndex, + selectorItemMeta, + selectorItemOrderedChildrenIds, +} from '@mui/x-tree-view/internals'; import { warnOnce } from '@mui/x-internals/warning'; import { TreeViewItemsReorderingAction } from '@mui/x-tree-view/models'; import { @@ -14,12 +19,11 @@ import { moveItemInTree, } from './useTreeViewItemsReordering.utils'; import { useTreeViewItemsReorderingItemPlugin } from './useTreeViewItemsReordering.itemPlugin'; +import { selectorItemsReordering } from './useTreeViewItemsReordering.selectors'; export const useTreeViewItemsReordering: TreeViewPlugin = ({ params, - instance, - state, - setState, + store, }) => { const canItemBeDragged = React.useCallback( (itemId: string) => { @@ -39,7 +43,7 @@ export const useTreeViewItemsReordering: TreeViewPlugin { - const itemsReordering = state.itemsReordering; + const itemsReordering = selectorItemsReordering(store.value); if (!itemsReordering) { throw new Error('There is no ongoing reordering.'); } @@ -49,10 +53,10 @@ export const useTreeViewItemsReordering: TreeViewPlugin { - setState((prevState) => ({ + store.update((prevState) => ({ ...prevState, itemsReordering: { targetItemId: itemId, @@ -137,34 +141,35 @@ export const useTreeViewItemsReordering: TreeViewPlugin { - if (state.itemsReordering == null || state.itemsReordering.draggedItemId !== itemId) { + const itemsReordering = selectorItemsReordering(store.value); + if (itemsReordering == null || itemsReordering.draggedItemId !== itemId) { return; } if ( - state.itemsReordering.draggedItemId === state.itemsReordering.targetItemId || - state.itemsReordering.action == null || - state.itemsReordering.newPosition == null + itemsReordering.draggedItemId === itemsReordering.targetItemId || + itemsReordering.action == null || + itemsReordering.newPosition == null ) { - setState((prevState) => ({ ...prevState, itemsReordering: null })); + store.update((prevState) => ({ ...prevState, itemsReordering: null })); return; } - const draggedItemMeta = instance.getItemMeta(state.itemsReordering.draggedItemId); + const draggedItemMeta = selectorItemMeta(store.value, itemsReordering.draggedItemId)!; const oldPosition: TreeViewItemReorderPosition = { parentId: draggedItemMeta.parentId, - index: instance.getItemIndex(draggedItemMeta.id), + index: selectorItemIndex(store.value, draggedItemMeta.id), }; - const newPosition = state.itemsReordering.newPosition; + const newPosition = itemsReordering.newPosition; - setState((prevState) => ({ + store.update((prevState) => ({ ...prevState, itemsReordering: null, items: moveItemInTree({ @@ -182,23 +187,23 @@ export const useTreeViewItemsReordering: TreeViewPlugin( ({ itemId, validActions, targetHeight, cursorY, cursorX, contentElement }) => { - setState((prevState) => { + store.update((prevState) => { const prevSubState = prevState.itemsReordering; - if (prevSubState == null || isAncestor(instance, itemId, prevSubState.draggedItemId)) { + if (prevSubState == null || isAncestor(store, itemId, prevSubState.draggedItemId)) { return prevState; } const action = chooseActionToApply({ itemChildrenIndentation: params.itemChildrenIndentation, validActions, targetHeight, - targetDepth: prevState.items.itemMetaMap[itemId].depth!, + targetDepth: prevState.items.itemMetaLookup[itemId].depth!, cursorY, cursorX, contentElement, @@ -226,7 +231,17 @@ export const useTreeViewItemsReordering: TreeViewPlugin ({ + itemsReordering: { + enabled: params.itemsReordering, + isItemReorderable: params.isItemReorderable, + }, + }), + [params.itemsReordering, params.isItemReorderable], ); return { @@ -237,12 +252,7 @@ export const useTreeViewItemsReordering: TreeViewPlugin boolean) | undefined; }; } diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts index bfd7f19a3ba8..171d961838bc 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.utils.ts @@ -1,25 +1,26 @@ import { - TreeViewInstance, - UseTreeViewItemsSignature, + TreeViewUsedStore, UseTreeViewItemsState, buildSiblingIndexes, TREE_VIEW_ROOT_PARENT_ID, + selectorItemMeta, } from '@mui/x-tree-view/internals'; import { TreeViewItemId, TreeViewItemsReorderingAction } from '@mui/x-tree-view/models'; import { TreeViewItemItemReorderingValidActions, TreeViewItemReorderPosition, + UseTreeViewItemsReorderingSignature, } from './useTreeViewItemsReordering.types'; /** * Checks if the item with the id itemIdB is an ancestor of the item with the id itemIdA. */ export const isAncestor = ( - instance: TreeViewInstance<[UseTreeViewItemsSignature]>, + store: TreeViewUsedStore, itemIdA: string, itemIdB: string, ): boolean => { - const itemMetaA = instance.getItemMeta(itemIdA); + const itemMetaA = selectorItemMeta(store.value, itemIdA)!; if (itemMetaA.parentId === itemIdB) { return true; } @@ -28,7 +29,7 @@ export const isAncestor = ( return false; } - return isAncestor(instance, itemMetaA.parentId, itemIdB); + return isAncestor(store, itemMetaA.parentId, itemIdB); }; /** @@ -121,7 +122,7 @@ export const chooseActionToApply = ({ return action; }; -export const moveItemInTree = ({ +export const moveItemInTree = ({ itemToMoveId, oldPosition, newPosition, @@ -132,13 +133,13 @@ export const moveItemInTree = ({ newPosition: TreeViewItemReorderPosition; prevState: UseTreeViewItemsState['items']; }): UseTreeViewItemsState['items'] => { - const itemToMoveMeta = prevState.itemMetaMap[itemToMoveId]; + const itemToMoveMeta = prevState.itemMetaLookup[itemToMoveId]; const oldParentId = oldPosition.parentId ?? TREE_VIEW_ROOT_PARENT_ID; const newParentId = newPosition.parentId ?? TREE_VIEW_ROOT_PARENT_ID; // 1. Update the `itemOrderedChildrenIds`. - const itemOrderedChildrenIds = { ...prevState.itemOrderedChildrenIds }; + const itemOrderedChildrenIds = { ...prevState.itemOrderedChildrenIdsLookup }; if (oldParentId === newParentId) { const updatedChildren = [...itemOrderedChildrenIds[oldParentId]]; updatedChildren.splice(oldPosition.index, 1); @@ -155,27 +156,27 @@ export const moveItemInTree = ({ } // 2. Update the `itemChildrenIndexes` - const itemChildrenIndexes = { ...prevState.itemChildrenIndexes }; + const itemChildrenIndexes = { ...prevState.itemChildrenIndexesLookup }; itemChildrenIndexes[oldParentId] = buildSiblingIndexes(itemOrderedChildrenIds[oldParentId]); if (newParentId !== oldParentId) { itemChildrenIndexes[newParentId] = buildSiblingIndexes(itemOrderedChildrenIds[newParentId]); } - // 3. Update the `itemMetaMap` - const itemMetaMap = { ...prevState.itemMetaMap }; + // 3. Update the `itemMetaLookup` + const itemMetaLookup = { ...prevState.itemMetaLookup }; // 3.1 Update the `expandable` property of the old and the new parent if (oldParentId !== TREE_VIEW_ROOT_PARENT_ID && oldParentId !== newParentId) { - itemMetaMap[oldParentId].expandable = itemOrderedChildrenIds[oldParentId].length > 0; + itemMetaLookup[oldParentId].expandable = itemOrderedChildrenIds[oldParentId].length > 0; } if (newParentId !== TREE_VIEW_ROOT_PARENT_ID && newParentId !== oldParentId) { - itemMetaMap[newParentId].expandable = itemOrderedChildrenIds[newParentId].length > 0; + itemMetaLookup[newParentId].expandable = itemOrderedChildrenIds[newParentId].length > 0; } // 3.2 Update the `parentId` and `depth` properties of the item to move // The depth is always defined because drag&drop is only usable with Rich Tree View components. - const itemToMoveDepth = newPosition.parentId == null ? 0 : itemMetaMap[newParentId].depth! + 1; - itemMetaMap[itemToMoveId] = { + const itemToMoveDepth = newPosition.parentId == null ? 0 : itemMetaLookup[newParentId].depth! + 1; + itemMetaLookup[itemToMoveId] = { ...itemToMoveMeta, parentId: newPosition.parentId, depth: itemToMoveDepth, @@ -183,7 +184,7 @@ export const moveItemInTree = ({ // 3.3 Update the depth of all the children of the item to move const updateItemDepth = (itemId: string, depth: number) => { - itemMetaMap[itemId] = { ...itemMetaMap[itemId], depth }; + itemMetaLookup[itemId] = { ...itemMetaLookup[itemId], depth }; itemOrderedChildrenIds[itemId]?.forEach((childId) => updateItemDepth(childId, depth + 1)); }; itemOrderedChildrenIds[itemToMoveId]?.forEach((childId) => @@ -192,8 +193,8 @@ export const moveItemInTree = ({ return { ...prevState, - itemOrderedChildrenIds, - itemChildrenIndexes, - itemMetaMap, + itemOrderedChildrenIdsLookup: itemOrderedChildrenIds, + itemChildrenIndexesLookup: itemChildrenIndexes, + itemMetaLookup, }; }; diff --git a/packages/x-tree-view/package.json b/packages/x-tree-view/package.json index ddb626116fb5..d7d0fc74b139 100644 --- a/packages/x-tree-view/package.json +++ b/packages/x-tree-view/package.json @@ -49,7 +49,9 @@ "@types/react-transition-group": "^4.4.11", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" + "react-transition-group": "^4.4.5", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@emotion/react": "^11.9.0", @@ -72,6 +74,7 @@ "@mui/material": "^5.16.7", "@mui/system": "^5.16.7", "@types/prop-types": "^15.7.13", + "@types/use-sync-external-store": "^0.0.6", "rimraf": "^6.0.1" }, "engines": { diff --git a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx index 158ac9ab8ea4..0a31937d2971 100644 --- a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx +++ b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx @@ -68,10 +68,7 @@ const RichTreeView = React.forwardRef(function RichTreeView< } } - const { getRootProps, contextValue, instance } = useTreeView< - RichTreeViewPluginSignatures, - typeof props - >({ + const { getRootProps, contextValue } = useTreeView({ plugins: RICH_TREE_VIEW_PLUGINS, rootRef: ref, props, @@ -92,11 +89,7 @@ const RichTreeView = React.forwardRef(function RichTreeView< return ( - + ); diff --git a/packages/x-tree-view/src/RichTreeView/RichTreeView.types.ts b/packages/x-tree-view/src/RichTreeView/RichTreeView.types.ts index c774183d9a6d..502a88050ec6 100644 --- a/packages/x-tree-view/src/RichTreeView/RichTreeView.types.ts +++ b/packages/x-tree-view/src/RichTreeView/RichTreeView.types.ts @@ -2,7 +2,6 @@ import * as React from 'react'; import { Theme } from '@mui/material/styles'; import { SxProps } from '@mui/system'; import { SlotComponentProps } from '@mui/utils'; -import { SlotComponentPropsFromProps } from '@mui/x-internals/types'; import { RichTreeViewClasses } from './richTreeViewClasses'; import { RichTreeViewPluginParameters, @@ -10,32 +9,24 @@ import { RichTreeViewPluginSlots, RichTreeViewPluginSignatures, } from './RichTreeView.plugins'; -import { TreeItemProps } from '../TreeItem'; -import { TreeViewItemId } from '../models'; import { TreeViewExperimentalFeatures, TreeViewPublicAPI } from '../internals/models'; +import { + RichTreeViewItemsSlotProps, + RichTreeViewItemsSlots, +} from '../internals/components/RichTreeViewItems'; -interface RichTreeViewItemSlotOwnerState { - itemId: TreeViewItemId; - label: string; -} - -export interface RichTreeViewSlots extends RichTreeViewPluginSlots { +export interface RichTreeViewSlots extends RichTreeViewPluginSlots, RichTreeViewItemsSlots { /** * Element rendered at the root. * @default RichTreeViewRoot */ root?: React.ElementType; - /** - * Custom component for the item. - * @default TreeItem. - */ - item?: React.JSXElementConstructor; } export interface RichTreeViewSlotProps - extends RichTreeViewPluginSlotProps { + extends RichTreeViewPluginSlotProps, + RichTreeViewItemsSlotProps { root?: SlotComponentProps<'ul', {}, RichTreeViewProps>; - item?: SlotComponentPropsFromProps; } export type RichTreeViewApiRef = React.MutableRefObject< diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.tsx index 3f10accdbe42..98ac33c3f024 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItem.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItem.tsx @@ -220,6 +220,7 @@ export const TreeItem = React.forwardRef(function TreeItem( const { id, itemId, label, disabled, children, slots = {}, slotProps = {}, ...other } = props; const { + getContextProviderProps, getRootProps, getContentProps, getIconContainerProps, @@ -329,7 +330,7 @@ export const TreeItem = React.forwardRef(function TreeItem( }); return ( - + @@ -353,7 +354,7 @@ TreeItem.propTypes = { /** * The content of the component. */ - children: PropTypes.node, + children: PropTypes /* @typescript-to-proptypes-ignore */.any, /** * Override or extend the styles applied to the component. */ diff --git a/packages/x-tree-view/src/TreeItemProvider/TreeItemProvider.tsx b/packages/x-tree-view/src/TreeItemProvider/TreeItemProvider.tsx index e50cc7f9f81b..6994ee378e95 100644 --- a/packages/x-tree-view/src/TreeItemProvider/TreeItemProvider.tsx +++ b/packages/x-tree-view/src/TreeItemProvider/TreeItemProvider.tsx @@ -1,15 +1,17 @@ import PropTypes from 'prop-types'; import { TreeItemProviderProps } from './TreeItemProvider.types'; import { useTreeViewContext } from '../internals/TreeViewProvider'; +import { generateTreeItemIdAttribute } from '../internals/corePlugins/useTreeViewId/useTreeViewId.utils'; /** * @ignore - internal component. */ function TreeItemProvider(props: TreeItemProviderProps) { - const { children, itemId } = props; - const { wrapItem, instance } = useTreeViewContext<[]>(); + const { children, itemId, id } = props; + const { wrapItem, instance, treeId } = useTreeViewContext<[]>(); + const idAttribute = generateTreeItemIdAttribute({ itemId, treeId, id }); - return wrapItem({ children, itemId, instance }); + return wrapItem({ children, itemId, instance, idAttribute }); } TreeItemProvider.propTypes = { @@ -18,6 +20,7 @@ TreeItemProvider.propTypes = { // | To update them edit the TypeScript types and run "pnpm proptypes" | // ---------------------------------------------------------------------- children: PropTypes.node, + id: PropTypes.string, itemId: PropTypes.string.isRequired, } as any; diff --git a/packages/x-tree-view/src/TreeItemProvider/TreeItemProvider.types.ts b/packages/x-tree-view/src/TreeItemProvider/TreeItemProvider.types.ts index 5214935419c7..3357e56d27b8 100644 --- a/packages/x-tree-view/src/TreeItemProvider/TreeItemProvider.types.ts +++ b/packages/x-tree-view/src/TreeItemProvider/TreeItemProvider.types.ts @@ -4,4 +4,5 @@ import { TreeViewItemId } from '../models'; export interface TreeItemProviderProps { children: React.ReactNode; itemId: TreeViewItemId; + id: string | undefined; } diff --git a/packages/x-tree-view/src/hooks/index.ts b/packages/x-tree-view/src/hooks/index.ts index 5b9960c618bf..0149a071aef5 100644 --- a/packages/x-tree-view/src/hooks/index.ts +++ b/packages/x-tree-view/src/hooks/index.ts @@ -1,2 +1,3 @@ export { useTreeViewApiRef } from './useTreeViewApiRef'; export { useTreeItemUtils } from './useTreeItemUtils'; +export { useTreeItemModel } from './useTreeItemModel'; diff --git a/packages/x-tree-view/src/hooks/useTreeItemModel.ts b/packages/x-tree-view/src/hooks/useTreeItemModel.ts new file mode 100644 index 000000000000..696955b8d7c9 --- /dev/null +++ b/packages/x-tree-view/src/hooks/useTreeItemModel.ts @@ -0,0 +1,12 @@ +'use client'; +import { useTreeViewContext } from '../internals/TreeViewProvider'; +import { useSelector } from '../internals/hooks/useSelector'; +import { selectorItemModel } from '../internals/plugins/useTreeViewItems/useTreeViewItems.selectors'; +import { TreeViewBaseItem, TreeViewDefaultItemModelProperties, TreeViewItemId } from '../models'; + +export const useTreeItemModel = ( + itemId: TreeViewItemId, +) => { + const { store } = useTreeViewContext(); + return useSelector(store, selectorItemModel, itemId) as unknown as TreeViewBaseItem | null; +}; diff --git a/packages/x-tree-view/src/hooks/useTreeItemUtils/useTreeItemUtils.tsx b/packages/x-tree-view/src/hooks/useTreeItemUtils/useTreeItemUtils.tsx index ef03b9510dc3..623c80a30c05 100644 --- a/packages/x-tree-view/src/hooks/useTreeItemUtils/useTreeItemUtils.tsx +++ b/packages/x-tree-view/src/hooks/useTreeItemUtils/useTreeItemUtils.tsx @@ -13,6 +13,15 @@ import { import type { UseTreeItemStatus } from '../../useTreeItem'; import { hasPlugin } from '../../internals/utils/plugins'; import { TreeViewPublicAPI } from '../../internals/models'; +import { useSelector } from '../../internals/hooks/useSelector'; +import { selectorIsItemExpanded } from '../../internals/plugins/useTreeViewExpansion/useTreeViewExpansion.selectors'; +import { selectorIsItemFocused } from '../../internals/plugins/useTreeViewFocus/useTreeViewFocus.selectors'; +import { selectorIsItemDisabled } from '../../internals/plugins/useTreeViewItems/useTreeViewItems.selectors'; +import { selectorIsItemSelected } from '../../internals/plugins/useTreeViewSelection/useTreeViewSelection.selectors'; +import { + selectorIsItemBeingEdited, + selectorIsItemEditable, +} from '../../internals/plugins/useTreeViewLabel/useTreeViewLabel.selectors'; export interface UseTreeItemInteractions { handleExpansion: (event: React.MouseEvent) => void; @@ -51,7 +60,7 @@ interface UseTreeItemUtilsReturnValue< publicAPI: TreeViewPublicAPI; } -const isItemExpandable = (reactChildren: React.ReactNode) => { +export const isItemExpandable = (reactChildren: React.ReactNode) => { if (Array.isArray(reactChildren)) { return reactChildren.length > 0 && reactChildren.some(isItemExpandable); } @@ -66,22 +75,37 @@ export const useTreeItemUtils = < children, }: { itemId: string; - children: React.ReactNode; + children?: React.ReactNode; }): UseTreeItemUtilsReturnValue => { const { instance, + label, + store, selection: { multiSelect }, publicAPI, } = useTreeViewContext(); + const isExpanded = useSelector(store, selectorIsItemExpanded, itemId); + const isFocused = useSelector(store, selectorIsItemFocused, itemId); + const isSelected = useSelector(store, selectorIsItemSelected, itemId); + const isDisabled = useSelector(store, selectorIsItemDisabled, itemId); + const isEditing = useSelector(store, (state) => + label == null ? false : selectorIsItemBeingEdited(state, itemId), + ); + const isEditable = useSelector(store, (state) => + label == null + ? false + : selectorIsItemEditable(state, { itemId, isItemEditable: label.isItemEditable }), + ); + const status: UseTreeItemStatus = { expandable: isItemExpandable(children), - expanded: instance.isItemExpanded(itemId), - focused: instance.isItemFocused(itemId), - selected: instance.isItemSelected(itemId), - disabled: instance.isItemDisabled(itemId), - editing: instance?.isItemBeingEdited ? instance?.isItemBeingEdited(itemId) : false, - editable: instance.isItemEditable ? instance.isItemEditable(itemId) : false, + expanded: isExpanded, + focused: isFocused, + selected: isSelected, + disabled: isDisabled, + editing: isEditing, + editable: isEditable, }; const handleExpansion = (event: React.MouseEvent) => { @@ -96,7 +120,7 @@ export const useTreeItemUtils = < const multiple = multiSelect && (event.shiftKey || event.ctrlKey || event.metaKey); // If already expanded and trying to toggle selection don't close - if (status.expandable && !(multiple && instance.isItemExpanded(itemId))) { + if (status.expandable && !(multiple && selectorIsItemExpanded(store.value, itemId))) { instance.toggleItemExpansion(event, itemId); } }; @@ -141,8 +165,8 @@ export const useTreeItemUtils = < if (!hasPlugin(instance, useTreeViewLabel)) { return; } - if (instance.isItemEditable(itemId)) { - if (instance.isItemBeingEdited(itemId)) { + if (isEditable) { + if (isEditing) { instance.setEditedItemId(null); } else { instance.setEditedItemId(itemId); @@ -152,7 +176,7 @@ export const useTreeItemUtils = < const handleSaveItemLabel = ( event: React.SyntheticEvent & TreeViewCancellableEvent, - label: string, + newLabel: string, ) => { if (!hasPlugin(instance, useTreeViewLabel)) { return; @@ -161,9 +185,8 @@ export const useTreeItemUtils = < // As a side effect of `instance.focusItem` called here and in `handleCancelItemLabelEditing` the `labelInput` is blurred // The `onBlur` event is triggered, which calls `handleSaveItemLabel` again. // To avoid creating an unwanted behavior we need to check if the item is being edited before calling `updateItemLabel` - // using `instance.isItemBeingEditedRef` instead of `instance.isItemBeingEdited` since the state is not yet updated in this point - if (instance.isItemBeingEditedRef(itemId)) { - instance.updateItemLabel(itemId, label); + if (selectorIsItemBeingEdited(store.value, itemId)) { + instance.updateItemLabel(itemId, newLabel); toggleItemEditing(); instance.focusItem(event, itemId); } @@ -174,7 +197,7 @@ export const useTreeItemUtils = < return; } - if (instance.isItemBeingEditedRef(itemId)) { + if (selectorIsItemBeingEdited(store.value, itemId)) { toggleItemEditing(); instance.focusItem(event, itemId); } diff --git a/packages/x-tree-view/src/internals/TreeViewItemDepthContext/TreeViewItemDepthContext.ts b/packages/x-tree-view/src/internals/TreeViewItemDepthContext/TreeViewItemDepthContext.ts index e06f1da34038..ba31a7135222 100644 --- a/packages/x-tree-view/src/internals/TreeViewItemDepthContext/TreeViewItemDepthContext.ts +++ b/packages/x-tree-view/src/internals/TreeViewItemDepthContext/TreeViewItemDepthContext.ts @@ -1,8 +1,10 @@ import * as React from 'react'; import { TreeViewItemId } from '../../models'; +import { TreeViewState } from '../models'; +import type { UseTreeViewItemsSignature } from '../plugins/useTreeViewItems'; export const TreeViewItemDepthContext = React.createContext< - number | ((itemId: TreeViewItemId) => number) + ((state: TreeViewState<[UseTreeViewItemsSignature]>, itemId: TreeViewItemId) => number) | number >(() => -1); if (process.env.NODE_ENV !== 'production') { diff --git a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewChildrenItemProvider.tsx b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewChildrenItemProvider.tsx index 11d568f01469..956d468e00e6 100644 --- a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewChildrenItemProvider.tsx +++ b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewChildrenItemProvider.tsx @@ -4,7 +4,7 @@ import { useTreeViewContext } from './useTreeViewContext'; import { escapeOperandAttributeSelector } from '../utils/utils'; import type { UseTreeViewJSXItemsSignature } from '../plugins/useTreeViewJSXItems'; import type { UseTreeViewItemsSignature } from '../plugins/useTreeViewItems'; -import { generateTreeItemIdAttribute } from '../corePlugins/useTreeViewId/useTreeViewId.utils'; +import { selectorItemOrderedChildrenIds } from '../plugins/useTreeViewItems/useTreeViewItems.selectors'; export const TreeViewChildrenItemContext = React.createContext(null); @@ -14,14 +14,15 @@ if (process.env.NODE_ENV !== 'production') { } interface TreeViewChildrenItemProviderProps { - itemId?: string; + itemId: string | null; + idAttribute: string | null; children: React.ReactNode; } export function TreeViewChildrenItemProvider(props: TreeViewChildrenItemProviderProps) { - const { children, itemId = null } = props; + const { children, itemId = null, idAttribute } = props; - const { instance, treeId, rootRef } = + const { instance, store, rootRef } = useTreeViewContext<[UseTreeViewJSXItemsSignature, UseTreeViewItemsSignature]>(); const childrenIdAttrToIdRef = React.useRef>(new Map()); @@ -30,23 +31,8 @@ export function TreeViewChildrenItemProvider(props: TreeViewChildrenItemProvider return; } - let idAttr: string | null = null; - if (itemId == null) { - idAttr = rootRef.current.id; - } else { - // Undefined during 1st render - const itemMeta = instance.getItemMeta(itemId); - if (itemMeta !== undefined) { - idAttr = generateTreeItemIdAttribute({ itemId, treeId, id: itemMeta.idAttribute }); - } - } - - if (idAttr == null) { - return; - } - - const previousChildrenIds = instance.getItemOrderedChildrenIds(itemId ?? null) ?? []; - const escapedIdAttr = escapeOperandAttributeSelector(idAttr); + const previousChildrenIds = selectorItemOrderedChildrenIds(store.value, itemId ?? null) ?? []; + const escapedIdAttr = escapeOperandAttributeSelector(idAttribute ?? rootRef.current.id); const childrenElements = rootRef.current.querySelectorAll( `${itemId == null ? '' : `*[id="${escapedIdAttr}"] `}[role="treeitem"]:not(*[id="${escapedIdAttr}"] [role="treeitem"] [role="treeitem"])`, ); diff --git a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.tsx b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.tsx index 99b4813b2057..d85512736950 100644 --- a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.tsx +++ b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.tsx @@ -15,7 +15,7 @@ export function TreeViewProvider - {value.wrapRoot({ children, instance: value.instance })} + {value.wrapRoot({ children })} ); } diff --git a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.types.ts b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.types.ts index be08aeb5afe4..14a8d0df6b9b 100644 --- a/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.types.ts +++ b/packages/x-tree-view/src/internals/TreeViewProvider/TreeViewProvider.types.ts @@ -8,8 +8,9 @@ import { TreeViewItemPluginResponse, TreeViewPublicAPI, } from '../models'; +import { TreeViewStore } from '../utils/TreeViewStore'; import { TreeViewCorePluginSignatures } from '../corePlugins'; -import type { TreeItemProps } from '../../TreeItem'; +import type { TreeItemProps } from '../../TreeItem/TreeItem.types'; export type TreeViewItemPluginsRunner = ( props: TreeItemProps, @@ -22,9 +23,10 @@ export type TreeViewContextValue< Partial> & { instance: TreeViewInstance; publicAPI: TreeViewPublicAPI; + store: TreeViewStore; rootRef: React.RefObject; wrapItem: TreeItemWrapper; - wrapRoot: TreeRootWrapper; + wrapRoot: TreeRootWrapper; runItemPlugins: TreeViewItemPluginsRunner; }; diff --git a/packages/x-tree-view/src/internals/components/RichTreeViewItems.tsx b/packages/x-tree-view/src/internals/components/RichTreeViewItems.tsx index af1c4abeb5f2..3e39f2fdb86f 100644 --- a/packages/x-tree-view/src/internals/components/RichTreeViewItems.tsx +++ b/packages/x-tree-view/src/internals/components/RichTreeViewItems.tsx @@ -1,9 +1,74 @@ import * as React from 'react'; import useSlotProps from '@mui/utils/useSlotProps'; import { SlotComponentProps } from '@mui/utils'; +import { fastObjectShallowCompare } from '@mui/x-internals/fastObjectShallowCompare'; import { TreeItem, TreeItemProps } from '../../TreeItem'; import { TreeViewItemId } from '../../models'; -import { TreeViewItemToRenderProps } from '../plugins/useTreeViewItems'; +import { useSelector } from '../hooks/useSelector'; +import { + selectorItemMeta, + selectorItemOrderedChildrenIds, +} from '../plugins/useTreeViewItems/useTreeViewItems.selectors'; +import { useTreeViewContext } from '../TreeViewProvider'; + +const RichTreeViewItemsContext = React.createContext< + ((itemId: TreeViewItemId) => React.ReactNode) | null +>(null); + +if (process.env.NODE_ENV !== 'production') { + RichTreeViewItemsContext.displayName = 'RichTreeViewItemsProvider'; +} + +const WrappedTreeItem = React.memo(function WrappedTreeItem({ + itemSlot, + itemSlotProps, + itemId, +}: WrappedTreeItemProps) { + const renderItemForRichTreeView = React.useContext(RichTreeViewItemsContext)!; + const { store } = useTreeViewContext(); + + const itemMeta = useSelector(store, selectorItemMeta, itemId); + const children = useSelector(store, selectorItemOrderedChildrenIds, itemId); + const Item = (itemSlot ?? TreeItem) as React.JSXElementConstructor; + + const { ownerState, ...itemProps } = useSlotProps({ + elementType: Item, + externalSlotProps: itemSlotProps, + additionalProps: { label: itemMeta?.label!, id: itemMeta?.idAttribute!, itemId }, + ownerState: { itemId, label: itemMeta?.label! }, + }); + + return {children?.map(renderItemForRichTreeView)}; +}, fastObjectShallowCompare); + +export function RichTreeViewItems(props: RichTreeViewItemsProps) { + const { slots, slotProps } = props; + const { store } = useTreeViewContext(); + + const itemSlot = slots?.item as React.JSXElementConstructor | undefined; + const itemSlotProps = slotProps?.item; + const items = useSelector(store, selectorItemOrderedChildrenIds, null); + + const renderItem = React.useCallback( + (itemId: TreeViewItemId) => { + return ( + + ); + }, + [itemSlot, itemSlotProps], + ); + + return ( + + {items.map(renderItem)} + + ); +} interface RichTreeViewItemsOwnerState { itemId: TreeViewItemId; @@ -12,7 +77,7 @@ interface RichTreeViewItemsOwnerState { export interface RichTreeViewItemsSlots { /** - * Custom component for the item. + * Custom component to render a Tree Item. * @default TreeItem. */ item?: React.JSXElementConstructor; @@ -23,7 +88,6 @@ export interface RichTreeViewItemsSlotProps { } export interface RichTreeViewItemsProps { - itemsToRender: TreeViewItemToRenderProps[]; /** * Overridable component slots. * @default {} @@ -36,54 +100,7 @@ export interface RichTreeViewItemsProps { slotProps?: RichTreeViewItemsSlotProps; } -function WrappedTreeItem({ - slots, - slotProps, - label, - id, - itemId, - itemsToRender, -}: Pick & - Pick & { - label: string; - isContentHidden?: boolean; - itemsToRender: TreeViewItemToRenderProps[] | undefined; - }) { - const Item = slots?.item ?? TreeItem; - const { ownerState, ...itemProps } = useSlotProps({ - elementType: Item, - externalSlotProps: slotProps?.item, - additionalProps: { itemId, id, label }, - ownerState: { itemId, label }, - }); - - const children = React.useMemo( - () => - itemsToRender ? ( - - ) : null, - [itemsToRender, slots, slotProps], - ); - - return {children}; -} - -export function RichTreeViewItems(props: RichTreeViewItemsProps) { - const { itemsToRender, slots, slotProps } = props; - - return ( - - {itemsToRender.map((item) => ( - - ))} - - ); +interface WrappedTreeItemProps extends Pick { + itemSlot: React.JSXElementConstructor | undefined; + itemSlotProps: SlotComponentProps | undefined; } diff --git a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.selectors.ts b/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.selectors.ts new file mode 100644 index 000000000000..8b96ad677e08 --- /dev/null +++ b/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.selectors.ts @@ -0,0 +1,14 @@ +import { createSelector, TreeViewRootSelector } from '../../utils/selectors'; +import { UseTreeViewIdSignature } from './useTreeViewId.types'; + +const selectorTreeViewIdState: TreeViewRootSelector = (state) => state.id; + +/** + * Get the id attribute of the tree view. + * @param {TreeViewState<[UseTreeViewIdSignature]>} state The state of the tree view. + * @returns {string} The id attribute of the tree view. + */ +export const selectorTreeViewId = createSelector( + selectorTreeViewIdState, + (idState) => idState.treeId, +); diff --git a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.ts b/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.ts index bb059671285c..c6c0489eb589 100644 --- a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.ts +++ b/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.ts @@ -1,16 +1,14 @@ import * as React from 'react'; import { TreeViewPlugin } from '../../models'; import { UseTreeViewIdSignature } from './useTreeViewId.types'; +import { useSelector } from '../../hooks/useSelector'; +import { selectorTreeViewId } from './useTreeViewId.selectors'; import { createTreeViewDefaultId } from './useTreeViewId.utils'; -export const useTreeViewId: TreeViewPlugin = ({ - params, - state, - setState, -}) => { +export const useTreeViewId: TreeViewPlugin = ({ params, store }) => { React.useEffect(() => { - setState((prevState) => { - if (prevState.id.treeId === params.id && prevState.id.treeId !== undefined) { + store.update((prevState) => { + if (params.id === prevState.id.providedTreeId && prevState.id.treeId !== undefined) { return prevState; } @@ -19,17 +17,17 @@ export const useTreeViewId: TreeViewPlugin = ({ id: { ...prevState.id, treeId: params.id ?? createTreeViewDefaultId() }, }; }); - }, [setState, params.id]); + }, [store, params.id]); - const treeId = params.id ?? state.id.treeId; + const treeId = useSelector(store, selectorTreeViewId); + + const pluginContextValue = React.useMemo(() => ({ treeId }), [treeId]); return { getRootProps: () => ({ id: treeId, }), - contextValue: { - treeId, - }, + contextValue: pluginContextValue, }; }; @@ -37,4 +35,4 @@ useTreeViewId.params = { id: true, }; -useTreeViewId.getInitialState = ({ id }) => ({ id: { treeId: id ?? undefined } }); +useTreeViewId.getInitialState = ({ id }) => ({ id: { treeId: undefined, providedTreeId: id } }); diff --git a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.types.ts b/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.types.ts index 7baa508f7097..6704907447de 100644 --- a/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.types.ts +++ b/packages/x-tree-view/src/internals/corePlugins/useTreeViewId/useTreeViewId.types.ts @@ -13,6 +13,7 @@ export type UseTreeViewIdDefaultizedParameters = UseTreeViewIdParameters; export interface UseTreeViewIdState { id: { treeId: string | undefined; + providedTreeId: string | undefined; }; } diff --git a/packages/x-tree-view/src/internals/hooks/useSelector.ts b/packages/x-tree-view/src/internals/hooks/useSelector.ts new file mode 100644 index 000000000000..014efd73b5b1 --- /dev/null +++ b/packages/x-tree-view/src/internals/hooks/useSelector.ts @@ -0,0 +1,27 @@ +import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector'; +import { TreeViewAnyPluginSignature, TreeViewState } from '../models'; +import { TreeViewStore } from '../utils/TreeViewStore'; +import { TreeViewSelector } from '../utils/selectors'; + +const defaultCompare = Object.is; + +export const useSelector = < + TSignatures extends readonly TreeViewAnyPluginSignature[], + TArgs, + TValue, +>( + store: TreeViewStore, + selector: TreeViewSelector, TArgs, TValue>, + args: TArgs = undefined as TArgs, + equals: (a: TValue, b: TValue) => boolean = defaultCompare, +): TValue => { + const selectorWithArgs = (state: TreeViewState) => selector(state, args); + + return useSyncExternalStoreWithSelector( + store.subscribe, + store.getSnapshot, + store.getSnapshot, + selectorWithArgs, + equals, + ); +}; diff --git a/packages/x-tree-view/src/internals/index.ts b/packages/x-tree-view/src/internals/index.ts index 68127e7c85e8..5020622452e0 100644 --- a/packages/x-tree-view/src/internals/index.ts +++ b/packages/x-tree-view/src/internals/index.ts @@ -2,8 +2,13 @@ export { useTreeView } from './useTreeView'; export { TreeViewProvider, useTreeViewContext } from './TreeViewProvider'; export { RichTreeViewItems } from './components/RichTreeViewItems'; +export type { + RichTreeViewItemsSlots, + RichTreeViewItemsSlotProps, +} from './components/RichTreeViewItems'; export { unstable_resetCleanupTracking } from './hooks/useInstanceEventHandler'; +export { useSelector } from './hooks/useSelector'; export type { TreeViewPlugin, @@ -11,10 +16,12 @@ export type { ConvertPluginsIntoSignatures, MergeSignaturesProperty, TreeViewPublicAPI, + TreeViewState, TreeViewExperimentalFeatures, TreeViewItemMeta, TreeViewInstance, TreeViewItemPlugin, + TreeViewUsedStore, } from './models'; // Core plugins @@ -48,6 +55,12 @@ export { buildSiblingIndexes, TREE_VIEW_ROOT_PARENT_ID, } from './plugins/useTreeViewItems'; +export { + selectorItemMetaLookup, + selectorItemMeta, + selectorItemIndex, + selectorItemOrderedChildrenIds, +} from './plugins/useTreeViewItems/useTreeViewItems.selectors'; export type { UseTreeViewItemsSignature, UseTreeViewItemsParameters, @@ -64,4 +77,6 @@ export type { UseTreeViewJSXItemsParameters, } from './plugins/useTreeViewJSXItems'; +export { createSelector } from './utils/selectors'; export { isTargetInDescendants } from './utils/tree'; +export { TreeViewStore } from './utils/TreeViewStore'; diff --git a/packages/x-tree-view/src/internals/models/itemPlugin.ts b/packages/x-tree-view/src/internals/models/itemPlugin.ts index 64a54e5cc549..c41e09a8c473 100644 --- a/packages/x-tree-view/src/internals/models/itemPlugin.ts +++ b/packages/x-tree-view/src/internals/models/itemPlugin.ts @@ -1,6 +1,5 @@ import * as React from 'react'; import { EventHandlers } from '@mui/utils'; -import type { TreeItemProps } from '../../TreeItem'; import type { UseTreeItemContentSlotOwnProps, UseTreeItemDragAndDropOverlaySlotOwnProps, @@ -10,6 +9,7 @@ import type { UseTreeItemStatus, } from '../../useTreeItem'; import type { UseTreeItemInteractions } from '../../hooks/useTreeItemUtils/useTreeItemUtils'; +import type { TreeItemProps } from '../../TreeItem/TreeItem.types'; export interface TreeViewItemPluginSlotPropsEnhancerParams { rootRefObject: React.MutableRefObject; @@ -50,11 +50,11 @@ export interface TreeViewItemPluginResponse { propsEnhancers?: TreeViewItemPluginSlotPropsEnhancers; } -export interface TreeViewItemPluginOptions +export interface TreeViewItemPluginOptions extends Omit { - props: TProps; + props: TreeItemProps; } export type TreeViewItemPlugin = ( - options: TreeViewItemPluginOptions, + options: TreeViewItemPluginOptions, ) => void | TreeViewItemPluginResponse; diff --git a/packages/x-tree-view/src/internals/models/plugin.ts b/packages/x-tree-view/src/internals/models/plugin.ts index df0edd46a7fb..6db17e872fa0 100644 --- a/packages/x-tree-view/src/internals/models/plugin.ts +++ b/packages/x-tree-view/src/internals/models/plugin.ts @@ -6,16 +6,16 @@ import { TreeViewEventLookupElement } from './events'; import type { TreeViewCorePluginSignatures } from '../corePlugins'; import { TreeViewItemPlugin } from './itemPlugin'; import { TreeViewItemId } from '../../models'; +import { TreeViewStore } from '../utils/TreeViewStore'; export interface TreeViewPluginOptions { instance: TreeViewUsedInstance; params: TreeViewUsedDefaultizedParams; - state: TreeViewUsedState; slots: TSignature['slots']; slotProps: TSignature['slotProps']; experimentalFeatures: TreeViewUsedExperimentalFeatures; models: TreeViewUsedModels; - setState: React.Dispatch>>; + store: TreeViewUsedStore; rootRef: React.RefObject; plugins: TreeViewPlugin[]; } @@ -44,6 +44,7 @@ export type TreeViewPluginSignature< publicAPI?: {}; events?: { [key in keyof T['events']]: TreeViewEventLookupElement }; state?: {}; + cache?: {}; contextValue?: {}; slots?: { [key in keyof T['slots']]: React.ElementType }; slotProps?: { [key in keyof T['slotProps']]: {} | (() => {}) }; @@ -59,6 +60,7 @@ export type TreeViewPluginSignature< publicAPI: T extends { publicAPI: {} } ? T['publicAPI'] : {}; events: T extends { events: {} } ? T['events'] : {}; state: T extends { state: {} } ? T['state'] : {}; + cache: T extends { cache: {} } ? T['cache'] : {}; contextValue: T extends { contextValue: {} } ? T['contextValue'] : {}; slots: T extends { slots: {} } ? T['slots'] : {}; slotProps: T extends { slotProps: {} } ? T['slotProps'] : {}; @@ -79,6 +81,7 @@ export type TreeViewPluginSignature< }; export type TreeViewAnyPluginSignature = { + cache: any; state: any; instance: any; params: any; @@ -120,11 +123,15 @@ export type TreeViewUsedInstance $$signature: TSignature; }; -type TreeViewUsedState = - PluginPropertyWithDependencies; +export type TreeViewUsedStore = TreeViewStore< + [TSignature, ...TSignature['dependencies']] +>; type TreeViewUsedExperimentalFeatures = - TreeViewExperimentalFeatures<[TSignature, ...TSignature['dependencies']]>; + TreeViewExperimentalFeatures< + [TSignature, ...TSignature['dependencies']], + TSignature['optionalDependencies'] + >; type RemoveSetValue>> = { [K in keyof Models]: Omit; @@ -141,12 +148,10 @@ export type TreeItemWrapper; + idAttribute: string; }) => React.ReactNode; -export type TreeRootWrapper = (params: { - children: React.ReactNode; - instance: TreeViewInstance; -}) => React.ReactNode; +export type TreeRootWrapper = (params: { children: React.ReactNode }) => React.ReactNode; export type TreeViewPlugin = { (options: TreeViewPluginOptions): TreeViewResponse; @@ -155,6 +160,7 @@ export type TreeViewPlugin = { experimentalFeatures: TreeViewUsedExperimentalFeatures; }) => TSignature['defaultizedParams']; getInitialState?: (params: TreeViewUsedDefaultizedParams) => TSignature['state']; + getInitialCache?: () => TSignature['cache']; models?: TreeViewModelsInitializer; params: Record; itemPlugin?: TreeViewItemPlugin; @@ -169,5 +175,5 @@ export type TreeViewPlugin = { * @param {{ children: React.ReactNode; }} params The params of the root. * @returns {React.ReactNode} The wrapped root. */ - wrapRoot?: TreeRootWrapper<[TSignature, ...TSignature['dependencies']]>; + wrapRoot?: TreeRootWrapper; }; diff --git a/packages/x-tree-view/src/internals/models/treeView.ts b/packages/x-tree-view/src/internals/models/treeView.ts index df139e952924..b2c13afe8da2 100644 --- a/packages/x-tree-view/src/internals/models/treeView.ts +++ b/packages/x-tree-view/src/internals/models/treeView.ts @@ -40,3 +40,13 @@ export type TreeViewExperimentalFeatures< TSignatures extends readonly TreeViewAnyPluginSignature[], TOptionalSignatures extends readonly TreeViewAnyPluginSignature[] = [], > = MergeSignaturesProperty<[...TSignatures, ...TOptionalSignatures], 'experimentalFeatures'>; + +export type TreeViewStateCacheKey = { id: number }; + +export type TreeViewState< + TSignatures extends readonly TreeViewAnyPluginSignature[], + TOptionalSignatures extends readonly TreeViewAnyPluginSignature[] = [], +> = MergeSignaturesProperty<[...TreeViewCorePluginSignatures, ...TSignatures], 'state'> & + Partial> & { + cacheKey: TreeViewStateCacheKey; + }; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.selectors.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.selectors.ts new file mode 100644 index 000000000000..07523fbba84b --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.selectors.ts @@ -0,0 +1,26 @@ +import { createSelector, TreeViewRootSelector } from '../../utils/selectors'; +import { selectorItemMeta } from '../useTreeViewItems/useTreeViewItems.selectors'; +import { UseTreeViewExpansionSignature } from './useTreeViewExpansion.types'; + +const selectorExpansion: TreeViewRootSelector = (state) => + state.expansion; + +/** + * Check if an item is expanded. + * @param {TreeViewState<[UseTreeViewExpansionSignature]>} state The state of the tree view. + * @returns {boolean} `true` if the item is expanded, `false` otherwise. + */ +export const selectorIsItemExpanded = createSelector( + [selectorExpansion, (_, itemId: string) => itemId], + (expansionState, itemId) => expansionState.expandedItemsMap.has(itemId), +); + +/** + * Check if an item is expandable. + * @param {TreeViewState<[UseTreeViewItemsSignature]>} state The state of the tree view. + * @returns {boolean} `true` if the item is expandable, `false` otherwise. + */ +export const selectorIsItemExpandable = createSelector( + [selectorItemMeta], + (itemMeta) => itemMeta?.expandable ?? false, +); diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts index d01813aeab71..985dca5daac1 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts @@ -1,48 +1,49 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; +import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import { TreeViewPlugin } from '../../models'; import { UseTreeViewExpansionSignature } from './useTreeViewExpansion.types'; import { TreeViewItemId } from '../../../models'; +import { selectorIsItemExpandable, selectorIsItemExpanded } from './useTreeViewExpansion.selectors'; +import { createExpandedItemsMap } from './useTreeViewExpansion.utils'; +import { + selectorItemMeta, + selectorItemOrderedChildrenIds, +} from '../useTreeViewItems/useTreeViewItems.selectors'; export const useTreeViewExpansion: TreeViewPlugin = ({ instance, + store, params, models, + experimentalFeatures, }) => { - const expandedItemsMap = React.useMemo(() => { - const temp = new Map(); - models.expandedItems.value.forEach((id) => { - temp.set(id, true); - }); + const isTreeViewEditable = Boolean(params.isItemEditable) && !!experimentalFeatures.labelEditing; - return temp; - }, [models.expandedItems.value]); + useEnhancedEffect(() => { + store.update((prevState) => ({ + ...prevState, + expansion: { + expandedItemsMap: createExpandedItemsMap(models.expandedItems.value), + }, + })); + }, [store, models.expandedItems.value]); const setExpandedItems = (event: React.SyntheticEvent, value: TreeViewItemId[]) => { params.onExpandedItemsChange?.(event, value); models.expandedItems.setControlledValue(value); }; - const isItemExpanded = React.useCallback( - (itemId: string) => expandedItemsMap.has(itemId), - [expandedItemsMap], - ); - - const isItemExpandable = React.useCallback( - (itemId: string) => !!instance.getItemMeta(itemId)?.expandable, - [instance], - ); - const toggleItemExpansion = useEventCallback( (event: React.SyntheticEvent, itemId: TreeViewItemId) => { - const isExpandedBefore = instance.isItemExpanded(itemId); + const isExpandedBefore = selectorIsItemExpanded(store.value, itemId); instance.setItemExpansion(event, itemId, !isExpandedBefore); }, ); const setItemExpansion = useEventCallback( (event: React.SyntheticEvent, itemId: TreeViewItemId, isExpanded: boolean) => { - const isExpandedBefore = instance.isItemExpanded(itemId); + const isExpandedBefore = selectorIsItemExpanded(store.value, itemId); if (isExpandedBefore === isExpanded) { return; } @@ -63,11 +64,16 @@ export const useTreeViewExpansion: TreeViewPlugin ); const expandAllSiblings = (event: React.KeyboardEvent, itemId: TreeViewItemId) => { - const itemMeta = instance.getItemMeta(itemId); - const siblings = instance.getItemOrderedChildrenIds(itemMeta.parentId); + const itemMeta = selectorItemMeta(store.value, itemId); + if (itemMeta == null) { + return; + } + + const siblings = selectorItemOrderedChildrenIds(store.value, itemMeta.parentId); const diff = siblings.filter( - (child) => instance.isItemExpandable(child) && !instance.isItemExpanded(child), + (child) => + selectorIsItemExpandable(store.value, child) && !selectorIsItemExpanded(store.value, child), ); const newExpanded = models.expandedItems.value.concat(diff); @@ -88,29 +94,32 @@ export const useTreeViewExpansion: TreeViewPlugin return params.expansionTrigger; } - if (instance.isTreeViewEditable) { + if (isTreeViewEditable) { return 'iconContainer'; } return 'content'; - }, [params.expansionTrigger, instance.isTreeViewEditable]); + }, [params.expansionTrigger, isTreeViewEditable]); + + const pluginContextValue = React.useMemo( + () => ({ + expansion: { + expansionTrigger, + }, + }), + [expansionTrigger], + ); return { publicAPI: { setItemExpansion, }, instance: { - isItemExpanded, - isItemExpandable, setItemExpansion, toggleItemExpansion, expandAllSiblings, }, - contextValue: { - expansion: { - expansionTrigger, - }, - }, + contextValue: pluginContextValue, }; }; @@ -127,6 +136,14 @@ useTreeViewExpansion.getDefaultizedParams = ({ params }) => ({ defaultExpandedItems: params.defaultExpandedItems ?? DEFAULT_EXPANDED_ITEMS, }); +useTreeViewExpansion.getInitialState = (params) => ({ + expansion: { + expandedItemsMap: createExpandedItemsMap( + params.expandedItems === undefined ? params.defaultExpandedItems : params.expandedItems, + ), + }, +}); + useTreeViewExpansion.params = { expandedItems: true, defaultExpandedItems: true, diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts index dacc10a2ecda..70a75a3cad27 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts @@ -16,20 +16,6 @@ export interface UseTreeViewExpansionPublicAPI { } export interface UseTreeViewExpansionInstance extends UseTreeViewExpansionPublicAPI { - /** - * Check if an item is expanded. - * @param {TreeViewItemId} itemId The id of the item to check. - * @returns {boolean} `true` if the item is expanded, `false` otherwise. - */ - isItemExpanded: (itemId: TreeViewItemId) => boolean; - /** - * Check if an item is expandable. - * Currently, an item is expandable if it has children. - * In the future, the user should be able to flag an item as expandable even if it has no loaded children to support children lazy loading. - * @param {TreeViewItemId} itemId The id of the item to check. - * @returns {boolean} `true` if the item can be expanded, `false` otherwise. - */ - isItemExpandable: (itemId: TreeViewItemId) => boolean; /** * Toggle the current expansion of an item. * If it is expanded, it will be collapsed, and vice versa. @@ -86,6 +72,12 @@ export type UseTreeViewExpansionDefaultizedParameters = DefaultizedProps< 'defaultExpandedItems' >; +export interface UseTreeViewExpansionState { + expansion: { + expandedItemsMap: Map; + }; +} + interface UseTreeViewExpansionContextValue { expansion: Pick; } @@ -96,6 +88,7 @@ export type UseTreeViewExpansionSignature = TreeViewPluginSignature<{ instance: UseTreeViewExpansionInstance; publicAPI: UseTreeViewExpansionPublicAPI; modelNames: 'expandedItems'; + state: UseTreeViewExpansionState; contextValue: UseTreeViewExpansionContextValue; dependencies: [UseTreeViewItemsSignature]; optionalDependencies: [UseTreeViewLabelSignature]; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.utils.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.utils.ts new file mode 100644 index 000000000000..17303c07aeaa --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.utils.ts @@ -0,0 +1,10 @@ +import { TreeViewItemId } from '../../../models'; + +export const createExpandedItemsMap = (expandedItems: string[]) => { + const expandedItemsMap = new Map(); + expandedItems.forEach((id) => { + expandedItemsMap.set(id, true); + }); + + return expandedItemsMap; +}; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.selectors.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.selectors.ts new file mode 100644 index 000000000000..4681cdc793d4 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.selectors.ts @@ -0,0 +1,49 @@ +import { UseTreeViewFocusSignature } from './useTreeViewFocus.types'; +import { createSelector, TreeViewRootSelector } from '../../utils/selectors'; + +const selectorTreeViewFocusState: TreeViewRootSelector = (state) => + state.focus; + +/** + * Get the item that should be sequentially focusable (usually with the Tab key). + * At any point in time, there is a single item that can be sequentially focused in the Tree View. + * This item is the first selected item (that is both visible and navigable), if any, or the first navigable item if no item is selected. + * @param {TreeViewState<[UseTreeViewFocusSignature]>} state The state of the tree view. + * @returns {TreeViewItemId | null} The id of the item that should be sequentially focusable. + */ +export const selectorDefaultFocusableItemId = createSelector( + selectorTreeViewFocusState, + (focus) => focus.defaultFocusableItemId, +); + +/** + * Check if an item is the default focusable item. + * @param {TreeViewState<[UseTreeViewFocusSignature]>} state The state of the tree view. + * @param {TreeViewItemId} itemId The id of the item to check. + * @returns {boolean} `true` if the item is the default focusable item, `false` otherwise. + */ +export const selectorIsItemTheDefaultFocusableItem = createSelector( + [selectorDefaultFocusableItemId, (_, itemId: string) => itemId], + (defaultFocusableItemId, itemId) => defaultFocusableItemId === itemId, +); + +/** + * Get the id of the item that is currently focused. + * @param {TreeViewState<[UseTreeViewFocusSignature]>} state The state of the tree view. + * @returns {TreeViewItemId | null} The id of the item that is currently focused. + */ +export const selectorFocusedItemId = createSelector( + selectorTreeViewFocusState, + (focus) => focus.focusedItemId, +); + +/** + * Check if an item is focused. + * @param {TreeViewState<[UseTreeViewFocusSignature]>} state The state of the tree view. + * @param {TreeViewItemId} itemId The id of the item to check. + * @returns {boolean} `true` if the item is focused, `false` otherwise. + */ +export const selectorIsItemFocused = createSelector( + [selectorFocusedItemId, (_, itemId: string) => itemId], + (focusedItemId, itemId) => focusedItemId === itemId, +); diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.test.tsx index 94cf0c21ea63..779061acf847 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.test.tsx @@ -76,7 +76,7 @@ describeTreeView< expect(view.getItemRoot('3').tabIndex).to.equal(-1); }); - it('should set tabIndex={0} on the first item if the selected item is not visible', () => { + it('should set tabIndex={0} on the first item if selected item is not visible', () => { const view = render({ items: [{ id: '1' }, { id: '2', children: [{ id: '2.1' }] }], selectedItems: '2.1', @@ -86,7 +86,7 @@ describeTreeView< expect(view.getItemRoot('2').tabIndex).to.equal(-1); }); - it('should set tabIndex={0} on the first item if the no selected item is visible', () => { + it('should set tabIndex={0} on the first item if no selected item is visible', () => { const view = render({ items: [{ id: '1' }, { id: '2', children: [{ id: '2.1' }, { id: '2.2' }] }], selectedItems: ['2.1', '2.2'], diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts index 5e86c40374bd..04eabd9802c0 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts @@ -1,67 +1,79 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; +import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import { EventHandlers } from '@mui/utils'; -import ownerDocument from '@mui/utils/ownerDocument'; -import { TreeViewPlugin, TreeViewUsedInstance } from '../../models'; +import { TreeViewPlugin } from '../../models'; import { UseTreeViewFocusSignature } from './useTreeViewFocus.types'; import { useInstanceEventHandler } from '../../hooks/useInstanceEventHandler'; -import { getActiveElement } from '../../utils/utils'; import { getFirstNavigableItem } from '../../utils/tree'; import { TreeViewCancellableEvent } from '../../../models'; import { convertSelectedItemsToArray } from '../useTreeViewSelection/useTreeViewSelection.utils'; - -const useDefaultFocusableItemId = ( - instance: TreeViewUsedInstance, - selectedItems: string | string[] | null, -): string => { - let tabbableItemId = convertSelectedItemsToArray(selectedItems).find((itemId) => { - if (!instance.isItemNavigable(itemId)) { - return false; - } - - const itemMeta = instance.getItemMeta(itemId); - return itemMeta && (itemMeta.parentId == null || instance.isItemExpanded(itemMeta.parentId)); - }); - - if (tabbableItemId == null) { - tabbableItemId = getFirstNavigableItem(instance); - } - - return tabbableItemId; -}; +import { + selectorDefaultFocusableItemId, + selectorFocusedItemId, +} from './useTreeViewFocus.selectors'; +import { selectorIsItemExpanded } from '../useTreeViewExpansion/useTreeViewExpansion.selectors'; +import { + selectorCanItemBeFocused, + selectorItemMeta, +} from '../useTreeViewItems/useTreeViewItems.selectors'; export const useTreeViewFocus: TreeViewPlugin = ({ instance, params, - state, - setState, + store, models, - rootRef, }) => { - const defaultFocusableItemId = useDefaultFocusableItemId(instance, models.selectedItems.value); - - const setFocusedItemId = useEventCallback((itemId: React.SetStateAction) => { - const cleanItemId = typeof itemId === 'function' ? itemId(state.focusedItemId) : itemId; - if (state.focusedItemId !== cleanItemId) { - setState((prevState) => ({ ...prevState, focusedItemId: cleanItemId })); + useEnhancedEffect(() => { + let defaultFocusableItemId = convertSelectedItemsToArray(models.selectedItems.value).find( + (itemId) => { + if (!selectorCanItemBeFocused(store.value, itemId)) { + return false; + } + + const itemMeta = selectorItemMeta(store.value, itemId); + return ( + itemMeta && + (itemMeta.parentId == null || selectorIsItemExpanded(store.value, itemMeta.parentId)) + ); + }, + ); + + if (defaultFocusableItemId == null) { + defaultFocusableItemId = getFirstNavigableItem(store.value) ?? null; } - }); - const isTreeViewFocused = React.useCallback( - () => - !!rootRef.current && - rootRef.current.contains(getActiveElement(ownerDocument(rootRef.current))), - [rootRef], - ); + store.update((prevState) => { + if (defaultFocusableItemId === prevState.focus.defaultFocusableItemId) { + return prevState; + } - const isItemFocused = React.useCallback( - (itemId: string) => state.focusedItemId === itemId && isTreeViewFocused(), - [state.focusedItemId, isTreeViewFocused], - ); + return { + ...prevState, + focus: { + ...prevState.focus, + defaultFocusableItemId, + }, + }; + }); + }, [store, models.selectedItems.value]); + + const setFocusedItemId = useEventCallback((itemId: string | null) => { + const focusedItemId = selectorFocusedItemId(store.value); + if (focusedItemId !== itemId) { + store.update((prevState) => ({ + ...prevState, + focus: { ...prevState.focus, focusedItemId: itemId }, + })); + } + }); const isItemVisible = (itemId: string) => { - const itemMeta = instance.getItemMeta(itemId); - return itemMeta && (itemMeta.parentId == null || instance.isItemExpanded(itemMeta.parentId)); + const itemMeta = selectorItemMeta(store.value, itemId); + return ( + itemMeta && + (itemMeta.parentId == null || selectorIsItemExpanded(store.value, itemMeta.parentId)) + ); }; const innerFocusItem = (event: React.SyntheticEvent | null, itemId: string) => { @@ -85,13 +97,14 @@ export const useTreeViewFocus: TreeViewPlugin = ({ }); const removeFocusedItem = useEventCallback(() => { - if (state.focusedItemId == null) { + const focusedItemId = selectorFocusedItemId(store.value); + if (focusedItemId == null) { return; } - const itemMeta = instance.getItemMeta(state.focusedItemId); + const itemMeta = selectorItemMeta(store.value, focusedItemId); if (itemMeta) { - const itemElement = instance.getItemDOMElement(state.focusedItemId); + const itemElement = instance.getItemDOMElement(focusedItemId); if (itemElement) { itemElement.blur(); } @@ -100,10 +113,10 @@ export const useTreeViewFocus: TreeViewPlugin = ({ setFocusedItemId(null); }); - const canItemBeTabbed = (itemId: string) => itemId === defaultFocusableItemId; - useInstanceEventHandler(instance, 'removeItem', ({ id }) => { - if (state.focusedItemId === id) { + const focusedItemId = selectorFocusedItemId(store.value); + const defaultFocusableItemId = selectorDefaultFocusableItemId(store.value); + if (focusedItemId === id && defaultFocusableItemId != null) { innerFocusItem(null, defaultFocusableItemId); } }); @@ -117,28 +130,41 @@ export const useTreeViewFocus: TreeViewPlugin = ({ } // if the event bubbled (which is React specific) we don't want to steal focus - if (event.target === event.currentTarget) { + const defaultFocusableItemId = selectorDefaultFocusableItemId(store.value); + if (event.target === event.currentTarget && defaultFocusableItemId != null) { innerFocusItem(event, defaultFocusableItemId); } }; + const createRootHandleBlur = + (otherHandlers: EventHandlers) => + (event: React.FocusEvent & TreeViewCancellableEvent) => { + otherHandlers.onBlur?.(event); + if (event.defaultMuiPrevented) { + return; + } + + setFocusedItemId(null); + }; + return { getRootProps: (otherHandlers) => ({ onFocus: createRootHandleFocus(otherHandlers), + onBlur: createRootHandleBlur(otherHandlers), }), publicAPI: { focusItem, }, instance: { - isItemFocused, - canItemBeTabbed, focusItem, removeFocusedItem, }, }; }; -useTreeViewFocus.getInitialState = () => ({ focusedItemId: null }); +useTreeViewFocus.getInitialState = () => ({ + focus: { focusedItemId: null, defaultFocusableItemId: null }, +}); useTreeViewFocus.params = { onItemFocus: true, diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.types.ts index c84b650be8d5..105ad6a36469 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.types.ts @@ -3,7 +3,6 @@ import { TreeViewPluginSignature } from '../../models'; import type { UseTreeViewItemsSignature } from '../useTreeViewItems'; import type { UseTreeViewSelectionSignature } from '../useTreeViewSelection'; import { UseTreeViewExpansionSignature } from '../useTreeViewExpansion'; -import { TreeViewItemId } from '../../../models'; export interface UseTreeViewFocusPublicAPI { /** @@ -18,20 +17,6 @@ export interface UseTreeViewFocusPublicAPI { } export interface UseTreeViewFocusInstance extends UseTreeViewFocusPublicAPI { - /** - * Check if an item is the currently focused item. - * @param {TreeViewItemId} itemId The id of the item to check. - * @returns {boolean} `true` if the item is focused, `false` otherwise. - */ - isItemFocused: (itemId: TreeViewItemId) => boolean; - /** - * Check if an item should be sequentially focusable (usually with the Tab key). - * At any point in time, there is a single item that can be sequentially focused in the Tree View. - * This item is the first selected item (that is both visible and navigable), if any, or the first navigable item if no item is selected. - * @param {TreeViewItemId} itemId The id of the item to check. - * @returns {boolean} `true` if the item can be sequentially focusable, `false` otherwise. - */ - canItemBeTabbed: (itemId: TreeViewItemId) => boolean; /** * Remove the focus from the currently focused item (both from the internal state and the DOM). */ @@ -50,7 +35,10 @@ export interface UseTreeViewFocusParameters { export type UseTreeViewFocusDefaultizedParameters = UseTreeViewFocusParameters; export interface UseTreeViewFocusState { - focusedItemId: string | null; + focus: { + focusedItemId: string | null; + defaultFocusableItemId: string | null; + }; } export type UseTreeViewFocusSignature = TreeViewPluginSignature<{ diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewIcons/useTreeViewIcons.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewIcons/useTreeViewIcons.ts index 73b9d08db50e..4707a169ca72 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewIcons/useTreeViewIcons.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewIcons/useTreeViewIcons.ts @@ -1,3 +1,4 @@ +import * as React from 'react'; import { TreeViewPlugin } from '../../models'; import { UseTreeViewIconsSignature } from './useTreeViewIcons.types'; @@ -5,8 +6,8 @@ export const useTreeViewIcons: TreeViewPlugin = ({ slots, slotProps, }) => { - return { - contextValue: { + const pluginContextValue = React.useMemo( + () => ({ icons: { slots: { collapseIcon: slots.collapseIcon, @@ -19,7 +20,19 @@ export const useTreeViewIcons: TreeViewPlugin = ({ endIcon: slotProps.endIcon, }, }, - }, + }), + [ + slots.collapseIcon, + slots.expandIcon, + slots.endIcon, + slotProps.collapseIcon, + slotProps.expandIcon, + slotProps.endIcon, + ], + ); + + return { + contextValue: pluginContextValue, }; }; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/index.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/index.ts index 2ab1e4963528..b98e02587536 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/index.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/index.ts @@ -4,6 +4,5 @@ export type { UseTreeViewItemsParameters, UseTreeViewItemsDefaultizedParameters, UseTreeViewItemsState, - TreeViewItemToRenderProps, } from './useTreeViewItems.types'; export { buildSiblingIndexes, TREE_VIEW_ROOT_PARENT_ID } from './useTreeViewItems.utils'; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.selectors.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.selectors.ts new file mode 100644 index 000000000000..209e933a27a2 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.selectors.ts @@ -0,0 +1,146 @@ +import { TreeViewItemId } from '../../../models'; +import { TreeViewItemMeta } from '../../models'; +import { createSelector, TreeViewRootSelector } from '../../utils/selectors'; +import { UseTreeViewItemsSignature } from './useTreeViewItems.types'; +import { TREE_VIEW_ROOT_PARENT_ID } from './useTreeViewItems.utils'; + +const selectorTreeViewItemsState: TreeViewRootSelector = (state) => + state.items; + +/** + * Get the meta-information of all items. + * @param {TreeViewState<[UseTreeViewItemsSignature]>} state The state of the tree view. + * @returns {TreeViewItemMetaLookup} The meta-information of all items. + */ +export const selectorItemMetaLookup = createSelector( + selectorTreeViewItemsState, + (items) => items.itemMetaLookup, +); + +const EMPTY_CHILDREN: TreeViewItemId[] = []; + +/** + * Get the ordered children ids of a given item. + * @param {TreeViewState<[UseTreeViewItemsSignature]>} state The state of the tree view. + * @param {TreeViewItemId} itemId The id of the item to get the children of. + * @returns {TreeViewItemId[]} The ordered children ids of the item. + */ +export const selectorItemOrderedChildrenIds = createSelector( + [selectorTreeViewItemsState, (_, itemId: string | null) => itemId], + (itemsState, itemId) => + itemsState.itemOrderedChildrenIdsLookup[itemId ?? TREE_VIEW_ROOT_PARENT_ID] ?? EMPTY_CHILDREN, +); + +/** + * Get the model of an item. + * @param {TreeViewState<[UseTreeViewItemsSignature]>} state The state of the tree view. + * @param {TreeViewItemId} itemId The id of the item to get the model of. + * @returns {R} The model of the item. + */ +export const selectorItemModel = createSelector( + [selectorTreeViewItemsState, (_, itemId: string) => itemId], + (itemsState, itemId) => { + const a = itemsState.itemModelLookup[itemId]; + return a; + }, +); + +/** + * Get the meta-information of an item. + * Check the `TreeViewItemMeta` type for more information. + * @param {TreeViewState<[UseTreeViewItemsSignature]>} + * @param {TreeViewItemId} itemId The id of the item to get the meta-information of. + * @returns {TreeViewItemMeta | null} The meta-information of the item. + */ +export const selectorItemMeta = createSelector( + [selectorItemMetaLookup, (_, itemId: string | null) => itemId], + (itemMetaLookup, itemId) => + (itemMetaLookup[itemId ?? TREE_VIEW_ROOT_PARENT_ID] ?? null) as TreeViewItemMeta | null, +); + +/** + * Check if an item is disabled. + * @param {TreeViewState<[UseTreeViewItemsSignature]>} state The state of the tree view. + * @param {TreeViewItemId} itemId The id of the item to check. + * @returns {boolean} `true` if the item is disabled, `false` otherwise. + */ +export const selectorIsItemDisabled = createSelector( + [selectorItemMetaLookup, (_, itemId: string) => itemId], + (itemMetaLookup, itemId) => { + if (itemId == null) { + return false; + } + + let itemMeta = itemMetaLookup[itemId]; + + // This can be called before the item has been added to the item map. + if (!itemMeta) { + return false; + } + + if (itemMeta.disabled) { + return true; + } + + while (itemMeta.parentId != null) { + itemMeta = itemMetaLookup[itemMeta.parentId]; + if (itemMeta.disabled) { + return true; + } + } + + return false; + }, +); + +/** + * Get the index of an item in its parent's children. + * @param {TreeViewState<[UseTreeViewItemsSignature]>} state The state of the tree view. + * @param {TreeViewItemId} itemId The id of the item to get the index of. + * @returns {number} The index of the item in its parent's children. + */ +export const selectorItemIndex = createSelector( + [selectorTreeViewItemsState, selectorItemMeta], + (itemsState, itemMeta) => { + if (itemMeta == null) { + return -1; + } + + const parentIndexes = + itemsState.itemChildrenIndexesLookup[itemMeta.parentId ?? TREE_VIEW_ROOT_PARENT_ID]; + return parentIndexes[itemMeta.id]; + }, +); + +/** + * Get the id of the parent of an item. + * @param {TreeViewState<[UseTreeViewItemsSignature]>} state The state of the tree view. + * @param {TreeViewItemId} itemId The id of the item to get the parent id of. + * @returns {TreeViewItemId | null} The id of the parent of the item. + */ +export const selectorItemParentId = createSelector( + [selectorItemMeta], + (itemMeta) => itemMeta?.parentId ?? null, +); + +/** + * Get the depth of an item (items at the root level have a depth of 0). + * @param {TreeViewState<[UseTreeViewItemsSignature]>} state The state of the tree view. + * @param {TreeViewItemId} itemId The id of the item to get the depth of. + * @returns {number} The depth of the item. + */ +export const selectorItemDepth = createSelector( + [selectorItemMeta], + (itemMeta) => itemMeta?.depth ?? 0, +); + +export const selectorCanItemBeFocused = createSelector( + [selectorTreeViewItemsState, selectorIsItemDisabled], + (itemsState, isItemDisabled) => { + if (itemsState.disabledItemsFocusable) { + return true; + } + + return !isItemDisabled; + }, +); diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx index 1fda8500521e..1256f01c37e0 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.test.tsx @@ -8,6 +8,7 @@ import { UseTreeViewItemsSignature, UseTreeViewSelectionSignature, } from '@mui/x-tree-view/internals'; +import { TreeItemLabel } from '@mui/x-tree-view/TreeItem'; describeTreeView< [UseTreeViewItemsSignature, UseTreeViewExpansionSignature, UseTreeViewSelectionSignature] @@ -22,16 +23,25 @@ describeTreeView< this.skip(); } - expect(() => - render({ items: [{ id: '1' }, { id: '1' }], withErrorBoundary: true }), - ).toErrorDev([ - ...(treeViewComponentName === 'SimpleTreeView' - ? ['Encountered two children with the same key'] - : []), - 'MUI X: The Tree View component requires all items to have a unique `id` property.', - 'MUI X: The Tree View component requires all items to have a unique `id` property.', - `The above error occurred in the component`, - ]); + if (treeViewComponentName === 'SimpleTreeView') { + expect(() => + render({ items: [{ id: '1' }, { id: '1' }], withErrorBoundary: true }), + ).toErrorDev([ + 'Encountered two children with the same key, `1`', + 'MUI X: The Tree View component requires all items to have a unique `id` property.', + 'MUI X: The Tree View component requires all items to have a unique `id` property.', + `The above error occurred in the component`, + `The above error occurred in the component`, + ]); + } else { + expect(() => + render({ items: [{ id: '1' }, { id: '1' }], withErrorBoundary: true }), + ).toErrorDev([ + 'MUI X: The Tree View component requires all items to have a unique `id` property.', + 'MUI X: The Tree View component requires all items to have a unique `id` property.', + `The above error occurred in the component`, + ]); + } }); it('should be able to use a custom id attribute', function test() { @@ -212,6 +222,99 @@ describeTreeView< }); }); + describe('Memoization (Rich Tree View only)', () => { + it('should not re-render any children when the Tree View re-renders (flat tree)', function test() { + if (!treeViewComponentName.startsWith('RichTreeView')) { + this.skip(); + } + + const spyLabel = spy((props) => ); + const view = render({ + items: Array.from({ length: 10 }, (_, i) => ({ id: i.toString() })), + slotProps: { item: { slots: { label: spyLabel } } }, + }); + + spyLabel.resetHistory(); + + view.setProps({ onClick: () => {} }); + + const renders = spyLabel.getCalls().map((call) => call.args[0].children); + expect(renders).to.deep.equal([]); + }); + + it('should not re-render every children when updating the state on an item (flat tree)', function test() { + if (!treeViewComponentName.startsWith('RichTreeView')) { + this.skip(); + } + + const spyLabel = spy((props) => ); + const view = render({ + items: Array.from({ length: 10 }, (_, i) => ({ id: i.toString() })), + selectedItems: [], + slotProps: { item: { slots: { label: spyLabel } } }, + }); + + spyLabel.resetHistory(); + + view.setProps({ selectedItems: ['1'] }); + + const renders = spyLabel.getCalls().map((call) => call.args[0].children); + + // 2 renders of the 1st item to remove to tabIndex={0} + // 2 renders of the selected item to change its visual state + expect(renders).to.deep.equal(['0', '0', '1', '1']); + }); + + it('should not re-render any children when the Tree View re-renders (nested tree)', function test() { + if (!treeViewComponentName.startsWith('RichTreeView')) { + this.skip(); + } + + const spyLabel = spy((props) => ); + const view = render({ + items: Array.from({ length: 5 }, (_, i) => ({ + id: i.toString(), + children: Array.from({ length: 5 }, (_el, j) => ({ id: `${i}.${j}` })), + })), + slotProps: { item: { slots: { label: spyLabel } } }, + }); + + spyLabel.resetHistory(); + + view.setProps({ onClick: () => {} }); + + const renders = spyLabel.getCalls().map((call) => call.args[0].children); + expect(renders).to.deep.equal([]); + }); + + it('should not re-render every children when updating the state on an item (nested tree)', function test() { + if (!treeViewComponentName.startsWith('RichTreeView')) { + this.skip(); + } + + const spyLabel = spy((props) => ); + const view = render({ + items: Array.from({ length: 5 }, (_, i) => ({ + id: i.toString(), + children: Array.from({ length: 5 }, (_el, j) => ({ id: `${i}.${j}` })), + })), + defaultExpandedItems: Array.from({ length: 5 }, (_, i) => i.toString()), + selectedItems: [], + slotProps: { item: { slots: { label: spyLabel } } }, + }); + + spyLabel.resetHistory(); + + view.setProps({ selectedItems: ['1'] }); + + const renders = spyLabel.getCalls().map((call) => call.args[0].children); + + // 2 renders of the 1st item to remove to tabIndex={0} + // 2 renders of the selected item to change its visual state + expect(renders).to.deep.equal(['0', '0', '1', '1']); + }); + }); + describe('API methods', () => { describe('getItem', () => { // This method is only usable with Rich Tree View components diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx index 83dcd7421083..fe8bd453ab8b 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; import { TreeViewPlugin } from '../../models'; import { UseTreeViewItemsSignature, @@ -6,27 +7,39 @@ import { UseTreeViewItemsState, } from './useTreeViewItems.types'; import { publishTreeViewEvent } from '../../utils/publishTreeViewEvent'; -import { TreeViewBaseItem, TreeViewItemId } from '../../../models'; +import { + TreeViewBaseItem, + TreeViewDefaultItemModelProperties, + TreeViewItemId, +} from '../../../models'; import { buildSiblingIndexes, TREE_VIEW_ROOT_PARENT_ID } from './useTreeViewItems.utils'; import { TreeViewItemDepthContext } from '../../TreeViewItemDepthContext'; +import { + selectorItemMeta, + selectorItemOrderedChildrenIds, + selectorItemModel, + selectorItemDepth, +} from './useTreeViewItems.selectors'; +import { selectorTreeViewId } from '../../corePlugins/useTreeViewId/useTreeViewId.selectors'; import { generateTreeItemIdAttribute } from '../../corePlugins/useTreeViewId/useTreeViewId.utils'; -interface UpdateNodesStateParameters +interface UpdateItemsStateParameters extends Pick< UseTreeViewItemsDefaultizedParameters, - 'items' | 'isItemDisabled' | 'getItemLabel' | 'getItemId' + 'items' | 'isItemDisabled' | 'getItemLabel' | 'getItemId' | 'disabledItemsFocusable' > {} -type State = UseTreeViewItemsState['items']; +type State = UseTreeViewItemsState['items']; const updateItemsState = ({ + disabledItemsFocusable, items, isItemDisabled, getItemLabel, getItemId, -}: UpdateNodesStateParameters): State => { - const itemMetaMap: State['itemMetaMap'] = {}; - const itemMap: State['itemMap'] = {}; - const itemOrderedChildrenIds: State['itemOrderedChildrenIds'] = { +}: UpdateItemsStateParameters): State => { + const itemMetaLookup: State['itemMetaLookup'] = {}; + const itemModelLookup: State['itemModelLookup'] = {}; + const itemOrderedChildrenIdsLookup: State['itemOrderedChildrenIdsLookup'] = { [TREE_VIEW_ROOT_PARENT_ID]: [], }; @@ -44,7 +57,7 @@ const updateItemsState = ({ ); } - if (itemMetaMap[id] != null) { + if (itemMetaLookup[id] != null) { throw new Error( [ 'MUI X: The Tree View component requires all items to have a unique `id` property.', @@ -66,7 +79,7 @@ const updateItemsState = ({ ); } - itemMetaMap[id] = { + itemMetaLookup[id] = { id, label, parentId, @@ -76,120 +89,77 @@ const updateItemsState = ({ depth, }; - itemMap[id] = item; + itemModelLookup[id] = item; const parentIdWithDefault = parentId ?? TREE_VIEW_ROOT_PARENT_ID; - if (!itemOrderedChildrenIds[parentIdWithDefault]) { - itemOrderedChildrenIds[parentIdWithDefault] = []; + if (!itemOrderedChildrenIdsLookup[parentIdWithDefault]) { + itemOrderedChildrenIdsLookup[parentIdWithDefault] = []; } - itemOrderedChildrenIds[parentIdWithDefault].push(id); + itemOrderedChildrenIdsLookup[parentIdWithDefault].push(id); item.children?.forEach((child) => processItem(child, depth + 1, id)); }; items.forEach((item) => processItem(item, 0, null)); - const itemChildrenIndexes: State['itemChildrenIndexes'] = {}; - Object.keys(itemOrderedChildrenIds).forEach((parentId) => { - itemChildrenIndexes[parentId] = buildSiblingIndexes(itemOrderedChildrenIds[parentId]); + const itemChildrenIndexesLookup: State['itemChildrenIndexesLookup'] = {}; + Object.keys(itemOrderedChildrenIdsLookup).forEach((parentId) => { + itemChildrenIndexesLookup[parentId] = buildSiblingIndexes( + itemOrderedChildrenIdsLookup[parentId], + ); }); return { - itemMetaMap, - itemMap, - itemOrderedChildrenIds, - itemChildrenIndexes, + disabledItemsFocusable, + itemMetaLookup, + itemModelLookup, + itemOrderedChildrenIdsLookup, + itemChildrenIndexesLookup, }; }; export const useTreeViewItems: TreeViewPlugin = ({ instance, params, - state, - setState, + store, }) => { - const getItemMeta = React.useCallback( - (itemId: string) => state.items.itemMetaMap[itemId], - [state.items.itemMetaMap], - ); - const getItem = React.useCallback( - (itemId: string) => state.items.itemMap[itemId], - [state.items.itemMap], + (itemId: string) => selectorItemModel(store.value, itemId), + [store], ); const getItemTree = React.useCallback(() => { - const getItemFromItemId = (id: TreeViewItemId): TreeViewBaseItem => { - const { children: oldChildren, ...item } = state.items.itemMap[id]; - const newChildren = state.items.itemOrderedChildrenIds[id]; - if (newChildren) { + const getItemFromItemId = (itemId: TreeViewItemId): TreeViewBaseItem => { + const item = selectorItemModel(store.value, itemId); + const newChildren = selectorItemOrderedChildrenIds(store.value, itemId); + if (newChildren.length > 0) { item.children = newChildren.map(getItemFromItemId); + } else { + delete item.children; } return item; }; - return state.items.itemOrderedChildrenIds[TREE_VIEW_ROOT_PARENT_ID].map(getItemFromItemId); - }, [state.items.itemMap, state.items.itemOrderedChildrenIds]); - - const isItemDisabled = React.useCallback( - (itemId: string | null): itemId is string => { - if (itemId == null) { - return false; - } - - let itemMeta = instance.getItemMeta(itemId); - - // This can be called before the item has been added to the item map. - if (!itemMeta) { - return false; - } - - if (itemMeta.disabled) { - return true; - } - - while (itemMeta.parentId != null) { - itemMeta = instance.getItemMeta(itemMeta.parentId); - if (itemMeta.disabled) { - return true; - } - } - - return false; - }, - [instance], - ); - - const getItemIndex = React.useCallback( - (itemId: string) => { - const parentId = instance.getItemMeta(itemId).parentId ?? TREE_VIEW_ROOT_PARENT_ID; - return state.items.itemChildrenIndexes[parentId][itemId]; - }, - [instance, state.items.itemChildrenIndexes], - ); + return selectorItemOrderedChildrenIds(store.value, null).map(getItemFromItemId); + }, [store]); const getItemOrderedChildrenIds = React.useCallback( - (itemId: string | null) => - state.items.itemOrderedChildrenIds[itemId ?? TREE_VIEW_ROOT_PARENT_ID] ?? [], - [state.items.itemOrderedChildrenIds], + (itemId: string | null) => selectorItemOrderedChildrenIds(store.value, itemId), + [store], ); const getItemDOMElement = (itemId: string) => { - const itemMeta = instance.getItemMeta(itemId); + const itemMeta = selectorItemMeta(store.value, itemId); if (itemMeta == null) { return null; } - return document.getElementById( - generateTreeItemIdAttribute({ treeId: state.id.treeId, itemId, id: itemMeta.idAttribute }), - ); - }; - - const isItemNavigable = (itemId: string) => { - if (params.disabledItemsFocusable) { - return true; - } - return !instance.isItemDisabled(itemId); + const idAttribute = generateTreeItemIdAttribute({ + treeId: selectorTreeViewId(store.value), + itemId, + id: itemMeta.idAttribute, + }); + return document.getElementById(idAttribute); }; const areItemUpdatesPreventedRef = React.useRef(false); @@ -204,16 +174,17 @@ export const useTreeViewItems: TreeViewPlugin = ({ return; } - setState((prevState) => { + store.update((prevState) => { const newState = updateItemsState({ + disabledItemsFocusable: params.disabledItemsFocusable, items: params.items, isItemDisabled: params.isItemDisabled, getItemId: params.getItemId, getItemLabel: params.getItemLabel, }); - Object.values(prevState.items.itemMetaMap).forEach((item) => { - if (!newState.itemMetaMap[item.id]) { + Object.values(prevState.items.itemMetaLookup).forEach((item) => { + if (!newState.itemMetaLookup[item.id]) { publishTreeViewEvent(instance, 'removeItem', { id: item.id }); } }); @@ -222,28 +193,29 @@ export const useTreeViewItems: TreeViewPlugin = ({ }); }, [ instance, - setState, + store, params.items, + params.disabledItemsFocusable, params.isItemDisabled, params.getItemId, params.getItemLabel, ]); - const getItemsToRender = () => { - const getPropsFromItemId = ( - id: TreeViewItemId, - ): ReturnType[number] => { - const item = state.items.itemMetaMap[id]; - return { - label: item.label!, - itemId: item.id, - id: item.idAttribute, - children: state.items.itemOrderedChildrenIds[id]?.map(getPropsFromItemId), - }; - }; + // Wrap `props.onItemClick` with `useEventCallback` to prevent unneeded context updates. + const handleItemClick = useEventCallback((event: React.MouseEvent, itemId: string) => { + if (params.onItemClick) { + params.onItemClick(event, itemId); + } + }); - return state.items.itemOrderedChildrenIds[TREE_VIEW_ROOT_PARENT_ID].map(getPropsFromItemId); - }; + const pluginContextValue = React.useMemo( + () => ({ + items: { + onItemClick: handleItemClick, + }, + }), + [handleItemClick], + ); return { getRootProps: () => ({ @@ -261,29 +233,17 @@ export const useTreeViewItems: TreeViewPlugin = ({ getItemOrderedChildrenIds, }, instance: { - getItemMeta, - getItem, - getItemTree, - getItemsToRender, - getItemIndex, getItemDOMElement, - getItemOrderedChildrenIds, - isItemDisabled, - isItemNavigable, preventItemUpdates, areItemUpdatesPrevented, }, - contextValue: { - items: { - onItemClick: params.onItemClick, - disabledItemsFocusable: params.disabledItemsFocusable, - }, - }, + contextValue: pluginContextValue, }; }; useTreeViewItems.getInitialState = (params) => ({ items: updateItemsState({ + disabledItemsFocusable: params.disabledItemsFocusable, items: params.items, isItemDisabled: params.isItemDisabled, getItemId: params.getItemId, @@ -297,9 +257,9 @@ useTreeViewItems.getDefaultizedParams = ({ params }) => ({ itemChildrenIndentation: params.itemChildrenIndentation ?? '12px', }); -useTreeViewItems.wrapRoot = ({ children, instance }) => { +useTreeViewItems.wrapRoot = ({ children }) => { return ( - instance.getItemMeta(itemId)?.depth ?? 0}> + {children} ); diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts index a27e8913c9bd..65bd886fa1ee 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.types.ts @@ -1,14 +1,11 @@ import * as React from 'react'; import { DefaultizedProps } from '@mui/x-internals/types'; import { TreeViewItemMeta, TreeViewPluginSignature } from '../../models'; -import { TreeViewBaseItem, TreeViewItemId } from '../../../models'; - -export interface TreeViewItemToRenderProps { - label: string; - itemId: string; - id: string | undefined; - children?: TreeViewItemToRenderProps[]; -} +import { + TreeViewBaseItem, + TreeViewDefaultItemModelProperties, + TreeViewItemId, +} from '../../../models'; export interface UseTreeViewItemsPublicAPI { /** @@ -34,46 +31,13 @@ export interface UseTreeViewItemsPublicAPI { getItemOrderedChildrenIds: (itemId: TreeViewItemId | null) => TreeViewItemId[]; /** * Get all the items in the same format as provided by `props.items`. - * @returns {TreeViewItemToRenderProps[]} The items in the tree. + * @returns {TreeViewBaseItem[]} The items in the tree. */ getItemTree: () => TreeViewBaseItem[]; } -export interface UseTreeViewItemsInstance extends UseTreeViewItemsPublicAPI { - /** - * Get the meta-information of an item. - * Check the `TreeViewItemMeta` type for more information. - * @param {TreeViewItemId} itemId The id of the item to get the meta-information of. - * @returns {TreeViewItemMeta} The meta-information of the item. - */ - getItemMeta: (itemId: TreeViewItemId) => TreeViewItemMeta; - /** - * Get the item that should be rendered. - * This method is only used on Rich Tree View components. - * Check the `TreeViewItemToRenderProps` type for more information. - * @returns {TreeViewItemToRenderProps[]} The items to render. - */ - getItemsToRender: () => TreeViewItemToRenderProps[]; - /** - * Check if a given item is disabled. - * An item is disabled if it was marked as disabled or if one of its ancestors is disabled. - * @param {TreeViewItemId} itemId The id of the item to check. - * @returns {boolean} `true` if the item is disabled, `false` otherwise. - */ - isItemDisabled: (itemId: TreeViewItemId) => boolean; - /** - * Check if a given item is navigable (i.e.: if it can be accessed through keyboard navigation). - * An item is navigable if it is not disabled or if the `disabledItemsFocusable` prop is `true`. - * @param {TreeViewItemId} itemId The id of the item to check. - * @returns {boolean} `true` if the item is navigable, `false` otherwise. - */ - isItemNavigable: (itemId: TreeViewItemId) => boolean; - /** - * Get the index of a given item in its parent's children list. - * @param {TreeViewItemId} itemId The id of the item to get the index of. - * @returns {number} The index of the item in its parent's children list. - */ - getItemIndex: (itemId: TreeViewItemId) => number; +export interface UseTreeViewItemsInstance + extends Pick, 'getItemDOMElement'> { /** * Freeze any future update to the state based on the `items` prop. * This is useful when `useTreeViewJSXItems` is used to avoid having conflicting sources of truth. @@ -146,15 +110,18 @@ interface UseTreeViewItemsEventLookup { export interface UseTreeViewItemsState { items: { - itemMetaMap: TreeViewItemMetaMap; - itemMap: TreeViewItemMap; - itemOrderedChildrenIds: { [parentItemId: string]: string[] }; - itemChildrenIndexes: { [parentItemId: string]: { [itemId: string]: number } }; + disabledItemsFocusable: boolean; + itemModelLookup: TreeViewItemModelLookup; + itemMetaLookup: TreeViewItemMetaLookup; + itemOrderedChildrenIdsLookup: { [parentItemId: string]: string[] }; + itemChildrenIndexesLookup: { [parentItemId: string]: { [itemId: string]: number } }; }; } interface UseTreeViewItemsContextValue { - items: Pick, 'disabledItemsFocusable' | 'onItemClick'>; + items: { + onItemClick: (event: React.MouseEvent, itemId: string) => void; + }; } export type UseTreeViewItemsSignature = TreeViewPluginSignature<{ @@ -163,10 +130,10 @@ export type UseTreeViewItemsSignature = TreeViewPluginSignature<{ instance: UseTreeViewItemsInstance; publicAPI: UseTreeViewItemsPublicAPI; events: UseTreeViewItemsEventLookup; - state: UseTreeViewItemsState; + state: UseTreeViewItemsState; contextValue: UseTreeViewItemsContextValue; }>; -export type TreeViewItemMetaMap = { [itemId: string]: TreeViewItemMeta }; +export type TreeViewItemMetaLookup = { [itemId: string]: TreeViewItemMeta }; -export type TreeViewItemMap = { [itemId: string]: R }; +export type TreeViewItemModelLookup = { [itemId: string]: TreeViewBaseItem }; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.tsx index 764f22b72802..347080cea474 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewJSXItems/useTreeViewJSXItems.tsx @@ -16,16 +16,17 @@ import { } from '../useTreeViewItems/useTreeViewItems.utils'; import { TreeViewItemDepthContext } from '../../TreeViewItemDepthContext'; import { generateTreeItemIdAttribute } from '../../corePlugins/useTreeViewId/useTreeViewId.utils'; +import { isItemExpandable } from '../../../hooks/useTreeItemUtils/useTreeItemUtils'; export const useTreeViewJSXItems: TreeViewPlugin = ({ instance, - setState, + store, }) => { instance.preventItemUpdates(); const insertJSXItem = useEventCallback((item: TreeViewItemMeta) => { - setState((prevState) => { - if (prevState.items.itemMetaMap[item.id] != null) { + store.update((prevState) => { + if (prevState.items.itemMetaLookup[item.id] != null) { throw new Error( [ 'MUI X: The Tree View component requires all items to have a unique `id` property.', @@ -39,25 +40,28 @@ export const useTreeViewJSXItems: TreeViewPlugin = ...prevState, items: { ...prevState.items, - itemMetaMap: { ...prevState.items.itemMetaMap, [item.id]: item }, + itemMetaLookup: { ...prevState.items.itemMetaLookup, [item.id]: item }, // For Simple Tree View, we don't have a proper `item` object, so we create a very basic one. - itemMap: { ...prevState.items.itemMap, [item.id]: { id: item.id, label: item.label } }, + itemModelLookup: { + ...prevState.items.itemModelLookup, + [item.id]: { id: item.id, label: item.label ?? '' }, + }, }, }; }); return () => { - setState((prevState) => { - const newItemMetaMap = { ...prevState.items.itemMetaMap }; - const newItemMap = { ...prevState.items.itemMap }; - delete newItemMetaMap[item.id]; - delete newItemMap[item.id]; + store.update((prevState) => { + const newItemMetaLookup = { ...prevState.items.itemMetaLookup }; + const newItemModelLookup = { ...prevState.items.itemModelLookup }; + delete newItemMetaLookup[item.id]; + delete newItemModelLookup[item.id]; return { ...prevState, items: { ...prevState.items, - itemMetaMap: newItemMetaMap, - itemMap: newItemMap, + itemMetaLookup: newItemMetaLookup, + itemModelLookup: newItemModelLookup, }, }; }); @@ -68,16 +72,16 @@ export const useTreeViewJSXItems: TreeViewPlugin = const setJSXItemsOrderedChildrenIds = (parentId: string | null, orderedChildrenIds: string[]) => { const parentIdWithDefault = parentId ?? TREE_VIEW_ROOT_PARENT_ID; - setState((prevState) => ({ + store.update((prevState) => ({ ...prevState, items: { ...prevState.items, - itemOrderedChildrenIds: { - ...prevState.items.itemOrderedChildrenIds, + itemOrderedChildrenIdsLookup: { + ...prevState.items.itemOrderedChildrenIdsLookup, [parentIdWithDefault]: orderedChildrenIds, }, - itemChildrenIndexes: { - ...prevState.items.itemChildrenIndexes, + itemChildrenIndexesLookup: { + ...prevState.items.itemChildrenIndexesLookup, [parentIdWithDefault]: buildSiblingIndexes(orderedChildrenIds), }, }, @@ -108,15 +112,8 @@ export const useTreeViewJSXItems: TreeViewPlugin = }; }; -const isItemExpandable = (reactChildren: React.ReactNode) => { - if (Array.isArray(reactChildren)) { - return reactChildren.length > 0 && reactChildren.some(isItemExpandable); - } - return Boolean(reactChildren); -}; - const useTreeViewJSXItemsItemPlugin: TreeViewItemPlugin = ({ props, rootRef, contentRef }) => { - const { instance, treeId } = useTreeViewContext<[UseTreeViewJSXItemsSignature]>(); + const { instance, store, treeId } = useTreeViewContext<[UseTreeViewJSXItemsSignature]>(); const { children, disabled = false, label, itemId, id } = props; const parentContext = React.useContext(TreeViewChildrenItemContext); @@ -142,10 +139,11 @@ const useTreeViewJSXItemsItemPlugin: TreeViewItemPlugin = ({ props, rootRef, con return () => { unregisterChild(idAttribute); + unregisterChild(idAttribute); }; - }, [registerChild, unregisterChild, itemId, id, treeId]); + }, [store, instance, registerChild, unregisterChild, itemId, id, treeId]); - React.useEffect(() => { + useEnhancedEffect(() => { return instance.insertJSXItem({ id: itemId, idAttribute: id, @@ -173,12 +171,12 @@ const useTreeViewJSXItemsItemPlugin: TreeViewItemPlugin = ({ props, rootRef, con useTreeViewJSXItems.itemPlugin = useTreeViewJSXItemsItemPlugin; -useTreeViewJSXItems.wrapItem = ({ children, itemId }) => { +useTreeViewJSXItems.wrapItem = ({ children, itemId, idAttribute }) => { // eslint-disable-next-line react-hooks/rules-of-hooks const depthContext = React.useContext(TreeViewItemDepthContext); return ( - + {children} @@ -187,7 +185,7 @@ useTreeViewJSXItems.wrapItem = ({ children, itemId }) => { }; useTreeViewJSXItems.wrapRoot = ({ children }) => ( - + {children} ); diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts index e5f587f75571..76cd569b3b7b 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.ts @@ -16,6 +16,21 @@ import { } from './useTreeViewKeyboardNavigation.types'; import { hasPlugin } from '../../utils/plugins'; import { useTreeViewLabel } from '../useTreeViewLabel'; +import { useSelector } from '../../hooks/useSelector'; +import { + selectorItemMetaLookup, + selectorIsItemDisabled, + selectorItemParentId, +} from '../useTreeViewItems/useTreeViewItems.selectors'; +import { + selectorIsItemBeingEdited, + selectorIsItemEditable, +} from '../useTreeViewLabel/useTreeViewLabel.selectors'; +import { selectorIsItemSelected } from '../useTreeViewSelection/useTreeViewSelection.selectors'; +import { + selectorIsItemExpandable, + selectorIsItemExpanded, +} from '../useTreeViewExpansion/useTreeViewExpansion.selectors'; function isPrintableKey(string: string) { return !!string && string.length === 1 && !!string.match(/\S/); @@ -23,7 +38,7 @@ function isPrintableKey(string: string) { export const useTreeViewKeyboardNavigation: TreeViewPlugin< UseTreeViewKeyboardNavigationSignature -> = ({ instance, params, state }) => { +> = ({ instance, store, params }) => { const isRtl = useRtl(); const firstCharMap = React.useRef({}); @@ -33,6 +48,7 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< }, ); + const itemMetaLookup = useSelector(store, selectorItemMetaLookup); React.useEffect(() => { if (instance.areItemUpdatesPrevented()) { return; @@ -44,18 +60,18 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< newFirstCharMap[item.id] = item.label!.substring(0, 1).toLowerCase(); }; - Object.values(state.items.itemMetaMap).forEach(processItem); + Object.values(itemMetaLookup).forEach(processItem); firstCharMap.current = newFirstCharMap; - }, [state.items.itemMetaMap, params.getItemId, instance]); + }, [itemMetaLookup, params.getItemId, instance]); const getFirstMatchingItem = (itemId: string, query: string) => { const cleanQuery = query.toLowerCase(); const getNextItem = (itemIdToCheck: string) => { - const nextItemId = getNextNavigableItem(instance, itemIdToCheck); + const nextItemId = getNextNavigableItem(store.value, itemIdToCheck); // We reached the end of the tree, check from the beginning if (nextItemId === null) { - return getFirstNavigableItem(instance); + return getFirstNavigableItem(store.value); } return nextItemId; @@ -78,10 +94,12 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< }; const canToggleItemSelection = (itemId: string) => - !params.disableSelection && !instance.isItemDisabled(itemId); + !params.disableSelection && !selectorIsItemDisabled(store.value, itemId); const canToggleItemExpansion = (itemId: string) => { - return !instance.isItemDisabled(itemId) && instance.isItemExpandable(itemId); + return ( + !selectorIsItemDisabled(store.value, itemId) && selectorIsItemExpandable(store.value, itemId) + ); }; // ARIA specification: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/#keyboardinteraction @@ -126,8 +144,8 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< case key === 'Enter': { if ( hasPlugin(instance, useTreeViewLabel) && - instance.isItemEditable(itemId) && - !instance.isItemBeingEdited(itemId) + selectorIsItemEditable(store.value, { itemId, isItemEditable: params.isItemEditable! }) && + !selectorIsItemBeingEdited(store.value, itemId) ) { instance.setEditedItemId(itemId); } else if (canToggleItemExpansion(itemId)) { @@ -137,7 +155,7 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< if (params.multiSelect) { event.preventDefault(); instance.selectItem({ event, itemId, keepExistingSelection: true }); - } else if (!instance.isItemSelected(itemId)) { + } else if (!selectorIsItemSelected(store.value, itemId)) { instance.selectItem({ event, itemId }); event.preventDefault(); } @@ -148,7 +166,7 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< // Focus the next focusable item case key === 'ArrowDown': { - const nextItem = getNextNavigableItem(instance, itemId); + const nextItem = getNextNavigableItem(store.value, itemId); if (nextItem) { event.preventDefault(); instance.focusItem(event, nextItem); @@ -165,7 +183,7 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< // Focuses the previous focusable item case key === 'ArrowUp': { - const previousItem = getPreviousNavigableItem(instance, itemId); + const previousItem = getPreviousNavigableItem(store.value, itemId); if (previousItem) { event.preventDefault(); instance.focusItem(event, previousItem); @@ -186,8 +204,8 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< if (ctrlPressed) { return; } - if (instance.isItemExpanded(itemId)) { - const nextItemId = getNextNavigableItem(instance, itemId); + if (selectorIsItemExpanded(store.value, itemId)) { + const nextItemId = getNextNavigableItem(store.value, itemId); if (nextItemId) { instance.focusItem(event, nextItemId); event.preventDefault(); @@ -206,11 +224,11 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< if (ctrlPressed) { return; } - if (canToggleItemExpansion(itemId) && instance.isItemExpanded(itemId)) { + if (canToggleItemExpansion(itemId) && selectorIsItemExpanded(store.value, itemId)) { instance.toggleItemExpansion(event, itemId); event.preventDefault(); } else { - const parent = instance.getItemMeta(itemId).parentId; + const parent = selectorItemParentId(store.value, itemId); if (parent) { instance.focusItem(event, parent); event.preventDefault(); @@ -227,7 +245,7 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< if (canToggleItemSelection(itemId) && params.multiSelect && ctrlPressed && event.shiftKey) { instance.selectRangeFromStartToItem(event, itemId); } else { - instance.focusItem(event, getFirstNavigableItem(instance)); + instance.focusItem(event, getFirstNavigableItem(store.value)); } event.preventDefault(); @@ -241,7 +259,7 @@ export const useTreeViewKeyboardNavigation: TreeViewPlugin< if (canToggleItemSelection(itemId) && params.multiSelect && ctrlPressed && event.shiftKey) { instance.selectRangeFromItemToEnd(event, itemId); } else { - instance.focusItem(event, getLastNavigableItem(instance)); + instance.focusItem(event, getLastNavigableItem(store.value)); } event.preventDefault(); diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.itemPlugin.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.itemPlugin.ts index 7636ad9b39dd..f403056654ca 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.itemPlugin.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.itemPlugin.ts @@ -7,20 +7,26 @@ import { UseTreeItemLabelInputSlotPropsFromLabelEditing, UseTreeViewLabelSignature, } from './useTreeViewLabel.types'; +import { useSelector } from '../../hooks/useSelector'; +import { selectorIsItemBeingEdited, selectorIsItemEditable } from './useTreeViewLabel.selectors'; export const useTreeViewLabelItemPlugin: TreeViewItemPlugin = ({ props }) => { - const { instance } = useTreeViewContext<[UseTreeViewItemsSignature, UseTreeViewLabelSignature]>(); + const { + store, + label: { isItemEditable }, + } = useTreeViewContext<[UseTreeViewItemsSignature, UseTreeViewLabelSignature]>(); const { label, itemId } = props; const [labelInputValue, setLabelInputValue] = React.useState(label as string); - const isItemBeingEdited = instance.isItemBeingEdited(itemId); + const editable = useSelector(store, selectorIsItemEditable, { itemId, isItemEditable }); + const editing = useSelector(store, selectorIsItemBeingEdited, itemId); React.useEffect(() => { - if (!isItemBeingEdited) { + if (!editing) { setLabelInputValue(label as string); } - }, [isItemBeingEdited, label]); + }, [editing, label]); return { propsEnhancers: { @@ -28,8 +34,6 @@ export const useTreeViewLabelItemPlugin: TreeViewItemPlugin = ({ props }) => { externalEventHandlers, interactions, }): UseTreeItemLabelInputSlotPropsFromLabelEditing => { - const editable = instance.isItemEditable(itemId); - if (!editable) { return {}; } diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.selectors.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.selectors.ts new file mode 100644 index 000000000000..43c28f972606 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.selectors.ts @@ -0,0 +1,39 @@ +import { UseTreeViewLabelSignature } from './useTreeViewLabel.types'; +import { createSelector, TreeViewRootSelector } from '../../utils/selectors'; +import { selectorItemModel } from '../useTreeViewItems/useTreeViewItems.selectors'; + +const selectorTreeViewLabelState: TreeViewRootSelector = (state) => + state.label; + +/** + * Check if an item is editable. + * @param {TreeViewState<[UseTreeViewItemsSignature]>} state The state of the tree view. + * @param {object} params The parameters. + * @param {TreeViewItemId} params.itemId The id of the item to check. + * @param {((item: any) => boolean) | boolean} params.isItemEditable The function to determine if an item is editable. + * @returns {boolean} `true` if the item is editable, `false` otherwise. + */ +export const selectorIsItemEditable = createSelector( + [ + (_, args: { itemId: string; isItemEditable: ((item: any) => boolean) | boolean }) => args, + (state, args) => selectorItemModel(state, args.itemId), + ], + (args, itemModel) => { + if (!itemModel || !args.isItemEditable) { + return false; + } + + return typeof args.isItemEditable === 'function' ? args.isItemEditable(itemModel) : true; + }, +); + +/** + * Check if an item is being edited. + * @param {TreeViewState<[UseTreeViewLabelSignature]>} state The state of the tree view. + * @param {TreeViewItemId} itemId The id of the item to check. + * @returns {boolean} `true` if the item is being edited, `false` otherwise. + */ +export const selectorIsItemBeingEdited = createSelector( + [selectorTreeViewLabelState, (_, itemId: string) => itemId], + (labelState, itemId) => labelState.editedItemId === itemId, +); diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.ts index 460b4667674c..51f124b08f85 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.ts @@ -5,37 +5,9 @@ import { TreeViewItemId } from '../../../models'; import { UseTreeViewLabelSignature } from './useTreeViewLabel.types'; import { useTreeViewLabelItemPlugin } from './useTreeViewLabel.itemPlugin'; -export const useTreeViewLabel: TreeViewPlugin = ({ - instance, - state, - setState, - params, -}) => { - const editedItemRef = React.useRef(state.editedItemId); - - const isItemBeingEditedRef = (itemId: TreeViewItemId) => editedItemRef.current === itemId; - +export const useTreeViewLabel: TreeViewPlugin = ({ store, params }) => { const setEditedItemId = (editedItemId: TreeViewItemId | null) => { - setState((prevState) => ({ ...prevState, editedItemId })); - editedItemRef.current = editedItemId; - }; - - const isItemBeingEdited = (itemId: TreeViewItemId) => itemId === state.editedItemId; - - const isTreeViewEditable = Boolean(params.isItemEditable); - - const isItemEditable = (itemId: TreeViewItemId): boolean => { - if (itemId == null || !isTreeViewEditable) { - return false; - } - const item = instance.getItem(itemId); - - if (!item) { - return false; - } - return typeof params.isItemEditable === 'function' - ? params.isItemEditable(item) - : Boolean(params.isItemEditable); + store.update((prevState) => ({ ...prevState, label: { editedItemId } })); }; const updateItemLabel = (itemId: TreeViewItemId, label: string) => { @@ -48,14 +20,14 @@ export const useTreeViewLabel: TreeViewPlugin = ({ ].join('\n'), ); } - setState((prevState) => { - const item = prevState.items.itemMetaMap[itemId]; + store.update((prevState) => { + const item = prevState.items.itemMetaLookup[itemId]; if (item.label !== label) { return { ...prevState, items: { ...prevState.items, - itemMetaMap: { ...prevState.items.itemMetaMap, [itemId]: { ...item, label } }, + itemMetaLookup: { ...prevState.items.itemMetaLookup, [itemId]: { ...item, label } }, }, }; } @@ -68,18 +40,20 @@ export const useTreeViewLabel: TreeViewPlugin = ({ } }; + const pluginContextValue = React.useMemo( + () => ({ label: { isItemEditable: params.isItemEditable } }), + [params.isItemEditable], + ); + return { instance: { setEditedItemId, - isItemBeingEdited, updateItemLabel, - isItemEditable, - isTreeViewEditable, - isItemBeingEditedRef, }, publicAPI: { updateItemLabel, }, + contextValue: pluginContextValue, }; }; @@ -104,7 +78,7 @@ useTreeViewLabel.getDefaultizedParams = ({ params, experimentalFeatures }) => { }; useTreeViewLabel.getInitialState = () => ({ - editedItemId: null, + label: { editedItemId: null }, }); useTreeViewLabel.params = { diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.types.ts index 71b2018f913a..19c293a13996 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.types.ts @@ -20,29 +20,6 @@ export interface UseTreeViewLabelInstance extends UseTreeViewLabelPublicAPI { * @returns {void}. */ setEditedItemId: (itemId: TreeViewItemId | null) => void; - /** - * Checks if an item is being edited or not. - * @param {TreeViewItemId} itemId The id of the item to check. - * @returns {boolean}. - */ - isItemBeingEdited: (itemId: TreeViewItemId) => boolean; - /** - * Checks if an item is being edited or not. - * Purely internal use, used to avoid unnecessarily calling `updateItemLabel` twice. - * @param {TreeViewItemId} itemId The id of the item to check. - * @returns {boolean}. - */ - isItemBeingEditedRef: (itemId: TreeViewItemId) => boolean; - /** - * Determines if a given item is editable. - * @param {TreeViewItemId} itemId The id of the item to check. - * @returns {boolean} `true` if the item is editable. - */ - isItemEditable: (itemId: TreeViewItemId) => boolean; - /** - * Set to `true` if the Tree View is editable. - */ - isTreeViewEditable: boolean; } export interface UseTreeViewLabelParameters { @@ -70,7 +47,13 @@ export type UseTreeViewLabelDefaultizedParameters = DefaultizedPro >; export interface UseTreeViewLabelState { - editedItemId: string | null; + label: { + editedItemId: string | null; + }; +} + +export interface UseTreeViewLabelContextValue { + label: Pick, 'isItemEditable'>; } export type UseTreeViewLabelSignature = TreeViewPluginSignature<{ @@ -79,6 +62,7 @@ export type UseTreeViewLabelSignature = TreeViewPluginSignature<{ publicAPI: UseTreeViewLabelPublicAPI; instance: UseTreeViewLabelInstance; state: UseTreeViewLabelState; + contextValue: UseTreeViewLabelContextValue; experimentalFeatures: 'labelEditing'; dependencies: [UseTreeViewItemsSignature]; }>; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.itemPlugin.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.itemPlugin.ts index c2f7d4978cef..2a7d3f936441 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.itemPlugin.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.itemPlugin.ts @@ -5,21 +5,24 @@ import { TreeViewCancellableEvent, } from '../../../models'; import { useTreeViewContext } from '../../TreeViewProvider'; -import { TreeViewInstance, TreeViewItemPlugin } from '../../models'; +import { TreeViewItemPlugin } from '../../models'; import { UseTreeItemCheckboxSlotPropsFromSelection, UseTreeViewSelectionSignature, } from './useTreeViewSelection.types'; import { UseTreeViewItemsSignature } from '../useTreeViewItems'; +import { selectorItemOrderedChildrenIds } from '../useTreeViewItems/useTreeViewItems.selectors'; +import { selectorIsItemSelected } from './useTreeViewSelection.selectors'; +import { TreeViewStore } from '../../utils/TreeViewStore'; function getCheckboxStatus({ itemId, - instance, + store, selectionPropagation, selected, }: { itemId: TreeViewItemId; - instance: TreeViewInstance<[UseTreeViewItemsSignature, UseTreeViewSelectionSignature]>; + store: TreeViewStore<[UseTreeViewItemsSignature, UseTreeViewSelectionSignature]>; selectionPropagation: TreeViewSelectionPropagation; selected: boolean; }) { @@ -30,7 +33,7 @@ function getCheckboxStatus({ }; } - const children = instance.getItemOrderedChildrenIds(itemId); + const children = selectorItemOrderedChildrenIds(store.value, itemId); if (children.length === 0) { return { indeterminate: false, @@ -43,14 +46,14 @@ function getCheckboxStatus({ const traverseDescendants = (itemToTraverseId: TreeViewItemId) => { if (itemToTraverseId !== itemId) { - if (instance.isItemSelected(itemToTraverseId)) { + if (selectorIsItemSelected(store.value, itemToTraverseId)) { hasSelectedDescendant = true; } else { hasUnSelectedDescendant = true; } } - instance.getItemOrderedChildrenIds(itemToTraverseId).forEach(traverseDescendants); + selectorItemOrderedChildrenIds(store.value, itemToTraverseId).forEach(traverseDescendants); }; traverseDescendants(itemId); @@ -66,7 +69,7 @@ export const useTreeViewSelectionItemPlugin: TreeViewItemPlugin = ({ props }) => const { itemId } = props; const { - instance, + store, selection: { disableSelection, checkboxSelection, selectionPropagation }, } = useTreeViewContext<[UseTreeViewItemsSignature, UseTreeViewSelectionSignature]>(); return { @@ -92,7 +95,7 @@ export const useTreeViewSelectionItemPlugin: TreeViewItemPlugin = ({ props }) => }; const checkboxStatus = getCheckboxStatus({ - instance, + store, itemId, selectionPropagation, selected: status.selected, diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.selectors.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.selectors.ts new file mode 100644 index 000000000000..1fb1f745658e --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.selectors.ts @@ -0,0 +1,16 @@ +import { createSelector, TreeViewRootSelector } from '../../utils/selectors'; +import { UseTreeViewSelectionSignature } from './useTreeViewSelection.types'; + +const selectorTreeViewSelectionState: TreeViewRootSelector = ( + state, +) => state.selection; + +/** + * Check if an item is selected. + * @param {TreeViewState<[UseTreeViewSelectionSignature]>} state The state of the tree view. + * @returns {boolean} `true` if the item is selected, `false` otherwise. + */ +export const selectorIsItemSelected = createSelector( + [selectorTreeViewSelectionState, (_, itemId: string) => itemId], + (selectionState, itemId) => selectionState.selectedItemsMap.has(itemId), +); diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts index e2225d707777..01b740645e87 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import { TreeViewPlugin } from '../../models'; import { TreeViewItemId } from '../../../models'; import { @@ -10,6 +11,7 @@ import { } from '../../utils/tree'; import { UseTreeViewSelectionInstance, + UseTreeViewSelectionParameters, UseTreeViewSelectionSignature, } from './useTreeViewSelection.types'; import { @@ -17,29 +19,27 @@ import { propagateSelection, getAddedAndRemovedItems, getLookupFromArray, + createSelectedItemsMap, } from './useTreeViewSelection.utils'; +import { selectorIsItemSelected } from './useTreeViewSelection.selectors'; import { useTreeViewSelectionItemPlugin } from './useTreeViewSelection.itemPlugin'; export const useTreeViewSelection: TreeViewPlugin = ({ - instance, + store, params, models, }) => { const lastSelectedItem = React.useRef(null); const lastSelectedRange = React.useRef<{ [itemId: string]: boolean }>({}); - const selectedItemsMap = React.useMemo(() => { - const temp = new Map(); - if (Array.isArray(models.selectedItems.value)) { - models.selectedItems.value.forEach((id) => { - temp.set(id, true); - }); - } else if (models.selectedItems.value != null) { - temp.set(models.selectedItems.value, true); - } - - return temp; - }, [models.selectedItems.value]); + useEnhancedEffect(() => { + store.update((prevState) => ({ + ...prevState, + selection: { + selectedItemsMap: createSelectedItemsMap(models.selectedItems.value), + }, + })); + }, [store, models.selectedItems.value]); const setSelectedItems = ( event: React.SyntheticEvent, @@ -53,7 +53,7 @@ export const useTreeViewSelection: TreeViewPlugin (params.selectionPropagation.descendants || params.selectionPropagation.parents) ) { cleanModel = propagateSelection({ - instance, + store, selectionPropagation: params.selectionPropagation, newModel: newModel as string[], oldModel: models.selectedItems.value as string[], @@ -66,7 +66,7 @@ export const useTreeViewSelection: TreeViewPlugin if (params.onItemSelectionToggle) { if (params.multiSelect) { const changes = getAddedAndRemovedItems({ - instance, + store, newModel: cleanModel as string[], oldModel: models.selectedItems.value as string[], }); @@ -97,8 +97,6 @@ export const useTreeViewSelection: TreeViewPlugin models.selectedItems.setControlledValue(cleanModel); }; - const isItemSelected = (itemId: string) => selectedItemsMap.has(itemId); - const selectItem: UseTreeViewSelectionInstance['selectItem'] = ({ event, itemId, @@ -112,7 +110,7 @@ export const useTreeViewSelection: TreeViewPlugin let newSelected: typeof models.selectedItems.value; if (keepExistingSelection) { const cleanSelectedItems = convertSelectedItemsToArray(models.selectedItems.value); - const isSelectedBefore = instance.isItemSelected(itemId); + const isSelectedBefore = selectorIsItemSelected(store.value, itemId); if (isSelectedBefore && (shouldBeSelected === false || shouldBeSelected == null)) { newSelected = cleanSelectedItems.filter((id) => id !== itemId); } else if (!isSelectedBefore && (shouldBeSelected === true || shouldBeSelected == null)) { @@ -124,7 +122,7 @@ export const useTreeViewSelection: TreeViewPlugin // eslint-disable-next-line no-lonely-if if ( shouldBeSelected === false || - (shouldBeSelected == null && instance.isItemSelected(itemId)) + (shouldBeSelected == null && selectorIsItemSelected(store.value, itemId)) ) { newSelected = params.multiSelect ? [] : null; } else { @@ -135,7 +133,7 @@ export const useTreeViewSelection: TreeViewPlugin setSelectedItems( event, newSelected, - // If shouldBeSelected === instance.isItemSelect(itemId), we still want to propagate the select. + // If shouldBeSelected === selectorIsItemSelected(store, itemId), we still want to propagate the select. // This is useful when the element is in an indeterminate state. [itemId], ); @@ -158,7 +156,7 @@ export const useTreeViewSelection: TreeViewPlugin // Add to the model the items that are part of the new range and not already part of the model. const selectedItemsLookup = getLookupFromArray(newSelectedItems); - const range = getNonDisabledItemsInRange(instance, start, end); + const range = getNonDisabledItemsInRange(store.value, start, end); const itemsToAddToModel = range.filter((id) => !selectedItemsLookup[id]); newSelectedItems = newSelectedItems.concat(itemsToAddToModel); @@ -168,17 +166,17 @@ export const useTreeViewSelection: TreeViewPlugin const expandSelectionRange = (event: React.SyntheticEvent, itemId: string) => { if (lastSelectedItem.current != null) { - const [start, end] = findOrderInTremauxTree(instance, itemId, lastSelectedItem.current); + const [start, end] = findOrderInTremauxTree(store.value, itemId, lastSelectedItem.current); selectRange(event, [start, end]); } }; const selectRangeFromStartToItem = (event: React.SyntheticEvent, itemId: string) => { - selectRange(event, [getFirstNavigableItem(instance), itemId]); + selectRange(event, [getFirstNavigableItem(store.value), itemId]); }; const selectRangeFromItemToEnd = (event: React.SyntheticEvent, itemId: string) => { - selectRange(event, [itemId, getLastNavigableItem(instance)]); + selectRange(event, [itemId, getLastNavigableItem(store.value)]); }; const selectAllNavigableItems = (event: React.SyntheticEvent) => { @@ -186,7 +184,7 @@ export const useTreeViewSelection: TreeViewPlugin return; } - const navigableItems = getAllNavigableItems(instance); + const navigableItems = getAllNavigableItems(store.value); setSelectedItems(event, navigableItems); lastSelectedRange.current = getLookupFromArray(navigableItems); @@ -223,6 +221,27 @@ export const useTreeViewSelection: TreeViewPlugin setSelectedItems(event, newSelectedItems); }; + const pluginContextValue = React.useMemo( + () => ({ + selection: { + multiSelect: params.multiSelect, + checkboxSelection: params.checkboxSelection, + disableSelection: params.disableSelection, + selectionPropagation: { + descendants: params.selectionPropagation.descendants, + parents: params.selectionPropagation.parents, + }, + }, + }), + [ + params.multiSelect, + params.checkboxSelection, + params.disableSelection, + params.selectionPropagation.descendants, + params.selectionPropagation.parents, + ], + ); + return { getRootProps: () => ({ 'aria-multiselectable': params.multiSelect, @@ -231,7 +250,6 @@ export const useTreeViewSelection: TreeViewPlugin selectItem, }, instance: { - isItemSelected, selectItem, selectAllNavigableItems, expandSelectionRange, @@ -239,14 +257,7 @@ export const useTreeViewSelection: TreeViewPlugin selectRangeFromItemToEnd, selectItemFromArrowNavigation, }, - contextValue: { - selection: { - multiSelect: params.multiSelect, - checkboxSelection: params.checkboxSelection, - disableSelection: params.disableSelection, - selectionPropagation: params.selectionPropagation, - }, - }, + contextValue: pluginContextValue, }; }; @@ -260,6 +271,8 @@ useTreeViewSelection.models = { const DEFAULT_SELECTED_ITEMS: string[] = []; +const EMPTY_SELECTION_PROPAGATION: UseTreeViewSelectionParameters['selectionPropagation'] = + {}; useTreeViewSelection.getDefaultizedParams = ({ params }) => ({ ...params, disableSelection: params.disableSelection ?? false, @@ -267,7 +280,23 @@ useTreeViewSelection.getDefaultizedParams = ({ params }) => ({ checkboxSelection: params.checkboxSelection ?? false, defaultSelectedItems: params.defaultSelectedItems ?? (params.multiSelect ? DEFAULT_SELECTED_ITEMS : null), - selectionPropagation: params.selectionPropagation ?? {}, + selectionPropagation: params.selectionPropagation ?? EMPTY_SELECTION_PROPAGATION, +}); + +useTreeViewSelection.getInitialState = (params) => ({ + selection: { + selectedItemsMap: createSelectedItemsMap( + params.selectedItems === undefined ? params.defaultSelectedItems : params.selectedItems, + ), + }, +}); + +useTreeViewSelection.getInitialState = (params) => ({ + selection: { + selectedItemsMap: createSelectedItemsMap( + params.selectedItems === undefined ? params.defaultSelectedItems : params.selectedItems, + ), + }, }); useTreeViewSelection.params = { diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts index 78f313320d04..573055dbe75c 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts @@ -23,12 +23,6 @@ export interface UseTreeViewSelectionPublicAPI { } export interface UseTreeViewSelectionInstance extends UseTreeViewSelectionPublicAPI { - /** - * Check if an item is selected. - * @param {TreeViewItemId} itemId The id of the item to check. - * @returns {boolean} `true` if the item is selected, `false` otherwise. - */ - isItemSelected: (itemId: string) => boolean; /** * Select all the navigable items in the tree. * @param {React.SyntheticEvent} event The DOM event that triggered the change. @@ -145,6 +139,12 @@ export type UseTreeViewSelectionDefaultizedParameters | 'selectionPropagation' >; +export interface UseTreeViewSelectionState { + selection: { + selectedItemsMap: Map; + }; +} + interface UseTreeViewSelectionContextValue { selection: Pick< UseTreeViewSelectionDefaultizedParameters, @@ -159,6 +159,7 @@ export type UseTreeViewSelectionSignature = TreeViewPluginSignature<{ publicAPI: UseTreeViewSelectionPublicAPI; contextValue: UseTreeViewSelectionContextValue; modelNames: 'selectedItems'; + state: UseTreeViewSelectionState; dependencies: [ UseTreeViewItemsSignature, UseTreeViewExpansionSignature, diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.ts index 29913562a4b6..ed99d6f9fa75 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.ts @@ -1,7 +1,11 @@ import { TreeViewItemId, TreeViewSelectionPropagation } from '../../../models'; -import { TreeViewInstance } from '../../models'; -import { UseTreeViewItemsSignature } from '../useTreeViewItems'; +import { TreeViewUsedStore } from '../../models'; import { UseTreeViewSelectionSignature } from './useTreeViewSelection.types'; +import { selectorIsItemSelected } from './useTreeViewSelection.selectors'; +import { + selectorItemOrderedChildrenIds, + selectorItemParentId, +} from '../useTreeViewItems/useTreeViewItems.selectors'; /** * Transform the `selectedItems` model to be an array if it was a string or null. @@ -20,6 +24,14 @@ export const convertSelectedItemsToArray = (model: string[] | string | null): st return []; }; +export const createSelectedItemsMap = (selectedItems: string | string[] | null) => { + const selectedItemsMap = new Map(); + convertSelectedItemsToArray(selectedItems).forEach((id) => { + selectedItemsMap.set(id, true); + }); + return selectedItemsMap; +}; + export const getLookupFromArray = (array: string[]) => { const lookup: { [itemId: string]: true } = {}; array.forEach((itemId) => { @@ -29,30 +41,30 @@ export const getLookupFromArray = (array: string[]) => { }; export const getAddedAndRemovedItems = ({ - instance, + store, oldModel, newModel, }: { - instance: TreeViewInstance<[UseTreeViewSelectionSignature]>; + store: TreeViewUsedStore; oldModel: TreeViewItemId[]; newModel: TreeViewItemId[]; }) => { - const newModelLookup = getLookupFromArray(newModel); + const newModelLookup = createSelectedItemsMap(newModel); return { - added: newModel.filter((itemId) => !instance.isItemSelected(itemId)), - removed: oldModel.filter((itemId) => !newModelLookup[itemId]), + added: newModel.filter((itemId) => !selectorIsItemSelected(store.value, itemId)), + removed: oldModel.filter((itemId) => !newModelLookup.has(itemId)), }; }; export const propagateSelection = ({ - instance, + store, selectionPropagation, newModel, oldModel, additionalItemsToPropagate, }: { - instance: TreeViewInstance<[UseTreeViewItemsSignature, UseTreeViewSelectionSignature]>; + store: TreeViewUsedStore; selectionPropagation: TreeViewSelectionPropagation; newModel: TreeViewItemId[]; oldModel: TreeViewItemId[]; @@ -66,7 +78,7 @@ export const propagateSelection = ({ const newModelLookup = getLookupFromArray(newModel); const changes = getAddedAndRemovedItems({ - instance, + store, newModel, oldModel, }); @@ -89,7 +101,7 @@ export const propagateSelection = ({ newModelLookup[itemId] = true; } - instance.getItemOrderedChildrenIds(itemId).forEach(selectDescendants); + selectorItemOrderedChildrenIds(store.value, itemId).forEach(selectDescendants); }; selectDescendants(addedItemId); @@ -101,17 +113,17 @@ export const propagateSelection = ({ return false; } - const children = instance.getItemOrderedChildrenIds(itemId); + const children = selectorItemOrderedChildrenIds(store.value, itemId); return children.every(checkAllDescendantsSelected); }; const selectParents = (itemId: TreeViewItemId) => { - const parentId = instance.getItemMeta(itemId).parentId; + const parentId = selectorItemParentId(store.value, itemId); if (parentId == null) { return; } - const siblings = instance.getItemOrderedChildrenIds(parentId); + const siblings = selectorItemOrderedChildrenIds(store.value, parentId); if (siblings.every(checkAllDescendantsSelected)) { shouldRegenerateModel = true; newModelLookup[parentId] = true; @@ -124,14 +136,14 @@ export const propagateSelection = ({ changes.removed.forEach((removedItemId) => { if (selectionPropagation.parents) { - let parentId = instance.getItemMeta(removedItemId).parentId; + let parentId = selectorItemParentId(store.value, removedItemId); while (parentId != null) { if (newModelLookup[parentId]) { shouldRegenerateModel = true; delete newModelLookup[parentId]; } - parentId = instance.getItemMeta(parentId).parentId; + parentId = selectorItemParentId(store.value, parentId); } } @@ -142,7 +154,7 @@ export const propagateSelection = ({ delete newModelLookup[itemId]; } - instance.getItemOrderedChildrenIds(itemId).forEach(deSelectDescendants); + selectorItemOrderedChildrenIds(store.value, itemId).forEach(deSelectDescendants); }; deSelectDescendants(removedItemId); diff --git a/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts b/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts index 09f41ff67af7..ea0f06caeeeb 100644 --- a/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts +++ b/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts @@ -5,9 +5,9 @@ import { TreeViewAnyPluginSignature, TreeViewInstance, TreeViewPlugin, - MergeSignaturesProperty, TreeViewPublicAPI, ConvertSignaturesIntoPlugins, + TreeViewState, } from '../models'; import { UseTreeViewBaseProps, @@ -19,6 +19,7 @@ import { useTreeViewModels } from './useTreeViewModels'; import { TREE_VIEW_CORE_PLUGINS, TreeViewCorePluginSignatures } from '../corePlugins'; import { extractPluginParamsFromProps } from './extractPluginParamsFromProps'; import { useTreeViewBuildContext } from './useTreeViewBuildContext'; +import { TreeViewStore } from '../utils/TreeViewStore'; export function useTreeViewApiInitialization( inputApiRef: React.MutableRefObject | undefined, @@ -35,6 +36,7 @@ export function useTreeViewApiInitialization( return fallbackPublicApiRef.current; } +let globalId: number = 0; export const useTreeView = < TSignatures extends readonly TreeViewAnyPluginSignature[], TProps extends Partial>, @@ -47,10 +49,14 @@ export const useTreeView = < ...TreeViewCorePluginSignatures, ...TSignatures, ]; - const plugins = [ - ...TREE_VIEW_CORE_PLUGINS, - ...inPlugins, - ] as unknown as ConvertSignaturesIntoPlugins; + const plugins = React.useMemo( + () => + [ + ...TREE_VIEW_CORE_PLUGINS, + ...inPlugins, + ] as unknown as ConvertSignaturesIntoPlugins, + [inPlugins], + ); const { pluginParams, forwardedProps, apiRef, experimentalFeatures, slots, slotProps } = extractPluginParamsFromProps({ @@ -65,27 +71,35 @@ export const useTreeView = < const innerRootRef: React.RefObject = React.useRef(null); const handleRootRef = useForkRef(innerRootRef, rootRef); - const contextValue = useTreeViewBuildContext({ - plugins, - instance, - publicAPI, - rootRef: innerRootRef, - }); + const storeRef = React.useRef | null>(null); + if (storeRef.current == null) { + globalId += 1; + const initialState = { + cacheKey: { id: globalId }, + } as TreeViewState; - const [state, setState] = React.useState(() => { - const temp = {} as MergeSignaturesProperty; plugins.forEach((plugin) => { if (plugin.getInitialState) { - Object.assign(temp, plugin.getInitialState(pluginParams)); + Object.assign(initialState, plugin.getInitialState(pluginParams)); } }); - return temp; + storeRef.current = new TreeViewStore(initialState); + } + + const baseContextValue = useTreeViewBuildContext({ + plugins, + instance, + publicAPI, + store: storeRef.current as TreeViewStore, + rootRef: innerRootRef, }); const rootPropsGetters: (( otherHandlers: TOther, ) => React.HTMLAttributes)[] = []; + + const pluginContextValues: any[] = []; const runPlugin = (plugin: TreeViewPlugin) => { const pluginResponse = plugin({ instance, @@ -93,11 +107,10 @@ export const useTreeView = < slots, slotProps, experimentalFeatures, - state, - setState, rootRef: innerRootRef, models, plugins, + store: storeRef.current as TreeViewStore, }); if (pluginResponse.getRootProps) { @@ -113,7 +126,7 @@ export const useTreeView = < } if (pluginResponse.contextValue) { - Object.assign(contextValue, pluginResponse.contextValue); + pluginContextValues.push(pluginResponse.contextValue); } }; @@ -136,10 +149,15 @@ export const useTreeView = < return rootProps; }; + const contextValue = React.useMemo(() => { + const copiedBaseContextValue = { ...baseContextValue }; + return Object.assign(copiedBaseContextValue, ...pluginContextValues); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [baseContextValue, ...pluginContextValues]); + return { getRootProps, rootRef: handleRootRef, contextValue, - instance, }; }; diff --git a/packages/x-tree-view/src/internals/useTreeView/useTreeView.types.ts b/packages/x-tree-view/src/internals/useTreeView/useTreeView.types.ts index d4ebb91b39c4..f45c98934a0c 100644 --- a/packages/x-tree-view/src/internals/useTreeView/useTreeView.types.ts +++ b/packages/x-tree-view/src/internals/useTreeView/useTreeView.types.ts @@ -5,7 +5,6 @@ import { TreeViewAnyPluginSignature, ConvertSignaturesIntoPlugins, MergeSignaturesProperty, - TreeViewInstance, TreeViewPublicAPI, TreeViewExperimentalFeatures, } from '../models'; @@ -40,5 +39,4 @@ export interface UseTreeViewReturnValue UseTreeViewRootSlotProps; rootRef: React.RefCallback | null; contextValue: TreeViewContextValue; - instance: TreeViewInstance; } diff --git a/packages/x-tree-view/src/internals/useTreeView/useTreeViewBuildContext.ts b/packages/x-tree-view/src/internals/useTreeView/useTreeViewBuildContext.ts index 288a308346ce..d8c75ccbe3f5 100644 --- a/packages/x-tree-view/src/internals/useTreeView/useTreeViewBuildContext.ts +++ b/packages/x-tree-view/src/internals/useTreeView/useTreeViewBuildContext.ts @@ -11,117 +11,140 @@ import { TreeViewItemPluginSlotPropsEnhancerParams, } from '../models'; import { TreeViewCorePluginSignatures } from '../corePlugins'; +import { TreeViewStore } from '../utils/TreeViewStore'; export const useTreeViewBuildContext = ({ plugins, instance, publicAPI, + store, rootRef, }: { plugins: ConvertSignaturesIntoPlugins; instance: TreeViewInstance; publicAPI: TreeViewPublicAPI; + store: TreeViewStore; rootRef: React.RefObject; }): TreeViewContextValue => { - const runItemPlugins: TreeViewItemPluginsRunner = (itemPluginProps) => { - let finalRootRef: React.RefCallback | null = null; - let finalContentRef: React.RefCallback | null = null; - const pluginPropEnhancers: TreeViewItemPluginSlotPropsEnhancers[] = []; - const pluginPropEnhancersNames: { [key in keyof TreeViewItemPluginSlotPropsEnhancers]?: true } = - {}; + const runItemPlugins = React.useCallback( + (itemPluginProps) => { + let finalRootRef: React.RefCallback | null = null; + let finalContentRef: React.RefCallback | null = null; + const pluginPropEnhancers: TreeViewItemPluginSlotPropsEnhancers[] = []; + const pluginPropEnhancersNames: { + [key in keyof TreeViewItemPluginSlotPropsEnhancers]?: true; + } = {}; - plugins.forEach((plugin) => { - if (!plugin.itemPlugin) { - return; - } + plugins.forEach((plugin) => { + if (!plugin.itemPlugin) { + return; + } - const itemPluginResponse = plugin.itemPlugin({ - props: itemPluginProps, - rootRef: finalRootRef, - contentRef: finalContentRef, - }); - if (itemPluginResponse?.rootRef) { - finalRootRef = itemPluginResponse.rootRef; - } - if (itemPluginResponse?.contentRef) { - finalContentRef = itemPluginResponse.contentRef; - } - if (itemPluginResponse?.propsEnhancers) { - pluginPropEnhancers.push(itemPluginResponse.propsEnhancers); - - // Prepare a list of all the slots which are enhanced by at least one plugin - Object.keys(itemPluginResponse.propsEnhancers).forEach((propsEnhancerName) => { - pluginPropEnhancersNames[ - propsEnhancerName as keyof TreeViewItemPluginSlotPropsEnhancers - ] = true; + const itemPluginResponse = plugin.itemPlugin({ + props: itemPluginProps, + rootRef: finalRootRef, + contentRef: finalContentRef, }); - } - }); + if (itemPluginResponse?.rootRef) { + finalRootRef = itemPluginResponse.rootRef; + } + if (itemPluginResponse?.contentRef) { + finalContentRef = itemPluginResponse.contentRef; + } + if (itemPluginResponse?.propsEnhancers) { + pluginPropEnhancers.push(itemPluginResponse.propsEnhancers); - const resolvePropsEnhancer = - (currentSlotName: keyof TreeViewItemPluginSlotPropsEnhancers) => - (currentSlotParams: TreeViewItemPluginSlotPropsEnhancerParams) => { - const enhancedProps = {}; - pluginPropEnhancers.forEach((propsEnhancersForCurrentPlugin) => { - const propsEnhancerForCurrentPluginAndSlot = - propsEnhancersForCurrentPlugin[currentSlotName]; - if (propsEnhancerForCurrentPluginAndSlot != null) { - Object.assign(enhancedProps, propsEnhancerForCurrentPluginAndSlot(currentSlotParams)); - } - }); + // Prepare a list of all the slots which are enhanced by at least one plugin + Object.keys(itemPluginResponse.propsEnhancers).forEach((propsEnhancerName) => { + pluginPropEnhancersNames[ + propsEnhancerName as keyof TreeViewItemPluginSlotPropsEnhancers + ] = true; + }); + } + }); - return enhancedProps; - }; + const resolvePropsEnhancer = + (currentSlotName: keyof TreeViewItemPluginSlotPropsEnhancers) => + (currentSlotParams: TreeViewItemPluginSlotPropsEnhancerParams) => { + const enhancedProps = {}; + pluginPropEnhancers.forEach((propsEnhancersForCurrentPlugin) => { + const propsEnhancerForCurrentPluginAndSlot = + propsEnhancersForCurrentPlugin[currentSlotName]; + if (propsEnhancerForCurrentPluginAndSlot != null) { + Object.assign(enhancedProps, propsEnhancerForCurrentPluginAndSlot(currentSlotParams)); + } + }); - const propsEnhancers = Object.fromEntries( - Object.keys(pluginPropEnhancersNames).map( - (propEnhancerName) => - [ - propEnhancerName, - resolvePropsEnhancer(propEnhancerName as keyof TreeViewItemPluginSlotPropsEnhancers), - ] as const, - ), - ); + return enhancedProps; + }; - return { - contentRef: finalContentRef, - rootRef: finalRootRef, - propsEnhancers, - }; - }; + const propsEnhancers = Object.fromEntries( + Object.keys(pluginPropEnhancersNames).map( + (propEnhancerName) => + [ + propEnhancerName, + resolvePropsEnhancer(propEnhancerName as keyof TreeViewItemPluginSlotPropsEnhancers), + ] as const, + ), + ); + + return { + contentRef: finalContentRef, + rootRef: finalRootRef, + propsEnhancers, + }; + }, + [plugins], + ); - const wrapItem: TreeItemWrapper = ({ itemId, children }) => { - let finalChildren: React.ReactNode = children; - // The wrappers are reversed to ensure that the first wrapper is the outermost one. - for (let i = plugins.length - 1; i >= 0; i -= 1) { - const plugin = plugins[i]; - if (plugin.wrapItem) { - finalChildren = plugin.wrapItem({ itemId, children: finalChildren, instance }); + const wrapItem = React.useCallback>( + ({ itemId, children, idAttribute }) => { + let finalChildren: React.ReactNode = children; + // The wrappers are reversed to ensure that the first wrapper is the outermost one. + for (let i = plugins.length - 1; i >= 0; i -= 1) { + const plugin = plugins[i]; + if (plugin.wrapItem) { + finalChildren = plugin.wrapItem({ + instance, + itemId, + children: finalChildren, + idAttribute, + }); + } } - } - return finalChildren; - }; + return finalChildren; + }, + [plugins, instance], + ); - const wrapRoot: TreeRootWrapper = ({ children }) => { - let finalChildren: React.ReactNode = children; - // The wrappers are reversed to ensure that the first wrapper is the outermost one. - for (let i = plugins.length - 1; i >= 0; i -= 1) { - const plugin = plugins[i]; - if (plugin.wrapRoot) { - finalChildren = plugin.wrapRoot({ children: finalChildren, instance }); + const wrapRoot = React.useCallback( + ({ children }) => { + let finalChildren: React.ReactNode = children; + // The wrappers are reversed to ensure that the first wrapper is the outermost one. + for (let i = plugins.length - 1; i >= 0; i -= 1) { + const plugin = plugins[i]; + if (plugin.wrapRoot) { + finalChildren = plugin.wrapRoot({ + children: finalChildren, + }); + } } - } - return finalChildren; - }; + return finalChildren; + }, + [plugins], + ); - return { - runItemPlugins, - wrapItem, - wrapRoot, - instance, - rootRef, - publicAPI, - } as TreeViewContextValue; + return React.useMemo(() => { + return { + runItemPlugins, + wrapItem, + wrapRoot, + instance, + publicAPI, + store, + rootRef, + } as TreeViewContextValue; + }, [runItemPlugins, wrapItem, wrapRoot, instance, publicAPI, store, rootRef]); }; diff --git a/packages/x-tree-view/src/internals/utils/TreeViewStore.ts b/packages/x-tree-view/src/internals/utils/TreeViewStore.ts new file mode 100644 index 000000000000..cf36a1bc6f8f --- /dev/null +++ b/packages/x-tree-view/src/internals/utils/TreeViewStore.ts @@ -0,0 +1,37 @@ +import type { TreeViewAnyPluginSignature, TreeViewState } from '../models'; + +type Listener = (value: T) => void; + +export type StoreUpdater = ( + prevState: TreeViewState, +) => TreeViewState; + +export class TreeViewStore { + public value: TreeViewState; + + private listeners: Set>>; + + constructor(value: TreeViewState) { + this.value = value; + this.listeners = new Set(); + } + + public subscribe = (fn: Listener>) => { + this.listeners.add(fn); + return () => { + this.listeners.delete(fn); + }; + }; + + public getSnapshot = () => { + return this.value; + }; + + public update = (updater: StoreUpdater) => { + const newState = updater(this.value); + if (newState !== this.value) { + this.value = newState; + this.listeners.forEach((l) => l(newState)); + } + }; +} diff --git a/packages/x-tree-view/src/internals/utils/selectors.ts b/packages/x-tree-view/src/internals/utils/selectors.ts new file mode 100644 index 000000000000..5c76cb78fc53 --- /dev/null +++ b/packages/x-tree-view/src/internals/utils/selectors.ts @@ -0,0 +1,54 @@ +import { lruMemoize, createSelectorCreator, CreateSelectorFunction } from 'reselect'; +import { TreeViewAnyPluginSignature, TreeViewState, TreeViewStateCacheKey } from '../models'; + +const reselectCreateSelector = createSelectorCreator({ + memoize: lruMemoize, + memoizeOptions: { + maxSize: 1, + equalityCheck: Object.is, + }, +}); + +const cache = new WeakMap< + TreeViewStateCacheKey, + Map, any> +>(); + +export type TreeViewRootSelector = < + TSignatures extends [TSignature], +>( + state: TreeViewState, +) => TSignature['state'][keyof TSignature['state']]; + +export type TreeViewSelector = (state: TState, args: TArgs) => TResult; + +/** + * Method wrapping reselect's createSelector to provide caching for tree view instances. + * + */ +export const createSelector = ((...createSelectorArgs: any) => { + const selector: TreeViewSelector, any, any> = (state, selectorArgs) => { + const cacheKey = state.cacheKey; + + // If there is no cache for the current tree view instance, create one. + let cacheForCurrentTreeViewInstance = cache.get(cacheKey); + if (!cacheForCurrentTreeViewInstance) { + cacheForCurrentTreeViewInstance = new Map(); + cache.set(cacheKey, cacheForCurrentTreeViewInstance); + } + + // If there is a cached selector, execute it. + const cachedSelector = cacheForCurrentTreeViewInstance.get(createSelectorArgs); + if (cachedSelector) { + return cachedSelector(state, selectorArgs); + } + + // Otherwise, create a new selector and cache it and execute it. + const fn = reselectCreateSelector(...createSelectorArgs); + cacheForCurrentTreeViewInstance.set(createSelectorArgs, fn); + + return fn(state, selectorArgs); + }; + + return selector; +}) as unknown as CreateSelectorFunction; diff --git a/packages/x-tree-view/src/internals/utils/tree.ts b/packages/x-tree-view/src/internals/utils/tree.ts index deacfe7bbb71..60ef4b3215fb 100644 --- a/packages/x-tree-view/src/internals/utils/tree.ts +++ b/packages/x-tree-view/src/internals/utils/tree.ts @@ -1,14 +1,26 @@ -import { TreeViewInstance } from '../models'; +import { TreeViewItemMeta, TreeViewState } from '../models'; import type { UseTreeViewExpansionSignature } from '../plugins/useTreeViewExpansion'; +import { + selectorIsItemExpandable, + selectorIsItemExpanded, +} from '../plugins/useTreeViewExpansion/useTreeViewExpansion.selectors'; import type { UseTreeViewItemsSignature } from '../plugins/useTreeViewItems'; +import { + selectorCanItemBeFocused, + selectorIsItemDisabled, + selectorItemIndex, + selectorItemMeta, + selectorItemOrderedChildrenIds, + selectorItemParentId, +} from '../plugins/useTreeViewItems/useTreeViewItems.selectors'; const getLastNavigableItemInArray = ( - instance: TreeViewInstance<[UseTreeViewItemsSignature]>, + state: TreeViewState<[UseTreeViewItemsSignature]>, items: string[], ) => { // Equivalent to Array.prototype.findLastIndex let itemIndex = items.length - 1; - while (itemIndex >= 0 && !instance.isItemNavigable(items[itemIndex])) { + while (itemIndex >= 0 && !selectorCanItemBeFocused(state, items[itemIndex])) { itemIndex -= 1; } @@ -20,12 +32,16 @@ const getLastNavigableItemInArray = ( }; export const getPreviousNavigableItem = ( - instance: TreeViewInstance<[UseTreeViewItemsSignature, UseTreeViewExpansionSignature]>, + state: TreeViewState<[UseTreeViewItemsSignature, UseTreeViewExpansionSignature]>, itemId: string, ): string | null => { - const itemMeta = instance.getItemMeta(itemId); - const siblings = instance.getItemOrderedChildrenIds(itemMeta.parentId); - const itemIndex = instance.getItemIndex(itemId); + const itemMeta = selectorItemMeta(state, itemId); + if (!itemMeta) { + return null; + } + + const siblings = selectorItemOrderedChildrenIds(state, itemMeta.parentId); + const itemIndex = selectorItemIndex(state, itemId); // TODO: What should we do if the parent is not navigable? if (itemIndex === 0) { @@ -35,7 +51,7 @@ export const getPreviousNavigableItem = ( // Finds the previous navigable sibling. let previousNavigableSiblingIndex = itemIndex - 1; while ( - !instance.isItemNavigable(siblings[previousNavigableSiblingIndex]) && + !selectorCanItemBeFocused(state, siblings[previousNavigableSiblingIndex]) && previousNavigableSiblingIndex >= 0 ) { previousNavigableSiblingIndex -= 1; @@ -48,73 +64,73 @@ export const getPreviousNavigableItem = ( } // Otherwise, we can try to go up a level and find the previous navigable item. - return getPreviousNavigableItem(instance, itemMeta.parentId); + return getPreviousNavigableItem(state, itemMeta.parentId); } // Finds the last navigable ancestor of the previous navigable sibling. let currentItemId: string = siblings[previousNavigableSiblingIndex]; let lastNavigableChild = getLastNavigableItemInArray( - instance, - instance.getItemOrderedChildrenIds(currentItemId), + state, + selectorItemOrderedChildrenIds(state, currentItemId), ); - while (instance.isItemExpanded(currentItemId) && lastNavigableChild != null) { + while (selectorIsItemExpanded(state, currentItemId) && lastNavigableChild != null) { currentItemId = lastNavigableChild; - lastNavigableChild = instance - .getItemOrderedChildrenIds(currentItemId) - .find(instance.isItemNavigable); + lastNavigableChild = selectorItemOrderedChildrenIds(state, currentItemId).find((childId) => + selectorCanItemBeFocused(state, childId), + ); } return currentItemId; }; export const getNextNavigableItem = ( - instance: TreeViewInstance<[UseTreeViewExpansionSignature, UseTreeViewItemsSignature]>, + state: TreeViewState<[UseTreeViewItemsSignature, UseTreeViewExpansionSignature]>, itemId: string, ) => { // If the item is expanded and has some navigable children, return the first of them. - if (instance.isItemExpanded(itemId)) { - const firstNavigableChild = instance - .getItemOrderedChildrenIds(itemId) - .find(instance.isItemNavigable); + if (selectorIsItemExpanded(state, itemId)) { + const firstNavigableChild = selectorItemOrderedChildrenIds(state, itemId).find((childId) => + selectorCanItemBeFocused(state, childId), + ); if (firstNavigableChild != null) { return firstNavigableChild; } } - let itemMeta = instance.getItemMeta(itemId); + let itemMeta = selectorItemMeta(state, itemId); while (itemMeta != null) { // Try to find the first navigable sibling after the current item. - const siblings = instance.getItemOrderedChildrenIds(itemMeta.parentId); - const currentItemIndex = instance.getItemIndex(itemMeta.id); + const siblings = selectorItemOrderedChildrenIds(state, itemMeta.parentId); + const currentItemIndex = selectorItemIndex(state, itemMeta.id); if (currentItemIndex < siblings.length - 1) { let nextItemIndex = currentItemIndex + 1; while ( - !instance.isItemNavigable(siblings[nextItemIndex]) && + !selectorCanItemBeFocused(state, siblings[nextItemIndex]) && nextItemIndex < siblings.length - 1 ) { nextItemIndex += 1; } - if (instance.isItemNavigable(siblings[nextItemIndex])) { + if (selectorCanItemBeFocused(state, siblings[nextItemIndex])) { return siblings[nextItemIndex]; } } // If the sibling does not exist, go up a level to the parent and try again. - itemMeta = instance.getItemMeta(itemMeta.parentId!); + itemMeta = selectorItemMeta(state, itemMeta.parentId!); } return null; }; export const getLastNavigableItem = ( - instance: TreeViewInstance<[UseTreeViewExpansionSignature, UseTreeViewItemsSignature]>, + state: TreeViewState<[UseTreeViewExpansionSignature, UseTreeViewItemsSignature]>, ) => { let itemId: string | null = null; - while (itemId == null || instance.isItemExpanded(itemId)) { - const children = instance.getItemOrderedChildrenIds(itemId); - const lastNavigableChild = getLastNavigableItemInArray(instance, children); + while (itemId == null || selectorIsItemExpanded(state, itemId)) { + const children = selectorItemOrderedChildrenIds(state, itemId); + const lastNavigableChild = getLastNavigableItemInArray(state, children); // The item has no navigable children. if (lastNavigableChild == null) { @@ -127,8 +143,12 @@ export const getLastNavigableItem = ( return itemId!; }; -export const getFirstNavigableItem = (instance: TreeViewInstance<[UseTreeViewItemsSignature]>) => - instance.getItemOrderedChildrenIds(null).find(instance.isItemNavigable)!; +export const getFirstNavigableItem = ( + state: TreeViewState<[UseTreeViewExpansionSignature, UseTreeViewItemsSignature]>, +) => + selectorItemOrderedChildrenIds(state, null).find((itemId) => + selectorCanItemBeFocused(state, itemId), + )!; /** * This is used to determine the start and end of a selection range so @@ -145,7 +165,7 @@ export const getFirstNavigableItem = (instance: TreeViewInstance<[UseTreeViewIte * https://en.wikipedia.org/wiki/Tr%C3%A9maux_tree */ export const findOrderInTremauxTree = ( - instance: TreeViewInstance<[UseTreeViewItemsSignature]>, + state: TreeViewState<[UseTreeViewExpansionSignature, UseTreeViewItemsSignature]>, itemAId: string, itemBId: string, ) => { @@ -153,8 +173,12 @@ export const findOrderInTremauxTree = ( return [itemAId, itemBId]; } - const itemMetaA = instance.getItemMeta(itemAId); - const itemMetaB = instance.getItemMeta(itemBId); + const itemMetaA = selectorItemMeta(state, itemAId); + const itemMetaB = selectorItemMeta(state, itemBId); + + if (!itemMetaA || !itemMetaB) { + return [itemAId, itemBId]; + } if (itemMetaA.parentId === itemMetaB.id || itemMetaB.parentId === itemMetaA.id) { return itemMetaB.parentId === itemMetaA.id @@ -180,7 +204,7 @@ export const findOrderInTremauxTree = ( aAncestorIsCommon = bFamily.indexOf(aAncestor) !== -1; continueA = aAncestor !== null; if (!aAncestorIsCommon && continueA) { - aAncestor = instance.getItemMeta(aAncestor!).parentId; + aAncestor = selectorItemParentId(state, aAncestor!); } } @@ -189,13 +213,13 @@ export const findOrderInTremauxTree = ( bAncestorIsCommon = aFamily.indexOf(bAncestor) !== -1; continueB = bAncestor !== null; if (!bAncestorIsCommon && continueB) { - bAncestor = instance.getItemMeta(bAncestor!).parentId; + bAncestor = selectorItemParentId(state, bAncestor!); } } } const commonAncestor = aAncestorIsCommon ? aAncestor : bAncestor; - const ancestorFamily = instance.getItemOrderedChildrenIds(commonAncestor); + const ancestorFamily = selectorItemOrderedChildrenIds(state, commonAncestor); const aSide = aFamily[aFamily.indexOf(commonAncestor) - 1]; const bSide = bFamily[bFamily.indexOf(commonAncestor) - 1]; @@ -206,40 +230,40 @@ export const findOrderInTremauxTree = ( }; export const getNonDisabledItemsInRange = ( - instance: TreeViewInstance<[UseTreeViewItemsSignature, UseTreeViewExpansionSignature]>, + state: TreeViewState<[UseTreeViewItemsSignature, UseTreeViewExpansionSignature]>, itemAId: string, itemBId: string, ) => { const getNextItem = (itemId: string) => { // If the item is expanded and has some children, return the first of them. - if (instance.isItemExpandable(itemId) && instance.isItemExpanded(itemId)) { - return instance.getItemOrderedChildrenIds(itemId)[0]; + if (selectorIsItemExpandable(state, itemId) && selectorIsItemExpanded(state, itemId)) { + return selectorItemOrderedChildrenIds(state, itemId)[0]; } - let itemMeta = instance.getItemMeta(itemId); + let itemMeta: TreeViewItemMeta | null = selectorItemMeta(state, itemId); while (itemMeta != null) { // Try to find the first navigable sibling after the current item. - const siblings = instance.getItemOrderedChildrenIds(itemMeta.parentId); - const currentItemIndex = instance.getItemIndex(itemMeta.id); + const siblings = selectorItemOrderedChildrenIds(state, itemMeta.parentId); + const currentItemIndex = selectorItemIndex(state, itemMeta.id); if (currentItemIndex < siblings.length - 1) { return siblings[currentItemIndex + 1]; } // If the item is the last of its siblings, go up a level to the parent and try again. - itemMeta = instance.getItemMeta(itemMeta.parentId!); + itemMeta = itemMeta.parentId ? selectorItemMeta(state, itemMeta.parentId) : null; } throw new Error('Invalid range'); }; - const [first, last] = findOrderInTremauxTree(instance, itemAId, itemBId); + const [first, last] = findOrderInTremauxTree(state, itemAId, itemBId); const items = [first]; let current = first; while (current !== last) { current = getNextItem(current); - if (!instance.isItemDisabled(current)) { + if (!selectorIsItemDisabled(state, current)) { items.push(current); } } @@ -248,13 +272,13 @@ export const getNonDisabledItemsInRange = ( }; export const getAllNavigableItems = ( - instance: TreeViewInstance<[UseTreeViewItemsSignature, UseTreeViewExpansionSignature]>, + state: TreeViewState<[UseTreeViewItemsSignature, UseTreeViewExpansionSignature]>, ) => { - let item: string | null = getFirstNavigableItem(instance); + let item: string | null = getFirstNavigableItem(state); const navigableItems: string[] = []; while (item != null) { navigableItems.push(item); - item = getNextNavigableItem(instance, item); + item = getNextNavigableItem(state, item); } return navigableItems; diff --git a/packages/x-tree-view/src/models/items.ts b/packages/x-tree-view/src/models/items.ts index 0d15e359d2d6..66bd474c6fb8 100644 --- a/packages/x-tree-view/src/models/items.ts +++ b/packages/x-tree-view/src/models/items.ts @@ -1,7 +1,9 @@ // TODO: Add support for number export type TreeViewItemId = string; -export type TreeViewBaseItem = R & { +export type TreeViewDefaultItemModelProperties = { id: string; label: string }; + +export type TreeViewBaseItem = R & { children?: TreeViewBaseItem[]; }; diff --git a/packages/x-tree-view/src/useTreeItem/useTreeItem.ts b/packages/x-tree-view/src/useTreeItem/useTreeItem.ts index cb5e07fbacaa..939973e40fa5 100644 --- a/packages/x-tree-view/src/useTreeItem/useTreeItem.ts +++ b/packages/x-tree-view/src/useTreeItem/useTreeItem.ts @@ -25,7 +25,10 @@ import { TreeViewItemPluginSlotPropsEnhancerParams } from '../internals/models'; import { useTreeItemUtils } from '../hooks/useTreeItemUtils'; import { TreeViewItemDepthContext } from '../internals/TreeViewItemDepthContext'; import { isTargetInDescendants } from '../internals/utils/tree'; +import { useSelector } from '../internals/hooks/useSelector'; +import { selectorIsItemTheDefaultFocusableItem } from '../internals/plugins/useTreeViewFocus/useTreeViewFocus.selectors'; import { generateTreeItemIdAttribute } from '../internals/corePlugins/useTreeViewId/useTreeViewId.utils'; +import { selectorCanItemBeFocused } from '../internals/plugins/useTreeViewItems/useTreeViewItems.selectors'; export const useTreeItem = < TSignatures extends UseTreeItemMinimalPlugins = UseTreeItemMinimalPlugins, @@ -35,15 +38,29 @@ export const useTreeItem = < ): UseTreeItemReturnValue => { const { runItemPlugins, - items: { onItemClick, disabledItemsFocusable }, + items: { onItemClick }, selection: { disableSelection, checkboxSelection }, expansion: { expansionTrigger }, + label: labelContext, treeId, instance, publicAPI, + store, } = useTreeViewContext(); const depthContext = React.useContext(TreeViewItemDepthContext); + const depth = useSelector( + store, + (...params) => { + if (typeof depthContext === 'function') { + return depthContext(...params); + } + + return depthContext; + }, + parameters.itemId, + ); + const { id, itemId, label, children, rootRef } = parameters; const { rootRef: pluginRootRef, contentRef, propsEnhancers } = runItemPlugins(parameters); @@ -55,7 +72,11 @@ export const useTreeItem = < const checkboxRef = React.useRef(null); const idAttribute = generateTreeItemIdAttribute({ itemId, treeId, id }); - const rootTabIndex = instance.canItemBeTabbed(itemId) ? 0 : -1; + const shouldBeAccessibleWithTab = useSelector( + store, + selectorIsItemTheDefaultFocusableItem, + itemId, + ); const sharedPropsEnhancerParams: Omit< TreeViewItemPluginSlotPropsEnhancerParams, @@ -70,8 +91,11 @@ export const useTreeItem = < return; } - const canBeFocused = !status.disabled || disabledItemsFocusable; - if (!status.focused && canBeFocused && event.currentTarget === event.target) { + if ( + !status.focused && + selectorCanItemBeFocused(store.value, itemId) && + event.currentTarget === event.target + ) { instance.focusItem(event, itemId); } }; @@ -132,7 +156,7 @@ export const useTreeItem = < const createContentHandleClick = (otherHandlers: EventHandlers) => (event: React.MouseEvent & TreeViewCancellableEvent) => { otherHandlers.onClick?.(event); - onItemClick?.(event, itemId); + onItemClick(event, itemId); if (event.defaultMuiPrevented || checkboxRef.current?.contains(event.target as HTMLElement)) { return; @@ -170,6 +194,8 @@ export const useTreeItem = < } }; + const getContextProviderProps = () => ({ itemId, id }); + const getRootProps = = {}>( externalProps: ExternalProps = {} as ExternalProps, ): UseTreeItemRootSlotProps => { @@ -195,7 +221,7 @@ export const useTreeItem = < ...externalEventHandlers, ref: handleRootRef, role: 'treeitem', - tabIndex: rootTabIndex, + tabIndex: shouldBeAccessibleWithTab ? 0 : -1, id: idAttribute, 'aria-expanded': status.expandable ? status.expanded : undefined, 'aria-selected': ariaSelected, @@ -203,8 +229,7 @@ export const useTreeItem = < ...externalProps, style: { ...(externalProps.style ?? {}), - '--TreeView-itemDepth': - typeof depthContext === 'function' ? depthContext(itemId) : depthContext, + '--TreeView-itemDepth': depth, } as React.CSSProperties, onFocus: createRootHandleFocus(externalEventHandlers), onBlur: createRootHandleBlur(externalEventHandlers), @@ -280,7 +305,7 @@ export const useTreeItem = < onDoubleClick: createLabelHandleDoubleClick(externalEventHandlers), }; - if (instance.isTreeViewEditable) { + if (labelContext?.isItemEditable) { props.editable = status.editable; } @@ -352,6 +377,7 @@ export const useTreeItem = < }; return { + getContextProviderProps, getRootProps, getContentProps, getGroupTransitionProps, diff --git a/packages/x-tree-view/src/useTreeItem/useTreeItem.types.ts b/packages/x-tree-view/src/useTreeItem/useTreeItem.types.ts index 0fae841967eb..fc4462713ffc 100644 --- a/packages/x-tree-view/src/useTreeItem/useTreeItem.types.ts +++ b/packages/x-tree-view/src/useTreeItem/useTreeItem.types.ts @@ -34,6 +34,11 @@ export interface UseTreeItemParameters { children?: React.ReactNode; } +export interface UseTreeItemContextProviderProps { + itemId: string; + id: string | undefined; +} + export interface UseTreeItemRootSlotPropsFromUseTreeItem { role: 'treeitem'; tabIndex: 0 | -1; @@ -127,6 +132,11 @@ export interface UseTreeItemReturnValue< TSignatures extends UseTreeItemMinimalPlugins, TOptionalSignatures extends UseTreeItemOptionalPlugins, > { + /** + * Resolver for the context provider's props. + * @returns {UseTreeItemContextProviderProps} Props that should be spread on the context provider slot. + */ + getContextProviderProps: () => UseTreeItemContextProviderProps; /** * Resolver for the root slot's props. * @param {ExternalProps} externalProps Additional props for the root slot. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63cac7483cb2..94b2fc4488a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,8 +99,8 @@ importers: specifier: ^5.16.7 version: 5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/monorepo': - specifier: github:mui/material-ui#123f0de85460c82ed1dc9b8015878c3239a56895 - version: https://codeload.github.com/mui/material-ui/tar.gz/123f0de85460c82ed1dc9b8015878c3239a56895(encoding@0.1.13) + specifier: github:mui/material-ui#a0ffee42815b110e14107249f193b7505d1761e5 + version: https://codeload.github.com/mui/material-ui/tar.gz/a0ffee42815b110e14107249f193b7505d1761e5(encoding@0.1.13) '@mui/utils': specifier: ^5.16.6 version: 5.16.6(@types/react@18.3.12)(react@18.3.1) @@ -169,7 +169,7 @@ importers: version: 7.18.0(eslint@8.57.1)(typescript@5.6.3) autoprefixer: specifier: ^10.4.20 - version: 10.4.20(postcss@8.4.47) + version: 10.4.20(postcss@8.4.49) axe-core: specifier: 4.10.2 version: 4.10.2 @@ -246,8 +246,8 @@ importers: specifier: ^2.31.0 version: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-webpack@0.13.9)(eslint@8.57.1) eslint-plugin-jsdoc: - specifier: ^50.4.3 - version: 50.4.3(eslint@8.57.1) + specifier: ^50.5.0 + version: 50.5.0(eslint@8.57.1) eslint-plugin-jsx-a11y: specifier: ^6.10.2 version: 6.10.2(eslint@8.57.1) @@ -264,8 +264,8 @@ importers: specifier: ^7.37.2 version: 7.37.2(eslint@8.57.1) eslint-plugin-react-compiler: - specifier: 19.0.0-beta-63b359f-20241101 - version: 19.0.0-beta-63b359f-20241101(eslint@8.57.1) + specifier: 19.0.0-beta-a7bf2bd-20241110 + version: 19.0.0-beta-a7bf2bd-20241110(eslint@8.57.1) eslint-plugin-react-hooks: specifier: ^5.0.0 version: 5.0.0(eslint@8.57.1) @@ -414,8 +414,8 @@ importers: specifier: ^7.26.0 version: 7.26.0 '@docsearch/react': - specifier: ^3.7.0 - version: 3.7.0(@algolia/client-search@5.12.0)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.13.0) + specifier: ^3.8.0 + version: 3.8.0(@algolia/client-search@5.12.0)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.13.0) '@emotion/cache': specifier: ^11.13.1 version: 11.13.1 @@ -430,7 +430,7 @@ importers: version: 11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) '@mui/docs': specifier: 6.1.7 - version: 6.1.7(jd23wyl5ec6tn4c5zv7d3fvige) + version: 6.1.7(5mdjhfhz45wjjqmb34n5tm4y5u) '@mui/icons-material': specifier: ^5.16.7 version: 5.16.7(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) @@ -445,7 +445,7 @@ importers: version: 5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/material-nextjs': specifier: ^5.16.6 - version: 5.16.6(@emotion/cache@11.13.1)(@emotion/server@11.11.0)(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(next@14.2.17(@babel/core@7.26.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.44.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 5.16.6(@emotion/cache@11.13.1)(@emotion/server@11.11.0)(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(next@14.2.18(@babel/core@7.26.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.44.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@mui/styles': specifier: ^5.16.7 version: 5.16.7(@types/react@18.3.12)(react@18.3.1) @@ -486,14 +486,14 @@ importers: specifier: ^9.7.5 version: 9.7.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/query-core': - specifier: ^5.59.20 - version: 5.59.20 + specifier: ^5.60.5 + version: 5.60.5 ast-types: specifier: ^0.14.2 version: 0.14.2 autoprefixer: specifier: ^10.4.20 - version: 10.4.20(postcss@8.4.47) + version: 10.4.20(postcss@8.4.49) babel-plugin-module-resolver: specifier: ^5.0.2 version: 5.0.2 @@ -561,8 +561,8 @@ importers: specifier: ^1.5.0 version: 1.5.0 markdown-to-jsx: - specifier: ^7.5.0 - version: 7.5.0(react@18.3.1) + specifier: ^7.6.2 + version: 7.6.2(react@18.3.1) moment: specifier: ^2.30.1 version: 2.30.1 @@ -576,14 +576,14 @@ importers: specifier: ^0.5.46 version: 0.5.46 next: - specifier: ^14.2.17 - version: 14.2.17(@babel/core@7.26.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.44.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^14.2.18 + version: 14.2.18(@babel/core@7.26.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.44.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nprogress: specifier: ^0.2.0 version: 0.2.0 postcss: - specifier: ^8.4.47 - version: 8.4.47 + specifier: ^8.4.49 + version: 8.4.49 prismjs: specifier: ^1.29.0 version: 1.29.0 @@ -769,6 +769,12 @@ importers: react-dom: specifier: ^17.0.0 || ^18.0.0 version: 18.3.1(react@18.3.1) + reselect: + specifier: ^5.1.1 + version: 5.1.1 + use-sync-external-store: + specifier: ^1.0.0 + version: 1.2.2(react@18.3.1) devDependencies: '@mui/internal-test-utils': specifier: ^1.0.20 @@ -788,6 +794,9 @@ importers: '@types/prop-types': specifier: ^15.7.13 version: 15.7.13 + '@types/use-sync-external-store': + specifier: ^0.0.6 + version: 0.0.6 csstype: specifier: ^3.1.3 version: 3.1.3 @@ -1449,6 +1458,12 @@ importers: react-transition-group: specifier: ^4.4.5 version: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + reselect: + specifier: ^5.1.1 + version: 5.1.1 + use-sync-external-store: + specifier: ^1.2.2 + version: 1.2.2(react@18.3.1) devDependencies: '@mui/internal-test-utils': specifier: ^1.0.20 @@ -1462,6 +1477,9 @@ importers: '@types/prop-types': specifier: ^15.7.13 version: 15.7.13 + '@types/use-sync-external-store': + specifier: ^0.0.6 + version: 0.0.6 rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -1508,6 +1526,12 @@ importers: react-transition-group: specifier: ^4.4.5 version: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + reselect: + specifier: ^5.1.1 + version: 5.1.1 + use-sync-external-store: + specifier: ^1.2.2 + version: 1.2.2(react@18.3.1) devDependencies: '@mui/internal-test-utils': specifier: ^1.0.20 @@ -1521,6 +1545,9 @@ importers: '@types/prop-types': specifier: ^15.7.13 version: 15.7.13 + '@types/use-sync-external-store': + specifier: ^0.0.6 + version: 0.0.6 rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -1626,7 +1653,7 @@ importers: devDependencies: '@codspeed/vitest-plugin': specifier: ^3.1.1 - version: 3.1.1(vite@5.3.4(@types/node@20.17.6)(terser@5.27.0))(vitest@2.1.4) + version: 3.1.1(vite@5.3.4(@types/node@20.17.6)(terser@5.27.0))(vitest@2.1.5) '@emotion/react': specifier: ^11.13.3 version: 11.13.3(@types/react@18.3.12)(react@18.3.1) @@ -1652,11 +1679,11 @@ importers: specifier: ^3.7.1 version: 3.7.1(@swc/helpers@0.5.5)(vite@5.3.4(@types/node@20.17.6)(terser@5.27.0)) '@vitest/browser': - specifier: 2.1.4 - version: 2.1.4(@types/node@20.17.6)(playwright@1.48.2)(typescript@5.6.3)(vite@5.3.4(@types/node@20.17.6)(terser@5.27.0))(vitest@2.1.4) + specifier: 2.1.5 + version: 2.1.5(@types/node@20.17.6)(playwright@1.48.2)(typescript@5.6.3)(vite@5.3.4(@types/node@20.17.6)(terser@5.27.0))(vitest@2.1.5) '@vitest/ui': - specifier: 2.1.4 - version: 2.1.4(vitest@2.1.4) + specifier: 2.1.5 + version: 2.1.5(vitest@2.1.5) jsdom: specifier: ^24.1.3 version: 24.1.3 @@ -1667,8 +1694,8 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) vitest: - specifier: 2.1.4 - version: 2.1.4(@types/node@20.17.6)(@vitest/browser@2.1.4)(@vitest/ui@2.1.4)(jsdom@24.1.3)(msw@2.6.2(@types/node@20.17.6)(typescript@5.6.3))(terser@5.27.0) + specifier: 2.1.5 + version: 2.1.5(@types/node@20.17.6)(@vitest/browser@2.1.5)(@vitest/ui@2.1.5)(jsdom@24.1.3)(msw@2.6.5(@types/node@20.17.6)(typescript@5.6.3))(terser@5.27.0) packages: @@ -1694,22 +1721,22 @@ packages: '@adobe/css-tools@4.4.0': resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==} - '@algolia/autocomplete-core@1.17.6': - resolution: {integrity: sha512-lkDoW4I7h2kKlIgf3pUt1LqvxyYKkVyiypoGLlUnhPSnCpmeOwudM6rNq6YYsCmdQtnDQoW5lUNNuj6ASg3qeg==} + '@algolia/autocomplete-core@1.17.7': + resolution: {integrity: sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==} - '@algolia/autocomplete-plugin-algolia-insights@1.17.6': - resolution: {integrity: sha512-17NnaacuFzSWVuZu4NKzVeaFIe9Abpw8w+/gjc7xhZFtqj+GadufzodIdchwiB2eM2cDdiR3icW7gbNTB3K2YA==} + '@algolia/autocomplete-plugin-algolia-insights@1.17.7': + resolution: {integrity: sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==} peerDependencies: search-insights: '>= 1 < 3' - '@algolia/autocomplete-preset-algolia@1.17.6': - resolution: {integrity: sha512-Cvg5JENdSCMuClwhJ1ON1/jSuojaYMiUW2KePm18IkdCzPJj/NXojaOxw58RFtQFpJgfVW8h2E8mEoDtLlMdeA==} + '@algolia/autocomplete-preset-algolia@1.17.7': + resolution: {integrity: sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==} peerDependencies: '@algolia/client-search': '>= 4.9.1 < 6' algoliasearch: '>= 4.9.1 < 6' - '@algolia/autocomplete-shared@1.17.6': - resolution: {integrity: sha512-aq/3V9E00Tw2GC/PqgyPGXtqJUlVc17v4cn1EUhSc+O/4zd04Uwb3UmPm8KDaYQQOrkt1lwvCj2vG2wRE5IKhw==} + '@algolia/autocomplete-shared@1.17.7': + resolution: {integrity: sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==} peerDependencies: '@algolia/client-search': '>= 4.9.1 < 6' algoliasearch: '>= 4.9.1 < 6' @@ -2436,11 +2463,11 @@ packages: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} - '@docsearch/css@3.7.0': - resolution: {integrity: sha512-1OorbTwi1eeDmr0v5t+ckSRlt1zM5GHjm92iIl3kUu7im3GHuP+csf6E0WBg8pdXQczTWP9J9+o9n+Vg6DH5cQ==} + '@docsearch/css@3.8.0': + resolution: {integrity: sha512-pieeipSOW4sQ0+bE5UFC51AOZp9NGxg89wAlZ1BAQFaiRAGK1IKUaPQ0UGZeNctJXyqZ1UvBtOQh2HH+U5GtmA==} - '@docsearch/react@3.7.0': - resolution: {integrity: sha512-8e6tdDfkYoxafEEPuX5eE1h9cTkLvhe4KgoFkO5JCddXSQONnN1FHcDZRI4r8894eMpbYq6rdJF0dVYh8ikwNQ==} + '@docsearch/react@3.8.0': + resolution: {integrity: sha512-WnFK720+iwTVt94CxY3u+FgX6exb3BfN5kE9xUY6uuAH/9W/UFboBZFLlrw/zxFRHoHZCOXRtOylsXF+6LHI+Q==} peerDependencies: '@types/react': '>= 16.8.0 < 19.0.0' react: '>= 16.8.0 < 19.0.0' @@ -3061,8 +3088,8 @@ packages: resolution: {integrity: sha512-DPnl5lPX4v49eVxEbJnAizrpMdMTBz1qykZrAbBul9rfgk531v8oAt+Pm6O/rpAleRombNM7FJb5rYGzBJatOQ==} engines: {node: '>=18.0.0'} - '@mswjs/interceptors@0.36.10': - resolution: {integrity: sha512-GXrJgakgJW3DWKueebkvtYgGKkxA7s0u5B0P5syJM5rvQUnrpLPigvci8Hukl7yEM+sU06l+er2Fgvx/gmiRgg==} + '@mswjs/interceptors@0.37.0': + resolution: {integrity: sha512-lDiHQMCBV9qz8c7+zxaNFQtWWaSogTYkqJ3Pg+FGYYC76nsfSxkMQ0df8fojyz16E+w4vp57NLjN2muNG7LugQ==} engines: {node: '>=18'} '@mui/base@5.0.0-beta.40': @@ -3197,9 +3224,9 @@ packages: '@types/react': optional: true - '@mui/monorepo@https://codeload.github.com/mui/material-ui/tar.gz/123f0de85460c82ed1dc9b8015878c3239a56895': - resolution: {tarball: https://codeload.github.com/mui/material-ui/tar.gz/123f0de85460c82ed1dc9b8015878c3239a56895} - version: 6.1.6 + '@mui/monorepo@https://codeload.github.com/mui/material-ui/tar.gz/a0ffee42815b110e14107249f193b7505d1761e5': + resolution: {tarball: https://codeload.github.com/mui/material-ui/tar.gz/a0ffee42815b110e14107249f193b7505d1761e5} + version: 6.1.7 engines: {pnpm: 9.12.3} '@mui/private-theming@5.16.6': @@ -3281,62 +3308,62 @@ packages: resolution: {integrity: sha512-q3L9i3HoNfz0SGpTIS4zTcKBbRkxzCRpd169eyiTuk3IwcPC3/85mzLHranlKo2b+HYT0gu37YxGB45aD8A3Tw==} engines: {node: '>=18.0.0'} - '@next/env@14.2.17': - resolution: {integrity: sha512-MCgO7VHxXo8sYR/0z+sk9fGyJJU636JyRmkjc7ZJY8Hurl8df35qG5hoAh5KMs75FLjhlEo9bb2LGe89Y/scDA==} + '@next/env@14.2.18': + resolution: {integrity: sha512-2vWLOUwIPgoqMJKG6dt35fVXVhgM09tw4tK3/Q34GFXDrfiHlG7iS33VA4ggnjWxjiz9KV5xzfsQzJX6vGAekA==} '@next/eslint-plugin-next@15.0.3': resolution: {integrity: sha512-3Ln/nHq2V+v8uIaxCR6YfYo7ceRgZNXfTd3yW1ukTaFbO+/I8jNakrjYWODvG9BuR2v5kgVtH/C8r0i11quOgw==} - '@next/swc-darwin-arm64@14.2.17': - resolution: {integrity: sha512-WiOf5nElPknrhRMTipXYTJcUz7+8IAjOYw3vXzj3BYRcVY0hRHKWgTgQ5439EvzQyHEko77XK+yN9x9OJ0oOog==} + '@next/swc-darwin-arm64@14.2.18': + resolution: {integrity: sha512-tOBlDHCjGdyLf0ube/rDUs6VtwNOajaWV+5FV/ajPgrvHeisllEdymY/oDgv2cx561+gJksfMUtqf8crug7sbA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@14.2.17': - resolution: {integrity: sha512-29y425wYnL17cvtxrDQWC3CkXe/oRrdt8ie61S03VrpwpPRI0XsnTvtKO06XCisK4alaMnZlf8riwZIbJTaSHQ==} + '@next/swc-darwin-x64@14.2.18': + resolution: {integrity: sha512-uJCEjutt5VeJ30jjrHV1VIHCsbMYnEqytQgvREx+DjURd/fmKy15NaVK4aR/u98S1LGTnjq35lRTnRyygglxoA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@14.2.17': - resolution: {integrity: sha512-SSHLZls3ZwNEHsc+d0ynKS+7Af0Nr8+KTUBAy9pm6xz9SHkJ/TeuEg6W3cbbcMSh6j4ITvrjv3Oi8n27VR+IPw==} + '@next/swc-linux-arm64-gnu@14.2.18': + resolution: {integrity: sha512-IL6rU8vnBB+BAm6YSWZewc+qvdL1EaA+VhLQ6tlUc0xp+kkdxQrVqAnh8Zek1ccKHlTDFRyAft0e60gteYmQ4A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@14.2.17': - resolution: {integrity: sha512-VFge37us5LNPatB4F7iYeuGs9Dprqe4ZkW7lOEJM91r+Wf8EIdViWHLpIwfdDXinvCdLl6b4VyLpEBwpkctJHA==} + '@next/swc-linux-arm64-musl@14.2.18': + resolution: {integrity: sha512-RCaENbIZqKKqTlL8KNd+AZV/yAdCsovblOpYFp0OJ7ZxgLNbV5w23CUU1G5On+0fgafrsGcW+GdMKdFjaRwyYA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@14.2.17': - resolution: {integrity: sha512-aaQlpxUVb9RZ41adlTYVQ3xvYEfBPUC8+6rDgmQ/0l7SvK8S1YNJzPmDPX6a4t0jLtIoNk7j+nroS/pB4nx7vQ==} + '@next/swc-linux-x64-gnu@14.2.18': + resolution: {integrity: sha512-3kmv8DlyhPRCEBM1Vavn8NjyXtMeQ49ID0Olr/Sut7pgzaQTo4h01S7Z8YNE0VtbowyuAL26ibcz0ka6xCTH5g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@14.2.17': - resolution: {integrity: sha512-HSyEiFaEY3ay5iATDqEup5WAfrhMATNJm8dYx3ZxL+e9eKv10XKZCwtZByDoLST7CyBmyDz+OFJL1wigyXeaoA==} + '@next/swc-linux-x64-musl@14.2.18': + resolution: {integrity: sha512-mliTfa8seVSpTbVEcKEXGjC18+TDII8ykW4a36au97spm9XMPqQTpdGPNBJ9RySSFw9/hLuaCMByluQIAnkzlw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@14.2.17': - resolution: {integrity: sha512-h5qM9Btqv87eYH8ArrnLoAHLyi79oPTP2vlGNSg4CDvUiXgi7l0+5KuEGp5pJoMhjuv9ChRdm7mRlUUACeBt4w==} + '@next/swc-win32-arm64-msvc@14.2.18': + resolution: {integrity: sha512-J5g0UFPbAjKYmqS3Cy7l2fetFmWMY9Oao32eUsBPYohts26BdrMUyfCJnZFQkX9npYaHNDOWqZ6uV9hSDPw9NA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-ia32-msvc@14.2.17': - resolution: {integrity: sha512-BD/G++GKSLexQjdyoEUgyo5nClU7er5rK0sE+HlEqnldJSm96CIr/+YOTT063LVTT/dUOeQsNgp5DXr86/K7/A==} + '@next/swc-win32-ia32-msvc@14.2.18': + resolution: {integrity: sha512-Ynxuk4ZgIpdcN7d16ivJdjsDG1+3hTvK24Pp8DiDmIa2+A4CfhJSEHHVndCHok6rnLUzAZD+/UOKESQgTsAZGg==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@next/swc-win32-x64-msvc@14.2.17': - resolution: {integrity: sha512-vkQfN1+4V4KqDibkW2q0sJ6CxQuXq5l2ma3z0BRcfIqkAMZiiW67T9yCpwqJKP68QghBtPEFjPAlaqe38O6frw==} + '@next/swc-win32-x64-msvc@14.2.18': + resolution: {integrity: sha512-dtRGMhiU9TN5nyhwzce+7c/4CCeykYS+ipY/4mIrGzJ71+7zNo55ZxCB7cAVuNqdwtYniFNR2c9OFQ6UdFIMcg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -3984,8 +4011,8 @@ packages: '@swc/types@0.1.13': resolution: {integrity: sha512-JL7eeCk6zWCbiYQg2xQSdLXQJl8Qoc9rXmG2cEKvHe3CKwMHwHGpfOb8frzNLmbycOo6I51qxnLnn9ESf4I20Q==} - '@tanstack/query-core@5.59.20': - resolution: {integrity: sha512-e8vw0lf7KwfGe1if4uPFhvZRWULqHjFcz3K8AebtieXvnMOz5FSzlZe3mTLlPuUBcydCnBRqYs2YJ5ys68wwLg==} + '@tanstack/query-core@5.60.5': + resolution: {integrity: sha512-jiS1aC3XI3BJp83ZiTuDLerTmn9P3U95r6p+6/SNauLJaYxfIC4dMuWygwnBHIZxjn2zJqEpj3nysmPieoxfPQ==} '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} @@ -4268,6 +4295,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/webpack-bundle-analyzer@4.7.0': resolution: {integrity: sha512-c5i2ThslSNSG8W891BRvOd/RoCjI2zwph8maD22b1adtSns20j+0azDDMCK06DiVrzTgnwiDl5Ntmu1YRJw8Sg==} @@ -4379,12 +4409,12 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 - '@vitest/browser@2.1.4': - resolution: {integrity: sha512-89SrvShW6kWzmEYtBj5k1gBq88emoC2qrngw5hE1vNpRFteQ5/1URbKIVww391rIALTpzhhCt5yJt5tjLPZxYw==} + '@vitest/browser@2.1.5': + resolution: {integrity: sha512-JrpnxvkrjlBrF7oXbK/YytWVYfJIzWYeDKppANlUaisBKwDso+yXlWocAJrANx8gUxyirF355Yx80S+SKQqayg==} peerDependencies: playwright: '*' safaridriver: '*' - vitest: 2.1.4 + vitest: 2.1.5 webdriverio: '*' peerDependenciesMeta: playwright: @@ -4394,11 +4424,11 @@ packages: webdriverio: optional: true - '@vitest/expect@2.1.4': - resolution: {integrity: sha512-DOETT0Oh1avie/D/o2sgMHGrzYUFFo3zqESB2Hn70z6QB1HrS2IQ9z5DfyTqU8sg4Bpu13zZe9V4+UTNQlUeQA==} + '@vitest/expect@2.1.5': + resolution: {integrity: sha512-nZSBTW1XIdpZvEJyoP/Sy8fUg0b8od7ZpGDkTUcfJ7wz/VoZAFzFfLyxVxGFhUjJzhYqSbIpfMtl/+k/dpWa3Q==} - '@vitest/mocker@2.1.4': - resolution: {integrity: sha512-Ky/O1Lc0QBbutJdW0rqLeFNbuLEyS+mIPiNdlVlp2/yhJ0SbyYqObS5IHdhferJud8MbbwMnexg4jordE5cCoQ==} + '@vitest/mocker@2.1.5': + resolution: {integrity: sha512-XYW6l3UuBmitWqSUXTNXcVBUCRytDogBsWuNXQijc00dtnU/9OqpXWp4OJroVrad/gLIomAq9aW8yWDBtMthhQ==} peerDependencies: msw: ^2.4.9 vite: ^5.0.0 @@ -4408,25 +4438,25 @@ packages: vite: optional: true - '@vitest/pretty-format@2.1.4': - resolution: {integrity: sha512-L95zIAkEuTDbUX1IsjRl+vyBSLh3PwLLgKpghl37aCK9Jvw0iP+wKwIFhfjdUtA2myLgjrG6VU6JCFLv8q/3Ww==} + '@vitest/pretty-format@2.1.5': + resolution: {integrity: sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw==} - '@vitest/runner@2.1.4': - resolution: {integrity: sha512-sKRautINI9XICAMl2bjxQM8VfCMTB0EbsBc/EDFA57V6UQevEKY/TOPOF5nzcvCALltiLfXWbq4MaAwWx/YxIA==} + '@vitest/runner@2.1.5': + resolution: {integrity: sha512-pKHKy3uaUdh7X6p1pxOkgkVAFW7r2I818vHDthYLvUyjRfkKOU6P45PztOch4DZarWQne+VOaIMwA/erSSpB9g==} - '@vitest/snapshot@2.1.4': - resolution: {integrity: sha512-3Kab14fn/5QZRog5BPj6Rs8dc4B+mim27XaKWFWHWA87R56AKjHTGcBFKpvZKDzC4u5Wd0w/qKsUIio3KzWW4Q==} + '@vitest/snapshot@2.1.5': + resolution: {integrity: sha512-zmYw47mhfdfnYbuhkQvkkzYroXUumrwWDGlMjpdUr4jBd3HZiV2w7CQHj+z7AAS4VOtWxI4Zt4bWt4/sKcoIjg==} - '@vitest/spy@2.1.4': - resolution: {integrity: sha512-4JOxa+UAizJgpZfaCPKK2smq9d8mmjZVPMt2kOsg/R8QkoRzydHH1qHxIYNvr1zlEaFj4SXiaaJWxq/LPLKaLg==} + '@vitest/spy@2.1.5': + resolution: {integrity: sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw==} - '@vitest/ui@2.1.4': - resolution: {integrity: sha512-Zd9e5oU063c+j9N9XzGJagCLNvG71x/2tOme3Js4JEZKX55zsgxhJwUgLI8hkN6NjMLpdJO8d7nVUUuPGAA58Q==} + '@vitest/ui@2.1.5': + resolution: {integrity: sha512-ERgKkDMTfngrZip6VG5h8L9B5D0AH/4+bga4yR1UzGH7c2cxv3LWogw2Dvuwr9cP3/iKDHYys7kIFLDKpxORTg==} peerDependencies: - vitest: 2.1.4 + vitest: 2.1.5 - '@vitest/utils@2.1.4': - resolution: {integrity: sha512-MXDnZn0Awl2S86PSNIim5PWXgIAx8CIkzu35mBdSApUip6RFOGXBCf3YFyeEu8n1IHk4bWD46DeYFu9mQlFIRg==} + '@vitest/utils@2.1.5': + resolution: {integrity: sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg==} '@webassemblyjs/ast@1.12.1': resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} @@ -6027,8 +6057,8 @@ packages: '@typescript-eslint/parser': optional: true - eslint-plugin-jsdoc@50.4.3: - resolution: {integrity: sha512-uWtwFxGRv6B8sU63HZM5dAGDhgsatb+LONwmILZJhdRALLOkCX2HFZhdL/Kw2ls8SQMAVEfK+LmnEfxInRN8HA==} + eslint-plugin-jsdoc@50.5.0: + resolution: {integrity: sha512-xTkshfZrUbiSHXBwZ/9d5ulZ2OcHXxSvm/NPo494H/hadLRJwOq5PMV0EUpMqsb9V+kQo+9BAgi6Z7aJtdBp2A==} engines: {node: '>=18'} peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 @@ -6059,8 +6089,8 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-react-compiler@19.0.0-beta-63b359f-20241101: - resolution: {integrity: sha512-b7edYKziu3EFUh9I2UirMnzlKT/80TS1i9pHHp5Rssv5HG74wPtxxdU13BN42hNFj2/Wnq4ToPjMVyuIFO5dhA==} + eslint-plugin-react-compiler@19.0.0-beta-a7bf2bd-20241110: + resolution: {integrity: sha512-b5/hRnOQlnH9CEnJQ6UrPoIAG4y/wIGv+OVEHTeAkbq+1uojfcuQyLToYvK1T9a6vz5WQHeMjQqFOZk3mtWorg==} engines: {node: ^14.17.0 || ^16.0.0 || >= 18.0.0} peerDependencies: eslint: '>=7' @@ -7663,8 +7693,8 @@ packages: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true - markdown-to-jsx@7.5.0: - resolution: {integrity: sha512-RrBNcMHiFPcz/iqIj0n3wclzHXjwS7mzjBNWecKKVhNTIxQepIix6Il/wZCn2Cg5Y1ow2Qi84+eJrryFRWBEWw==} + markdown-to-jsx@7.6.2: + resolution: {integrity: sha512-gEcyiJXzBxmId2Y/kydLbD6KRNccDiUy/Src1cFGn3s2X0LZZ/hUiEc2VisFyA5kUE3SXclTCczjQiAuqKZiFQ==} engines: {node: '>= 10'} peerDependencies: react: '>= 0.14.0' @@ -7968,8 +7998,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - msw@2.6.2: - resolution: {integrity: sha512-RdRgPvjfuzMIACkWv7VOVAeSRYMU3ofokLv1w0RsbFX960qnj/tFEyOFXY0G2GTUd9trA6rHuHciM/FKpBp6/A==} + msw@2.6.5: + resolution: {integrity: sha512-PnlnTpUlOrj441kYQzzFhzMzMCGFT6a2jKUBG7zSpLkYS5oh8Arrbc0dL8/rNAtxaoBy0EVs2mFqj2qdmWK7lQ==} engines: {node: '>=18'} hasBin: true peerDependencies: @@ -8021,8 +8051,8 @@ packages: nested-error-stacks@2.1.1: resolution: {integrity: sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==} - next@14.2.17: - resolution: {integrity: sha512-hNo/Zy701DDO3nzKkPmsLRlDfNCtb1OJxFUvjGEl04u7SFa3zwC6hqsOUzMajcaEOEV8ey1GjvByvrg0Qr5AiQ==} + next@14.2.18: + resolution: {integrity: sha512-H9qbjDuGivUDEnK6wa+p2XKO+iMzgVgyr9Zp/4Iv29lKa+DYaxJGjOeEA+5VOvJh/M7HLiskehInSa0cWxVXUw==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: @@ -8534,8 +8564,8 @@ packages: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} - picocolors@1.1.0: - resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} @@ -8627,8 +8657,8 @@ packages: resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} engines: {node: ^10 || ^12 || >=14} - postcss@8.4.47: - resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.1.2: @@ -9359,8 +9389,8 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} - std-env@3.7.0: - resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + std-env@3.8.0: + resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} stream-browserify@3.0.0: resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} @@ -9922,6 +9952,11 @@ packages: urlpattern-polyfill@8.0.2: resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==} + use-sync-external-store@1.2.2: + resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -9976,8 +10011,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite-node@2.1.4: - resolution: {integrity: sha512-kqa9v+oi4HwkG6g8ufRnb5AeplcRw8jUF6/7/Qz1qRQOXHImG8YnLbB+LLszENwFnoBl9xIf9nVdCFzNd7GQEg==} + vite-node@2.1.5: + resolution: {integrity: sha512-rd0QIgx74q4S1Rd56XIiL2cYEdyWn13cunYBIuqh9mpmQr7gGS0IxXoP8R6OaZtNQQLyXSWbd4rXKYUbhFpK5w==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -10009,15 +10044,15 @@ packages: terser: optional: true - vitest@2.1.4: - resolution: {integrity: sha512-eDjxbVAJw1UJJCHr5xr/xM86Zx+YxIEXGAR+bmnEID7z9qWfoxpHw0zdobz+TQAFOLT+nEXz3+gx6nUJ7RgmlQ==} + vitest@2.1.5: + resolution: {integrity: sha512-P4ljsdpuzRTPI/kbND2sDZ4VmieerR2c9szEZpjc+98Z9ebvnXmM5+0tHEKqYZumXqlvnmfWsjeFOjXVriDG7A==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^20.17.6 - '@vitest/browser': 2.1.4 - '@vitest/ui': 2.1.4 + '@vitest/browser': 2.1.5 + '@vitest/ui': 2.1.5 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -10369,30 +10404,30 @@ snapshots: '@adobe/css-tools@4.4.0': {} - '@algolia/autocomplete-core@1.17.6(@algolia/client-search@5.12.0)(algoliasearch@5.12.0)(search-insights@2.13.0)': + '@algolia/autocomplete-core@1.17.7(@algolia/client-search@5.12.0)(algoliasearch@5.12.0)(search-insights@2.13.0)': dependencies: - '@algolia/autocomplete-plugin-algolia-insights': 1.17.6(@algolia/client-search@5.12.0)(algoliasearch@5.12.0)(search-insights@2.13.0) - '@algolia/autocomplete-shared': 1.17.6(@algolia/client-search@5.12.0)(algoliasearch@5.12.0) + '@algolia/autocomplete-plugin-algolia-insights': 1.17.7(@algolia/client-search@5.12.0)(algoliasearch@5.12.0)(search-insights@2.13.0) + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.12.0)(algoliasearch@5.12.0) transitivePeerDependencies: - '@algolia/client-search' - algoliasearch - search-insights - '@algolia/autocomplete-plugin-algolia-insights@1.17.6(@algolia/client-search@5.12.0)(algoliasearch@5.12.0)(search-insights@2.13.0)': + '@algolia/autocomplete-plugin-algolia-insights@1.17.7(@algolia/client-search@5.12.0)(algoliasearch@5.12.0)(search-insights@2.13.0)': dependencies: - '@algolia/autocomplete-shared': 1.17.6(@algolia/client-search@5.12.0)(algoliasearch@5.12.0) + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.12.0)(algoliasearch@5.12.0) search-insights: 2.13.0 transitivePeerDependencies: - '@algolia/client-search' - algoliasearch - '@algolia/autocomplete-preset-algolia@1.17.6(@algolia/client-search@5.12.0)(algoliasearch@5.12.0)': + '@algolia/autocomplete-preset-algolia@1.17.7(@algolia/client-search@5.12.0)(algoliasearch@5.12.0)': dependencies: - '@algolia/autocomplete-shared': 1.17.6(@algolia/client-search@5.12.0)(algoliasearch@5.12.0) + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.12.0)(algoliasearch@5.12.0) '@algolia/client-search': 5.12.0 algoliasearch: 5.12.0 - '@algolia/autocomplete-shared@1.17.6(@algolia/client-search@5.12.0)(algoliasearch@5.12.0)': + '@algolia/autocomplete-shared@1.17.7(@algolia/client-search@5.12.0)(algoliasearch@5.12.0)': dependencies: '@algolia/client-search': 5.12.0 algoliasearch: 5.12.0 @@ -10519,7 +10554,7 @@ snapshots: dependencies: '@babel/helper-validator-identifier': 7.25.9 js-tokens: 4.0.0 - picocolors: 1.1.0 + picocolors: 1.1.1 '@babel/compat-data@7.26.0': {} @@ -11351,11 +11386,11 @@ snapshots: transitivePeerDependencies: - debug - '@codspeed/vitest-plugin@3.1.1(vite@5.3.4(@types/node@20.17.6)(terser@5.27.0))(vitest@2.1.4)': + '@codspeed/vitest-plugin@3.1.1(vite@5.3.4(@types/node@20.17.6)(terser@5.27.0))(vitest@2.1.5)': dependencies: '@codspeed/core': 3.1.1 vite: 5.3.4(@types/node@20.17.6)(terser@5.27.0) - vitest: 2.1.4(@types/node@20.17.6)(@vitest/browser@2.1.4)(@vitest/ui@2.1.4)(jsdom@24.1.3)(msw@2.6.2(@types/node@20.17.6)(typescript@5.6.3))(terser@5.27.0) + vitest: 2.1.5(@types/node@20.17.6)(@vitest/browser@2.1.5)(@vitest/ui@2.1.5)(jsdom@24.1.3)(msw@2.6.5(@types/node@20.17.6)(typescript@5.6.3))(terser@5.27.0) transitivePeerDependencies: - debug @@ -11363,13 +11398,13 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} - '@docsearch/css@3.7.0': {} + '@docsearch/css@3.8.0': {} - '@docsearch/react@3.7.0(@algolia/client-search@5.12.0)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.13.0)': + '@docsearch/react@3.8.0(@algolia/client-search@5.12.0)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.13.0)': dependencies: - '@algolia/autocomplete-core': 1.17.6(@algolia/client-search@5.12.0)(algoliasearch@5.12.0)(search-insights@2.13.0) - '@algolia/autocomplete-preset-algolia': 1.17.6(@algolia/client-search@5.12.0)(algoliasearch@5.12.0) - '@docsearch/css': 3.7.0 + '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.12.0)(algoliasearch@5.12.0)(search-insights@2.13.0) + '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.12.0)(algoliasearch@5.12.0) + '@docsearch/css': 3.8.0 algoliasearch: 5.12.0 optionalDependencies: '@types/react': 18.3.12 @@ -11958,7 +11993,7 @@ snapshots: - supports-color - typescript - '@mswjs/interceptors@0.36.10': + '@mswjs/interceptors@0.37.0': dependencies: '@open-draft/deferred-promise': 2.2.0 '@open-draft/logger': 0.3.0 @@ -11983,7 +12018,7 @@ snapshots: '@mui/core-downloads-tracker@5.16.7': {} - '@mui/docs@6.1.7(jd23wyl5ec6tn4c5zv7d3fvige)': + '@mui/docs@6.1.7(5mdjhfhz45wjjqmb34n5tm4y5u)': dependencies: '@babel/runtime': 7.26.0 '@mui/base': 5.0.0-beta.40(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -11995,7 +12030,7 @@ snapshots: clipboard-copy: 4.0.1 clsx: 2.1.1 csstype: 3.1.3 - next: 14.2.17(@babel/core@7.26.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.44.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.18(@babel/core@7.26.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.44.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nprogress: 0.2.0 prop-types: 15.8.1 react: 18.3.1 @@ -12109,11 +12144,11 @@ snapshots: '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) '@types/react': 18.3.12 - '@mui/material-nextjs@5.16.6(@emotion/cache@11.13.1)(@emotion/server@11.11.0)(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(next@14.2.17(@babel/core@7.26.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.44.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@mui/material-nextjs@5.16.6(@emotion/cache@11.13.1)(@emotion/server@11.11.0)(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(next@14.2.18(@babel/core@7.26.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.44.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.26.0 '@mui/material': 5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - next: 14.2.17(@babel/core@7.26.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.44.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.18(@babel/core@7.26.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.44.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 optionalDependencies: '@emotion/cache': 11.13.1 @@ -12141,7 +12176,7 @@ snapshots: '@emotion/styled': 11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) '@types/react': 18.3.12 - '@mui/monorepo@https://codeload.github.com/mui/material-ui/tar.gz/123f0de85460c82ed1dc9b8015878c3239a56895(encoding@0.1.13)': + '@mui/monorepo@https://codeload.github.com/mui/material-ui/tar.gz/a0ffee42815b110e14107249f193b7505d1761e5(encoding@0.1.13)': dependencies: '@googleapis/sheets': 9.3.1(encoding@0.1.13) '@netlify/functions': 2.8.2 @@ -12241,37 +12276,37 @@ snapshots: '@netlify/node-cookies': 0.1.0 urlpattern-polyfill: 8.0.2 - '@next/env@14.2.17': {} + '@next/env@14.2.18': {} '@next/eslint-plugin-next@15.0.3': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@14.2.17': + '@next/swc-darwin-arm64@14.2.18': optional: true - '@next/swc-darwin-x64@14.2.17': + '@next/swc-darwin-x64@14.2.18': optional: true - '@next/swc-linux-arm64-gnu@14.2.17': + '@next/swc-linux-arm64-gnu@14.2.18': optional: true - '@next/swc-linux-arm64-musl@14.2.17': + '@next/swc-linux-arm64-musl@14.2.18': optional: true - '@next/swc-linux-x64-gnu@14.2.17': + '@next/swc-linux-x64-gnu@14.2.18': optional: true - '@next/swc-linux-x64-musl@14.2.17': + '@next/swc-linux-x64-musl@14.2.18': optional: true - '@next/swc-win32-arm64-msvc@14.2.17': + '@next/swc-win32-arm64-msvc@14.2.18': optional: true - '@next/swc-win32-ia32-msvc@14.2.17': + '@next/swc-win32-ia32-msvc@14.2.18': optional: true - '@next/swc-win32-x64-msvc@14.2.17': + '@next/swc-win32-x64-msvc@14.2.18': optional: true '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': @@ -13044,7 +13079,7 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tanstack/query-core@5.59.20': {} + '@tanstack/query-core@5.60.5': {} '@testing-library/dom@10.4.0': dependencies: @@ -13344,6 +13379,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} + '@types/webpack-bundle-analyzer@4.7.0(@swc/core@1.7.35(@swc/helpers@0.5.5))(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.2)(webpack@5.96.1))': dependencies: '@types/node': 20.17.6 @@ -13507,17 +13544,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/browser@2.1.4(@types/node@20.17.6)(playwright@1.48.2)(typescript@5.6.3)(vite@5.3.4(@types/node@20.17.6)(terser@5.27.0))(vitest@2.1.4)': + '@vitest/browser@2.1.5(@types/node@20.17.6)(playwright@1.48.2)(typescript@5.6.3)(vite@5.3.4(@types/node@20.17.6)(terser@5.27.0))(vitest@2.1.5)': dependencies: '@testing-library/dom': 10.4.0 '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) - '@vitest/mocker': 2.1.4(msw@2.6.2(@types/node@20.17.6)(typescript@5.6.3))(vite@5.3.4(@types/node@20.17.6)(terser@5.27.0)) - '@vitest/utils': 2.1.4 + '@vitest/mocker': 2.1.5(msw@2.6.5(@types/node@20.17.6)(typescript@5.6.3))(vite@5.3.4(@types/node@20.17.6)(terser@5.27.0)) + '@vitest/utils': 2.1.5 magic-string: 0.30.12 - msw: 2.6.2(@types/node@20.17.6)(typescript@5.6.3) + msw: 2.6.5(@types/node@20.17.6)(typescript@5.6.3) sirv: 3.0.0 tinyrainbow: 1.2.0 - vitest: 2.1.4(@types/node@20.17.6)(@vitest/browser@2.1.4)(@vitest/ui@2.1.4)(jsdom@24.1.3)(msw@2.6.2(@types/node@20.17.6)(typescript@5.6.3))(terser@5.27.0) + vitest: 2.1.5(@types/node@20.17.6)(@vitest/browser@2.1.5)(@vitest/ui@2.1.5)(jsdom@24.1.3)(msw@2.6.5(@types/node@20.17.6)(typescript@5.6.3))(terser@5.27.0) ws: 8.18.0 optionalDependencies: playwright: 1.48.2 @@ -13528,55 +13565,55 @@ snapshots: - utf-8-validate - vite - '@vitest/expect@2.1.4': + '@vitest/expect@2.1.5': dependencies: - '@vitest/spy': 2.1.4 - '@vitest/utils': 2.1.4 + '@vitest/spy': 2.1.5 + '@vitest/utils': 2.1.5 chai: 5.1.2 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.4(msw@2.6.2(@types/node@20.17.6)(typescript@5.6.3))(vite@5.3.4(@types/node@20.17.6)(terser@5.27.0))': + '@vitest/mocker@2.1.5(msw@2.6.5(@types/node@20.17.6)(typescript@5.6.3))(vite@5.3.4(@types/node@20.17.6)(terser@5.27.0))': dependencies: - '@vitest/spy': 2.1.4 + '@vitest/spy': 2.1.5 estree-walker: 3.0.3 magic-string: 0.30.12 optionalDependencies: - msw: 2.6.2(@types/node@20.17.6)(typescript@5.6.3) + msw: 2.6.5(@types/node@20.17.6)(typescript@5.6.3) vite: 5.3.4(@types/node@20.17.6)(terser@5.27.0) - '@vitest/pretty-format@2.1.4': + '@vitest/pretty-format@2.1.5': dependencies: tinyrainbow: 1.2.0 - '@vitest/runner@2.1.4': + '@vitest/runner@2.1.5': dependencies: - '@vitest/utils': 2.1.4 + '@vitest/utils': 2.1.5 pathe: 1.1.2 - '@vitest/snapshot@2.1.4': + '@vitest/snapshot@2.1.5': dependencies: - '@vitest/pretty-format': 2.1.4 + '@vitest/pretty-format': 2.1.5 magic-string: 0.30.12 pathe: 1.1.2 - '@vitest/spy@2.1.4': + '@vitest/spy@2.1.5': dependencies: tinyspy: 3.0.2 - '@vitest/ui@2.1.4(vitest@2.1.4)': + '@vitest/ui@2.1.5(vitest@2.1.5)': dependencies: - '@vitest/utils': 2.1.4 + '@vitest/utils': 2.1.5 fflate: 0.8.2 flatted: 3.3.1 pathe: 1.1.2 sirv: 3.0.0 tinyglobby: 0.2.10 tinyrainbow: 1.2.0 - vitest: 2.1.4(@types/node@20.17.6)(@vitest/browser@2.1.4)(@vitest/ui@2.1.4)(jsdom@24.1.3)(msw@2.6.2(@types/node@20.17.6)(typescript@5.6.3))(terser@5.27.0) + vitest: 2.1.5(@types/node@20.17.6)(@vitest/browser@2.1.5)(@vitest/ui@2.1.5)(jsdom@24.1.3)(msw@2.6.5(@types/node@20.17.6)(typescript@5.6.3))(terser@5.27.0) - '@vitest/utils@2.1.4': + '@vitest/utils@2.1.5': dependencies: - '@vitest/pretty-format': 2.1.4 + '@vitest/pretty-format': 2.1.5 loupe: 3.1.2 tinyrainbow: 1.2.0 @@ -13997,14 +14034,14 @@ snapshots: asynckit@0.4.0: {} - autoprefixer@10.4.20(postcss@8.4.47): + autoprefixer@10.4.20(postcss@8.4.49): dependencies: browserslist: 4.24.0 caniuse-lite: 1.0.30001667 fraction.js: 4.3.7 normalize-range: 0.1.2 - picocolors: 1.1.0 - postcss: 8.4.47 + picocolors: 1.1.1 + postcss: 8.4.49 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -15444,7 +15481,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsdoc@50.4.3(eslint@8.57.1): + eslint-plugin-jsdoc@50.5.0(eslint@8.57.1): dependencies: '@es-joy/jsdoccomment': 0.49.0 are-docs-informative: 0.0.2 @@ -15497,7 +15534,7 @@ snapshots: '@types/eslint': 8.56.12 eslint-config-prettier: 9.1.0(eslint@8.57.1) - eslint-plugin-react-compiler@19.0.0-beta-63b359f-20241101(eslint@8.57.1): + eslint-plugin-react-compiler@19.0.0-beta-a7bf2bd-20241110(eslint@8.57.1): dependencies: '@babel/core': 7.26.0 '@babel/parser': 7.26.1 @@ -16839,7 +16876,7 @@ snapshots: graceful-fs: 4.2.11 micromatch: 4.0.8 neo-async: 2.6.2 - picocolors: 1.1.0 + picocolors: 1.1.1 recast: 0.23.9 tmp: 0.2.3 write-file-atomic: 5.0.1 @@ -17465,7 +17502,7 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 - markdown-to-jsx@7.5.0(react@18.3.1): + markdown-to-jsx@7.6.2(react@18.3.1): dependencies: react: 18.3.1 @@ -17860,13 +17897,13 @@ snapshots: ms@2.1.3: {} - msw@2.6.2(@types/node@20.17.6)(typescript@5.6.3): + msw@2.6.5(@types/node@20.17.6)(typescript@5.6.3): dependencies: '@bundled-es-modules/cookie': 2.0.1 '@bundled-es-modules/statuses': 1.0.1 '@bundled-es-modules/tough-cookie': 0.1.6 '@inquirer/confirm': 5.0.1(@types/node@20.17.6) - '@mswjs/interceptors': 0.36.10 + '@mswjs/interceptors': 0.37.0 '@open-draft/deferred-promise': 2.2.0 '@open-draft/until': 2.1.0 '@types/cookie': 0.6.0 @@ -17922,9 +17959,9 @@ snapshots: nested-error-stacks@2.1.1: {} - next@14.2.17(@babel/core@7.26.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.44.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@14.2.18(@babel/core@7.26.0)(@opentelemetry/api@1.8.0)(@playwright/test@1.44.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@next/env': 14.2.17 + '@next/env': 14.2.18 '@swc/helpers': 0.5.5 busboy: 1.6.0 caniuse-lite: 1.0.30001667 @@ -17934,15 +17971,15 @@ snapshots: react-dom: 18.3.1(react@18.3.1) styled-jsx: 5.1.1(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react@18.3.1) optionalDependencies: - '@next/swc-darwin-arm64': 14.2.17 - '@next/swc-darwin-x64': 14.2.17 - '@next/swc-linux-arm64-gnu': 14.2.17 - '@next/swc-linux-arm64-musl': 14.2.17 - '@next/swc-linux-x64-gnu': 14.2.17 - '@next/swc-linux-x64-musl': 14.2.17 - '@next/swc-win32-arm64-msvc': 14.2.17 - '@next/swc-win32-ia32-msvc': 14.2.17 - '@next/swc-win32-x64-msvc': 14.2.17 + '@next/swc-darwin-arm64': 14.2.18 + '@next/swc-darwin-x64': 14.2.18 + '@next/swc-linux-arm64-gnu': 14.2.18 + '@next/swc-linux-arm64-musl': 14.2.18 + '@next/swc-linux-x64-gnu': 14.2.18 + '@next/swc-linux-x64-musl': 14.2.18 + '@next/swc-win32-arm64-msvc': 14.2.18 + '@next/swc-win32-ia32-msvc': 14.2.18 + '@next/swc-win32-x64-msvc': 14.2.18 '@opentelemetry/api': 1.8.0 '@playwright/test': 1.44.1 transitivePeerDependencies: @@ -18551,7 +18588,7 @@ snapshots: pathval@2.0.0: {} - picocolors@1.1.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -18615,19 +18652,19 @@ snapshots: postcss@8.4.31: dependencies: nanoid: 3.3.7 - picocolors: 1.1.0 + picocolors: 1.1.1 source-map-js: 1.2.1 postcss@8.4.38: dependencies: nanoid: 3.3.7 - picocolors: 1.1.0 + picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.4.47: + postcss@8.4.49: dependencies: nanoid: 3.3.7 - picocolors: 1.1.0 + picocolors: 1.1.1 source-map-js: 1.2.1 prelude-ls@1.1.2: {} @@ -18667,7 +18704,7 @@ snapshots: find-up: 5.0.0 ignore: 5.3.1 mri: 1.2.0 - picocolors: 1.1.0 + picocolors: 1.1.1 picomatch: 3.0.1 prettier: 3.3.3 tslib: 2.6.2 @@ -19493,7 +19530,7 @@ snapshots: statuses@2.0.1: {} - std-env@3.7.0: {} + std-env@3.8.0: {} stream-browserify@3.0.0: dependencies: @@ -20044,7 +20081,7 @@ snapshots: dependencies: browserslist: 4.24.0 escalade: 3.1.2 - picocolors: 1.1.0 + picocolors: 1.1.1 update-check@1.5.4: dependencies: @@ -20064,6 +20101,10 @@ snapshots: urlpattern-polyfill@8.0.2: {} + use-sync-external-store@1.2.2(react@18.3.1): + dependencies: + react: 18.3.1 + util-deprecate@1.0.2: {} util.inherits@1.0.3: {} @@ -20115,10 +20156,11 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@2.1.4(@types/node@20.17.6)(terser@5.27.0): + vite-node@2.1.5(@types/node@20.17.6)(terser@5.27.0): dependencies: cac: 6.7.14 debug: 4.3.7(supports-color@8.1.1) + es-module-lexer: 1.5.4 pathe: 1.1.2 vite: 5.3.4(@types/node@20.17.6)(terser@5.27.0) transitivePeerDependencies: @@ -20134,39 +20176,39 @@ snapshots: vite@5.3.4(@types/node@20.17.6)(terser@5.27.0): dependencies: esbuild: 0.21.5 - postcss: 8.4.47 + postcss: 8.4.49 rollup: 4.18.1 optionalDependencies: '@types/node': 20.17.6 fsevents: 2.3.3 terser: 5.27.0 - vitest@2.1.4(@types/node@20.17.6)(@vitest/browser@2.1.4)(@vitest/ui@2.1.4)(jsdom@24.1.3)(msw@2.6.2(@types/node@20.17.6)(typescript@5.6.3))(terser@5.27.0): + vitest@2.1.5(@types/node@20.17.6)(@vitest/browser@2.1.5)(@vitest/ui@2.1.5)(jsdom@24.1.3)(msw@2.6.5(@types/node@20.17.6)(typescript@5.6.3))(terser@5.27.0): dependencies: - '@vitest/expect': 2.1.4 - '@vitest/mocker': 2.1.4(msw@2.6.2(@types/node@20.17.6)(typescript@5.6.3))(vite@5.3.4(@types/node@20.17.6)(terser@5.27.0)) - '@vitest/pretty-format': 2.1.4 - '@vitest/runner': 2.1.4 - '@vitest/snapshot': 2.1.4 - '@vitest/spy': 2.1.4 - '@vitest/utils': 2.1.4 + '@vitest/expect': 2.1.5 + '@vitest/mocker': 2.1.5(msw@2.6.5(@types/node@20.17.6)(typescript@5.6.3))(vite@5.3.4(@types/node@20.17.6)(terser@5.27.0)) + '@vitest/pretty-format': 2.1.5 + '@vitest/runner': 2.1.5 + '@vitest/snapshot': 2.1.5 + '@vitest/spy': 2.1.5 + '@vitest/utils': 2.1.5 chai: 5.1.2 debug: 4.3.7(supports-color@8.1.1) expect-type: 1.1.0 magic-string: 0.30.12 pathe: 1.1.2 - std-env: 3.7.0 + std-env: 3.8.0 tinybench: 2.9.0 tinyexec: 0.3.1 tinypool: 1.0.1 tinyrainbow: 1.2.0 vite: 5.3.4(@types/node@20.17.6)(terser@5.27.0) - vite-node: 2.1.4(@types/node@20.17.6)(terser@5.27.0) + vite-node: 2.1.5(@types/node@20.17.6)(terser@5.27.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.17.6 - '@vitest/browser': 2.1.4(@types/node@20.17.6)(playwright@1.48.2)(typescript@5.6.3)(vite@5.3.4(@types/node@20.17.6)(terser@5.27.0))(vitest@2.1.4) - '@vitest/ui': 2.1.4(vitest@2.1.4) + '@vitest/browser': 2.1.5(@types/node@20.17.6)(playwright@1.48.2)(typescript@5.6.3)(vite@5.3.4(@types/node@20.17.6)(terser@5.27.0))(vitest@2.1.5) + '@vitest/ui': 2.1.5(vitest@2.1.5) jsdom: 24.1.3 transitivePeerDependencies: - less @@ -20210,7 +20252,7 @@ snapshots: gzip-size: 6.0.0 html-escaper: 2.0.2 opener: 1.5.2 - picocolors: 1.1.0 + picocolors: 1.1.1 sirv: 2.0.4 ws: 7.5.9 transitivePeerDependencies: diff --git a/renovate.json b/renovate.json index ad51a3c124a6..7c587261f30b 100644 --- a/renovate.json +++ b/renovate.json @@ -112,7 +112,8 @@ }, { "matchDepTypes": ["action"], - "pinDigests": true + "pinDigests": true, + "automerge": true }, { "groupName": "GitHub Actions", diff --git a/scripts/buildApiDocs/chartsSettings/index.ts b/scripts/buildApiDocs/chartsSettings/index.ts index 30c812c5182d..a9631f07f4ea 100644 --- a/scripts/buildApiDocs/chartsSettings/index.ts +++ b/scripts/buildApiDocs/chartsSettings/index.ts @@ -67,6 +67,7 @@ export default chartsApiPages; 'x-charts/src/ChartsOverlay/ChartsLoadingOverlay.tsx', 'x-charts/src/ChartsLegend/LegendPerItem.tsx', 'x-charts/src/LineChart/CircleMarkElement.tsx', + 'x-charts/src/BarChart/AnimatedBarElement.tsx', ].some((invalidPath) => filename.endsWith(invalidPath)); }, skipAnnotatingComponentDefinition: true, diff --git a/scripts/x-charts-pro.exports.json b/scripts/x-charts-pro.exports.json index 1d29c9cafbc0..faae92e438c1 100644 --- a/scripts/x-charts-pro.exports.json +++ b/scripts/x-charts-pro.exports.json @@ -9,7 +9,6 @@ { "name": "AreaElementClasses", "kind": "Interface" }, { "name": "AreaElementClassKey", "kind": "TypeAlias" }, { "name": "AreaElementOwnerState", "kind": "Interface" }, - { "name": "AreaElementPath", "kind": "Variable" }, { "name": "AreaElementProps", "kind": "Interface" }, { "name": "AreaElementSlotProps", "kind": "Interface" }, { "name": "AreaElementSlots", "kind": "Interface" }, @@ -30,7 +29,6 @@ { "name": "BarElementClasses", "kind": "Interface" }, { "name": "BarElementClassKey", "kind": "TypeAlias" }, { "name": "BarElementOwnerState", "kind": "Interface" }, - { "name": "BarElementPath", "kind": "Variable" }, { "name": "BarElementProps", "kind": "TypeAlias" }, { "name": "BarElementSlotProps", "kind": "Interface" }, { "name": "BarElementSlots", "kind": "Interface" }, @@ -57,6 +55,8 @@ { "name": "CartesianSeriesType", "kind": "TypeAlias" }, { "name": "ChartContainerPro", "kind": "Variable" }, { "name": "ChartContainerProProps", "kind": "Interface" }, + { "name": "ChartDataProvider", "kind": "Function" }, + { "name": "ChartDataProviderProps", "kind": "TypeAlias" }, { "name": "ChartsAxis", "kind": "Function" }, { "name": "ChartsAxisClasses", "kind": "Interface" }, { "name": "ChartsAxisClassKey", "kind": "TypeAlias" }, @@ -68,6 +68,7 @@ { "name": "ChartsAxisHighlightClassKey", "kind": "TypeAlias" }, { "name": "ChartsAxisHighlightPath", "kind": "Variable" }, { "name": "ChartsAxisHighlightProps", "kind": "TypeAlias" }, + { "name": "ChartsAxisHighlightType", "kind": "TypeAlias" }, { "name": "ChartsAxisProps", "kind": "Interface" }, { "name": "ChartsAxisTooltipContent", "kind": "Function" }, { "name": "ChartsClipPath", "kind": "Function" }, @@ -202,7 +203,6 @@ { "name": "LineElementClasses", "kind": "Interface" }, { "name": "LineElementClassKey", "kind": "TypeAlias" }, { "name": "LineElementOwnerState", "kind": "Interface" }, - { "name": "LineElementPath", "kind": "Variable" }, { "name": "LineElementProps", "kind": "Interface" }, { "name": "LineElementSlotProps", "kind": "Interface" }, { "name": "LineElementSlots", "kind": "Interface" }, diff --git a/scripts/x-charts.exports.json b/scripts/x-charts.exports.json index 7a4b49ed6348..eb38124c022f 100644 --- a/scripts/x-charts.exports.json +++ b/scripts/x-charts.exports.json @@ -9,7 +9,6 @@ { "name": "AreaElementClasses", "kind": "Interface" }, { "name": "AreaElementClassKey", "kind": "TypeAlias" }, { "name": "AreaElementOwnerState", "kind": "Interface" }, - { "name": "AreaElementPath", "kind": "Variable" }, { "name": "AreaElementProps", "kind": "Interface" }, { "name": "AreaElementSlotProps", "kind": "Interface" }, { "name": "AreaElementSlots", "kind": "Interface" }, @@ -28,7 +27,6 @@ { "name": "BarElementClasses", "kind": "Interface" }, { "name": "BarElementClassKey", "kind": "TypeAlias" }, { "name": "BarElementOwnerState", "kind": "Interface" }, - { "name": "BarElementPath", "kind": "Variable" }, { "name": "BarElementProps", "kind": "TypeAlias" }, { "name": "BarElementSlotProps", "kind": "Interface" }, { "name": "BarElementSlots", "kind": "Interface" }, @@ -55,6 +53,8 @@ { "name": "CartesianSeriesType", "kind": "TypeAlias" }, { "name": "ChartContainer", "kind": "Variable" }, { "name": "ChartContainerProps", "kind": "Interface" }, + { "name": "ChartDataProvider", "kind": "Function" }, + { "name": "ChartDataProviderProps", "kind": "TypeAlias" }, { "name": "ChartsAxis", "kind": "Function" }, { "name": "ChartsAxisClasses", "kind": "Interface" }, { "name": "ChartsAxisClassKey", "kind": "TypeAlias" }, @@ -66,6 +66,7 @@ { "name": "ChartsAxisHighlightClassKey", "kind": "TypeAlias" }, { "name": "ChartsAxisHighlightPath", "kind": "Variable" }, { "name": "ChartsAxisHighlightProps", "kind": "TypeAlias" }, + { "name": "ChartsAxisHighlightType", "kind": "TypeAlias" }, { "name": "ChartsAxisProps", "kind": "Interface" }, { "name": "ChartsAxisTooltipContent", "kind": "Function" }, { "name": "ChartsClipPath", "kind": "Function" }, @@ -191,7 +192,6 @@ { "name": "LineElementClasses", "kind": "Interface" }, { "name": "LineElementClassKey", "kind": "TypeAlias" }, { "name": "LineElementOwnerState", "kind": "Interface" }, - { "name": "LineElementPath", "kind": "Variable" }, { "name": "LineElementProps", "kind": "Interface" }, { "name": "LineElementSlotProps", "kind": "Interface" }, { "name": "LineElementSlots", "kind": "Interface" }, diff --git a/scripts/x-tree-view-pro.exports.json b/scripts/x-tree-view-pro.exports.json index 66646f1d424a..926170d64994 100644 --- a/scripts/x-tree-view-pro.exports.json +++ b/scripts/x-tree-view-pro.exports.json @@ -47,6 +47,7 @@ { "name": "TreeViewCancellableEvent", "kind": "TypeAlias" }, { "name": "TreeViewCancellableEventHandler", "kind": "TypeAlias" }, { "name": "TreeViewCollapseIcon", "kind": "Variable" }, + { "name": "TreeViewDefaultItemModelProperties", "kind": "TypeAlias" }, { "name": "TreeViewExpandIcon", "kind": "Variable" }, { "name": "TreeViewItemId", "kind": "TypeAlias" }, { "name": "TreeViewItemsReorderingAction", "kind": "TypeAlias" }, @@ -60,6 +61,7 @@ { "name": "UseTreeItemIconContainerSlotOwnProps", "kind": "Interface" }, { "name": "UseTreeItemLabelInputSlotOwnProps", "kind": "Interface" }, { "name": "UseTreeItemLabelSlotOwnProps", "kind": "Interface" }, + { "name": "useTreeItemModel", "kind": "Variable" }, { "name": "UseTreeItemParameters", "kind": "Interface" }, { "name": "UseTreeItemReturnValue", "kind": "Interface" }, { "name": "UseTreeItemRootSlotOwnProps", "kind": "Interface" }, diff --git a/scripts/x-tree-view.exports.json b/scripts/x-tree-view.exports.json index 6daaee01de81..e72c296ae182 100644 --- a/scripts/x-tree-view.exports.json +++ b/scripts/x-tree-view.exports.json @@ -51,6 +51,7 @@ { "name": "TreeViewCancellableEvent", "kind": "TypeAlias" }, { "name": "TreeViewCancellableEventHandler", "kind": "TypeAlias" }, { "name": "TreeViewCollapseIcon", "kind": "Variable" }, + { "name": "TreeViewDefaultItemModelProperties", "kind": "TypeAlias" }, { "name": "TreeViewExpandIcon", "kind": "Variable" }, { "name": "TreeViewItemId", "kind": "TypeAlias" }, { "name": "TreeViewItemsReorderingAction", "kind": "TypeAlias" }, @@ -64,6 +65,7 @@ { "name": "UseTreeItemIconContainerSlotOwnProps", "kind": "Interface" }, { "name": "UseTreeItemLabelInputSlotOwnProps", "kind": "Interface" }, { "name": "UseTreeItemLabelSlotOwnProps", "kind": "Interface" }, + { "name": "useTreeItemModel", "kind": "Variable" }, { "name": "UseTreeItemParameters", "kind": "Interface" }, { "name": "UseTreeItemReturnValue", "kind": "Interface" }, { "name": "UseTreeItemRootSlotOwnProps", "kind": "Interface" }, diff --git a/test/performance-charts/package.json b/test/performance-charts/package.json index bb79214b2c07..330b0cb2b671 100644 --- a/test/performance-charts/package.json +++ b/test/performance-charts/package.json @@ -16,11 +16,11 @@ "@testing-library/user-event": "^14.5.2", "@vitejs/plugin-react": "^4.3.3", "@vitejs/plugin-react-swc": "^3.7.1", - "@vitest/browser": "2.1.4", - "@vitest/ui": "2.1.4", + "@vitest/browser": "2.1.5", + "@vitest/ui": "2.1.5", "jsdom": "^24.1.3", "react": "^18.3.1", "react-dom": "^18.3.1", - "vitest": "2.1.4" + "vitest": "2.1.5" } } diff --git a/test/utils/tree-view/describeTreeView/describeTreeView.tsx b/test/utils/tree-view/describeTreeView/describeTreeView.tsx index 4d7d28078f17..dc4c6c99a043 100644 --- a/test/utils/tree-view/describeTreeView/describeTreeView.tsx +++ b/test/utils/tree-view/describeTreeView/describeTreeView.tsx @@ -118,6 +118,7 @@ const innerDescribeTreeView = items: rawItems, withErrorBoundary, slotProps, + slots, ...other }) => { const items = rawItems as readonly DescribeTreeViewItem[]; @@ -127,7 +128,7 @@ const innerDescribeTreeView = diff --git a/test/utils/tree-view/describeTreeView/describeTreeView.types.ts b/test/utils/tree-view/describeTreeView/describeTreeView.types.ts index 20996106b13b..a51c1ec53c76 100644 --- a/test/utils/tree-view/describeTreeView/describeTreeView.types.ts +++ b/test/utils/tree-view/describeTreeView/describeTreeView.types.ts @@ -109,7 +109,10 @@ export interface DescribeTreeViewRendererReturnValue< * Passes new props to the Tree View. * @param {Partial>} props A subset of the props accepted by the Tree View. */ - setProps: (props: Partial>) => void; + setProps: ( + props: Partial> & + React.HTMLAttributes, + ) => void; /** * Passes new items to the Tree View. * @param {readyonly DescribeTreeViewItem[]} items The new items. diff --git a/test/utils/tree-view/fakeContextValue.ts b/test/utils/tree-view/fakeContextValue.ts index b64e3cb6759f..3898a7e7c242 100644 --- a/test/utils/tree-view/fakeContextValue.ts +++ b/test/utils/tree-view/fakeContextValue.ts @@ -1,27 +1,12 @@ import { TreeViewContextValue } from '@mui/x-tree-view/internals/TreeViewProvider'; import { SimpleTreeViewPluginSignatures } from '@mui/x-tree-view/SimpleTreeView/SimpleTreeView.plugins'; +import { TreeViewStore } from '@mui/x-tree-view/internals/utils/TreeViewStore'; export const getFakeContextValue = ( features: { checkboxSelection?: boolean } = {}, ): TreeViewContextValue => ({ - instance: { - isItemExpandable: () => false, - isItemExpanded: () => false, - isItemFocused: () => false, - isItemSelected: () => false, - isItemDisabled: (itemId: string | null): itemId is string => !!itemId, - mapFirstCharFromJSX: () => () => {}, - canItemBeTabbed: () => false, - } as any, - publicAPI: { - focusItem: () => {}, - getItem: () => ({}), - getItemOrderedChildrenIds: () => [], - setItemExpansion: () => {}, - getItemDOMElement: () => null, - selectItem: () => {}, - getItemTree: () => [], - }, + instance: {} as any, + publicAPI: {} as any, runItemPlugins: () => ({ rootRef: null, contentRef: null, @@ -30,7 +15,7 @@ export const getFakeContextValue = ( wrapItem: ({ children }) => children, wrapRoot: ({ children }) => children, items: { - disabledItemsFocusable: false, + onItemClick: () => {}, }, icons: { slots: {}, @@ -47,4 +32,18 @@ export const getFakeContextValue = ( current: null, }, expansion: { expansionTrigger: 'content' }, + store: new TreeViewStore({ + cacheKey: { id: 1 }, + id: { treeId: 'mui-tree-view-1', providedTreeId: undefined }, + items: { + disabledItemsFocusable: false, + itemMetaLookup: {}, + itemModelLookup: {}, + itemOrderedChildrenIdsLookup: {}, + itemChildrenIndexesLookup: {}, + }, + expansion: { expandedItemsMap: new Map() }, + selection: { selectedItemsMap: new Map() }, + focus: { focusedItemId: null, defaultFocusableItemId: null }, + }), });