diff --git a/packages/x-charts/src/BarChart/BarPlot.tsx b/packages/x-charts/src/BarChart/BarPlot.tsx index 476e4319b916..7ff1e8999a57 100644 --- a/packages/x-charts/src/BarChart/BarPlot.tsx +++ b/packages/x-charts/src/BarChart/BarPlot.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { useTransition } from '@react-spring/web'; -import { SeriesContext } from '../context/SeriesContextProvider'; import { CartesianContext } from '../context/CartesianContextProvider'; import { BarElement, BarElementSlotProps, BarElementSlots } from './BarElement'; import { AxisDefaultized } from '../models/axis'; @@ -14,6 +13,7 @@ import { BarClipPath } from './BarClipPath'; import { BarLabelItemProps, BarLabelSlotProps, BarLabelSlots } from './BarLabel/BarLabelItem'; import { BarLabelPlot } from './BarLabel/BarLabelPlot'; import { checkScaleErrors } from './checkScaleErrors'; +import { useBarSeries } from '../hooks/useSeries'; /** * Solution of the equations @@ -87,7 +87,7 @@ const useAggregatedData = (): { masksData: MaskData[]; } => { const seriesData = - React.useContext(SeriesContext).bar ?? + useBarSeries() ?? ({ series: {}, stackingGroups: [], seriesOrder: [] } as FormatterResult<'bar'>); const axisData = React.useContext(CartesianContext); const chartId = useChartId(); diff --git a/packages/x-charts/src/ChartsLegend/ChartsLegend.tsx b/packages/x-charts/src/ChartsLegend/ChartsLegend.tsx index 6804c7592f0b..9be1635a6b77 100644 --- a/packages/x-charts/src/ChartsLegend/ChartsLegend.tsx +++ b/packages/x-charts/src/ChartsLegend/ChartsLegend.tsx @@ -4,11 +4,11 @@ import { useSlotProps } from '@mui/base/utils'; import { unstable_composeClasses as composeClasses } from '@mui/utils'; import { useThemeProps, useTheme, Theme } from '@mui/material/styles'; import { AnchorPosition, Direction, getSeriesToDisplay } from './utils'; -import { SeriesContext } from '../context/SeriesContextProvider'; import { ChartsLegendClasses, getLegendUtilityClass } from './chartsLegendClasses'; import { DefaultizedProps } from '../models/helpers'; import { DefaultChartsLegend, LegendRendererProps } from './DefaultChartsLegend'; import { useDrawingArea } from '../hooks'; +import { useSeries } from '../hooks/useSeries'; export interface ChartsLegendSlots { /** @@ -83,7 +83,7 @@ function ChartsLegend(inProps: ChartsLegendProps) { const classes = useUtilityClasses({ ...props, theme }); const drawingArea = useDrawingArea(); - const series = React.useContext(SeriesContext); + const series = useSeries(); const seriesToDisplay = getSeriesToDisplay(series); diff --git a/packages/x-charts/src/ChartsOnAxisClickHandler/ChartsOnAxisClickHandler.tsx b/packages/x-charts/src/ChartsOnAxisClickHandler/ChartsOnAxisClickHandler.tsx index 3a7735582e52..f134b0b2f599 100644 --- a/packages/x-charts/src/ChartsOnAxisClickHandler/ChartsOnAxisClickHandler.tsx +++ b/packages/x-charts/src/ChartsOnAxisClickHandler/ChartsOnAxisClickHandler.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { SvgContext } from '../context/DrawingProvider'; import { InteractionContext } from '../context/InteractionProvider'; import { CartesianContext } from '../context/CartesianContextProvider'; -import { SeriesContext } from '../context/SeriesContextProvider'; +import { useSeries } from '../hooks/useSeries'; +import { useSvgRef } from '../hooks'; type AxisData = { dataIndex: number; @@ -24,8 +24,8 @@ export interface ChartsOnAxisClickHandlerProps { function ChartsOnAxisClickHandler(props: ChartsOnAxisClickHandlerProps) { const { onAxisClick } = props; - const svgRef = React.useContext(SvgContext); - const series = React.useContext(SeriesContext); + const svgRef = useSvgRef(); + const series = useSeries(); const { axis } = React.useContext(InteractionContext); const { xAxisIds, xAxis, yAxisIds, yAxis } = React.useContext(CartesianContext); diff --git a/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx b/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx index c65b9e1e1786..c1d33d29c210 100644 --- a/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx +++ b/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import { SxProps, Theme } from '@mui/material/styles'; import { useSlotProps } from '@mui/base/utils'; import { AxisInteractionData } from '../context/InteractionProvider'; -import { SeriesContext } from '../context/SeriesContextProvider'; import { CartesianContext } from '../context/CartesianContextProvider'; import { ChartSeriesDefaultized, ChartSeriesType } from '../models/seriesType/config'; import { AxisDefaultized } from '../models/axis'; @@ -12,6 +11,7 @@ import { DefaultChartsAxisTooltipContent } from './DefaultChartsAxisTooltipConte import { isCartesianSeriesType } from './utils'; import colorGetter from '../internals/colorGetter'; import { ZAxisContext } from '../context/ZAxisContextProvider'; +import { useSeries } from '../hooks/useSeries'; type ChartSeriesDefaultizedWithColorGetter = ChartSeriesDefaultized & { getColor: (dataIndex: number) => string; @@ -61,7 +61,7 @@ function ChartsAxisTooltipContent(props: { const { xAxisIds, xAxis, yAxisIds, yAxis } = React.useContext(CartesianContext); const { zAxisIds, zAxis } = React.useContext(ZAxisContext); - const series = React.useContext(SeriesContext); + const series = useSeries(); const USED_AXIS_ID = isXaxis ? xAxisIds[0] : yAxisIds[0]; diff --git a/packages/x-charts/src/ChartsTooltip/ChartsItemTooltipContent.tsx b/packages/x-charts/src/ChartsTooltip/ChartsItemTooltipContent.tsx index 2f7183a6fd93..d77f16af9d47 100644 --- a/packages/x-charts/src/ChartsTooltip/ChartsItemTooltipContent.tsx +++ b/packages/x-charts/src/ChartsTooltip/ChartsItemTooltipContent.tsx @@ -3,13 +3,13 @@ import PropTypes from 'prop-types'; import { SxProps, Theme } from '@mui/material/styles'; import { useSlotProps } from '@mui/base/utils'; import { ItemInteractionData } from '../context/InteractionProvider'; -import { SeriesContext } from '../context/SeriesContextProvider'; import { ChartSeriesDefaultized, ChartSeriesType } from '../models/seriesType/config'; import { ChartsTooltipClasses } from './chartsTooltipClasses'; import { DefaultChartsItemTooltipContent } from './DefaultChartsItemTooltipContent'; import { CartesianContext } from '../context/CartesianContextProvider'; import colorGetter from '../internals/colorGetter'; import { ZAxisContext } from '../context/ZAxisContextProvider'; +import { useSeries } from '../hooks/useSeries'; export type ChartsItemContentProps = { /** @@ -42,9 +42,7 @@ function ChartsItemTooltipContent(props: { }) { const { content, itemData, sx, classes, contentProps } = props; - const series = React.useContext(SeriesContext)[itemData.type]!.series[ - itemData.seriesId - ] as ChartSeriesDefaultized; + const series = useSeries()[itemData.type]!.series[itemData.seriesId] as ChartSeriesDefaultized; const { xAxis, yAxis, xAxisIds, yAxisIds } = React.useContext(CartesianContext); const { zAxis, zAxisIds } = React.useContext(ZAxisContext); diff --git a/packages/x-charts/src/ChartsTooltip/utils.tsx b/packages/x-charts/src/ChartsTooltip/utils.tsx index 8d465635c61f..89d86636aefc 100644 --- a/packages/x-charts/src/ChartsTooltip/utils.tsx +++ b/packages/x-charts/src/ChartsTooltip/utils.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import { AxisInteractionData, ItemInteractionData } from '../context/InteractionProvider'; -import { SvgContext } from '../context/DrawingProvider'; import { CartesianChartSeriesType, ChartSeriesDefaultized, ChartSeriesType, } from '../models/seriesType/config'; +import { useSvgRef } from '../hooks'; export function generateVirtualElement(mousePosition: { x: number; y: number } | null) { if (mousePosition === null) { @@ -41,7 +41,7 @@ export function generateVirtualElement(mousePosition: { x: number; y: number } | } export function useMouseTracker() { - const svgRef = React.useContext(SvgContext); + const svgRef = useSvgRef(); // Use a ref to avoid rerendering on every mousemove event. const [mousePosition, setMousePosition] = React.useState(null); diff --git a/packages/x-charts/src/ChartsVoronoiHandler/ChartsVoronoiHandler.tsx b/packages/x-charts/src/ChartsVoronoiHandler/ChartsVoronoiHandler.tsx index c66a23fc06e2..170c4a71e112 100644 --- a/packages/x-charts/src/ChartsVoronoiHandler/ChartsVoronoiHandler.tsx +++ b/packages/x-charts/src/ChartsVoronoiHandler/ChartsVoronoiHandler.tsx @@ -4,13 +4,13 @@ import { Delaunay } from 'd3-delaunay'; import useEnhancedEffect from '@mui/utils/useEnhancedEffect'; import { InteractionContext } from '../context/InteractionProvider'; import { CartesianContext } from '../context/CartesianContextProvider'; -import { SeriesContext } from '../context/SeriesContextProvider'; import { getValueToPositionMapper } from '../hooks/useScale'; import { getSVGPoint } from '../internals/utils'; import { ScatterItemIdentifier } from '../models'; import { SeriesId } from '../models/seriesType/common'; import { useDrawingArea, useSvgRef } from '../hooks'; import { useHighlighted } from '../context'; +import { useScatterSeries } from '../hooks/useSeries'; export type ChartsVoronoiHandlerProps = { /** @@ -35,7 +35,7 @@ function ChartsVoronoiHandler(props: ChartsVoronoiHandlerProps) { const { xAxis, yAxis, xAxisIds, yAxisIds } = React.useContext(CartesianContext); const { dispatch } = React.useContext(InteractionContext); - const { series, seriesOrder } = React.useContext(SeriesContext).scatter ?? {}; + const { series, seriesOrder } = useScatterSeries() ?? {}; const voronoiRef = React.useRef>({}); const delauneyRef = React.useRef | undefined>(undefined); diff --git a/packages/x-charts/src/LineChart/AreaPlot.tsx b/packages/x-charts/src/LineChart/AreaPlot.tsx index 9334bbc122b4..51a62066c83a 100644 --- a/packages/x-charts/src/LineChart/AreaPlot.tsx +++ b/packages/x-charts/src/LineChart/AreaPlot.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { area as d3Area } from 'd3-shape'; -import { SeriesContext } from '../context/SeriesContextProvider'; import { CartesianContext } from '../context/CartesianContextProvider'; import { AreaElement, @@ -14,6 +13,7 @@ import getCurveFactory from '../internals/getCurve'; import { DEFAULT_X_AXIS_KEY } from '../constants'; import { LineItemIdentifier } from '../models/seriesType/line'; import { useChartGradient } from '../internals/components/ChartsAxesGradients'; +import { useLineSeries } from '../hooks/useSeries'; export interface AreaPlotSlots extends AreaElementSlots {} @@ -34,7 +34,7 @@ export interface AreaPlotProps } const useAggregatedData = () => { - const seriesData = React.useContext(SeriesContext).line; + const seriesData = useLineSeries(); const axisData = React.useContext(CartesianContext); if (seriesData === undefined) { diff --git a/packages/x-charts/src/LineChart/LineHighlightPlot.tsx b/packages/x-charts/src/LineChart/LineHighlightPlot.tsx index b2dd6ec8b08d..25343dd4c129 100644 --- a/packages/x-charts/src/LineChart/LineHighlightPlot.tsx +++ b/packages/x-charts/src/LineChart/LineHighlightPlot.tsx @@ -1,12 +1,12 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { SeriesContext } from '../context/SeriesContextProvider'; import { CartesianContext } from '../context/CartesianContextProvider'; 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'; export interface LineHighlightPlotSlots { lineHighlight?: React.JSXElementConstructor; @@ -42,7 +42,7 @@ export interface LineHighlightPlotProps extends React.SVGAttributes { - const seriesData = React.useContext(SeriesContext).line; + const seriesData = useLineSeries(); const axisData = React.useContext(CartesianContext); if (seriesData === undefined) { diff --git a/packages/x-charts/src/LineChart/MarkPlot.tsx b/packages/x-charts/src/LineChart/MarkPlot.tsx index 313d1b37db58..23c7695f443e 100644 --- a/packages/x-charts/src/LineChart/MarkPlot.tsx +++ b/packages/x-charts/src/LineChart/MarkPlot.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { SeriesContext } from '../context/SeriesContextProvider'; import { CartesianContext } from '../context/CartesianContextProvider'; import { MarkElement, MarkElementProps } from './MarkElement'; import { getValueToPositionMapper } from '../hooks/useScale'; @@ -9,6 +8,7 @@ import { DEFAULT_X_AXIS_KEY } from '../constants'; import { LineItemIdentifier } from '../models/seriesType/line'; import { cleanId } from '../internals/utils'; import getColor from './getColor'; +import { useLineSeries } from '../hooks/useSeries'; export interface MarkPlotSlots { mark?: React.JSXElementConstructor; @@ -55,7 +55,7 @@ export interface MarkPlotProps function MarkPlot(props: MarkPlotProps) { const { slots, slotProps, skipAnimation, onItemClick, ...other } = props; - const seriesData = React.useContext(SeriesContext).line; + const seriesData = useLineSeries(); const axisData = React.useContext(CartesianContext); const chartId = useChartId(); diff --git a/packages/x-charts/src/PieChart/PiePlot.tsx b/packages/x-charts/src/PieChart/PiePlot.tsx index dc85fdea29a2..dcd0e62fe9f4 100644 --- a/packages/x-charts/src/PieChart/PiePlot.tsx +++ b/packages/x-charts/src/PieChart/PiePlot.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { SeriesContext } from '../context/SeriesContextProvider'; import { DrawingContext } from '../context/DrawingProvider'; import { PieArcPlot, PieArcPlotProps, PieArcPlotSlotProps, PieArcPlotSlots } from './PieArcPlot'; import { PieArcLabelPlotSlots, PieArcLabelPlotSlotProps, PieArcLabelPlot } from './PieArcLabelPlot'; import { getPercentageValue } from '../internals/utils'; import { getPieCoordinates } from './getPieCoordinates'; +import { usePieSeries } from '../hooks/useSeries'; export interface PiePlotSlots extends PieArcPlotSlots, PieArcLabelPlotSlots {} @@ -36,7 +36,7 @@ export interface PiePlotProps extends Pick; @@ -39,7 +39,7 @@ export interface ScatterPlotProps extends Pick { */ function ScatterPlot(props: ScatterPlotProps) { const { slots, slotProps, onItemClick } = props; - const seriesData = React.useContext(SeriesContext).scatter; + const seriesData = useScatterSeries(); const axisData = React.useContext(CartesianContext); const { zAxis, zAxisIds } = React.useContext(ZAxisContext); diff --git a/packages/x-charts/src/context/CartesianContextProvider.tsx b/packages/x-charts/src/context/CartesianContextProvider.tsx index a6eaeb90f9aa..fb67be299bdb 100644 --- a/packages/x-charts/src/context/CartesianContextProvider.tsx +++ b/packages/x-charts/src/context/CartesianContextProvider.tsx @@ -14,7 +14,6 @@ import { } from '../LineChart/extremums'; import { AxisConfig, AxisDefaultized, isBandScaleConfig, isPointScaleConfig } from '../models/axis'; import { getScale } from '../internals/getScale'; -import { SeriesContext } from './SeriesContextProvider'; import { DEFAULT_X_AXIS_KEY, DEFAULT_Y_AXIS_KEY } from '../constants'; import { CartesianChartSeriesType, @@ -28,6 +27,7 @@ import { getTickNumber } from '../hooks/useTicks'; import { useDrawingArea } from '../hooks/useDrawingArea'; import { SeriesId } from '../models/seriesType/common'; import { getColorScale, getOrdinalColorScale } from '../internals/colorScale'; +import { useSeries } from '../hooks/useSeries'; export type CartesianContextProviderProps = { /** @@ -99,7 +99,7 @@ if (process.env.NODE_ENV !== 'production') { function CartesianContextProvider(props: CartesianContextProviderProps) { const { xAxis: inXAxis, yAxis: inYAxis, dataset, children } = props; - const formattedSeries = React.useContext(SeriesContext); + const formattedSeries = useSeries(); const drawingArea = useDrawingArea(); const xAxis = React.useMemo( diff --git a/packages/x-charts/src/context/DrawingProvider.tsx b/packages/x-charts/src/context/DrawingProvider.tsx index 6f418dd8f57e..6e7a5ceb6111 100644 --- a/packages/x-charts/src/context/DrawingProvider.tsx +++ b/packages/x-charts/src/context/DrawingProvider.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import useId from '@mui/utils/useId'; import useChartDimensions from '../hooks/useChartDimensions'; import { LayoutConfig } from '../models/layout'; +import { Initializable } from './context.types'; export interface DrawingProviderProps extends LayoutConfig { children: React.ReactNode; @@ -59,7 +60,12 @@ if (process.env.NODE_ENV !== 'production') { DrawingContext.displayName = 'DrawingContext'; } -export const SvgContext = React.createContext>({ current: null }); +export type SvgContextState = React.RefObject; + +export const SvgContext = React.createContext>({ + isInitialized: false, + data: { current: null }, +}); if (process.env.NODE_ENV !== 'production') { SvgContext.displayName = 'SvgContext'; @@ -75,8 +81,10 @@ export function DrawingProvider(props: DrawingProviderProps) { [chartId, drawingArea], ); + const refValue = React.useMemo(() => ({ isInitialized: true, data: svgRef }), [svgRef]); + return ( - + {children} ); diff --git a/packages/x-charts/src/context/HighlightedProvider/HighlightedContext.ts b/packages/x-charts/src/context/HighlightedProvider/HighlightedContext.ts index eef7143a95f9..335b8ba59223 100644 --- a/packages/x-charts/src/context/HighlightedProvider/HighlightedContext.ts +++ b/packages/x-charts/src/context/HighlightedProvider/HighlightedContext.ts @@ -1,5 +1,6 @@ import * as React from 'react'; import { SeriesId } from '../../models/seriesType/common'; +import { Initializable } from '../context.types'; /** * The data of the highlighted item. @@ -68,12 +69,15 @@ export type HighlightedState = { isFaded: (input: HighlightItemData) => boolean; }; -export const HighlightedContext = React.createContext({ - highlightedItem: null, - setHighlighted: () => {}, - clearHighlighted: () => {}, - isHighlighted: () => false, - isFaded: () => false, +export const HighlightedContext = React.createContext>({ + isInitialized: false, + data: { + highlightedItem: null, + setHighlighted: () => {}, + clearHighlighted: () => {}, + isHighlighted: () => false, + isFaded: () => false, + }, }); if (process.env.NODE_ENV !== 'production') { diff --git a/packages/x-charts/src/context/HighlightedProvider/HighlightedProvider.tsx b/packages/x-charts/src/context/HighlightedProvider/HighlightedProvider.tsx index e42bcc3d5cd0..d9922f67bf9d 100644 --- a/packages/x-charts/src/context/HighlightedProvider/HighlightedProvider.tsx +++ b/packages/x-charts/src/context/HighlightedProvider/HighlightedProvider.tsx @@ -12,6 +12,7 @@ import { createIsHighlighted } from './createIsHighlighted'; import { useSeries } from '../../hooks/useSeries'; import { ChartSeriesType } from '../../models/seriesType/config'; import { SeriesId } from '../../models/seriesType/common'; +import { Initializable } from '../context.types'; export type HighlightedProviderProps = { children: React.ReactNode; @@ -67,20 +68,23 @@ function HighlightedProvider({ ? seriesById.get(highlightedItem.seriesId) ?? undefined : undefined; - const providerValue = React.useMemo(() => { + const providerValue = React.useMemo>(() => { return { - highlightScope, - highlightedItem, - setHighlighted: (itemData) => { - setHighlightedItem(itemData); - onHighlightChange?.(itemData); + isInitialized: true, + data: { + highlightScope, + highlightedItem, + setHighlighted: (itemData) => { + setHighlightedItem(itemData); + onHighlightChange?.(itemData); + }, + clearHighlighted: () => { + setHighlightedItem(null); + onHighlightChange?.(null); + }, + isHighlighted: createIsHighlighted(highlightScope, highlightedItem), + isFaded: createIsFaded(highlightScope, highlightedItem), }, - clearHighlighted: () => { - setHighlightedItem(null); - onHighlightChange?.(null); - }, - isHighlighted: createIsHighlighted(highlightScope, highlightedItem), - isFaded: createIsFaded(highlightScope, highlightedItem), }; }, [highlightedItem, highlightScope, setHighlightedItem, onHighlightChange]); diff --git a/packages/x-charts/src/context/HighlightedProvider/useHighlighted.test.tsx b/packages/x-charts/src/context/HighlightedProvider/useHighlighted.test.tsx new file mode 100644 index 000000000000..03812aae6959 --- /dev/null +++ b/packages/x-charts/src/context/HighlightedProvider/useHighlighted.test.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { ErrorBoundary, createRenderer } from '@mui/internal-test-utils'; +import { useHighlighted } from './useHighlighted'; +import { HighlightedProvider } from './HighlightedProvider'; +import { SeriesContextProvider } from '../SeriesContextProvider'; + +function UseHighlighted() { + const { highlightedItem } = useHighlighted(); + return
{highlightedItem?.seriesId}
; +} + +describe('useHighlighted', () => { + const { render } = createRenderer(); + + it('should throw an error when parent context not present', function test() { + if (!/jsdom/.test(window.navigator.userAgent)) { + // can't catch render errors in the browser for unknown reason + // tried try-catch + error boundary + window onError preventDefault + this.skip(); + } + + const errorRef = React.createRef(); + + expect(() => + render( + + + , + ), + ).toErrorDev([ + 'MUI X: Could not find the highlighted ref context.', + 'It looks like you rendered your component outside of a ChartsContainer parent component.', + 'The above error occurred in the component:', + ]); + + expect((errorRef.current as any).errors).to.have.length(1); + expect((errorRef.current as any).errors[0].toString()).to.include( + 'MUI X: Could not find the highlighted ref context.', + ); + }); + + it('should not throw an error when parent context is present', () => { + const { getByText } = render( + + + + + , + ); + + expect(getByText('test-id')).toBeVisible(); + }); +}); diff --git a/packages/x-charts/src/context/HighlightedProvider/useHighlighted.ts b/packages/x-charts/src/context/HighlightedProvider/useHighlighted.ts index f8db7950e123..59e0c8b9ac27 100644 --- a/packages/x-charts/src/context/HighlightedProvider/useHighlighted.ts +++ b/packages/x-charts/src/context/HighlightedProvider/useHighlighted.ts @@ -9,9 +9,9 @@ import { HighlightedContext, HighlightedState } from './HighlightedContext'; * @returns {HighlightedState} the state of the chart */ export function useHighlighted(): HighlightedState { - const highlighted = React.useContext(HighlightedContext); + const { isInitialized, data } = React.useContext(HighlightedContext); - if (highlighted === undefined) { + if (!isInitialized) { throw new Error( [ 'MUI X: Could not find the highlighted ref context.', @@ -20,5 +20,5 @@ export function useHighlighted(): HighlightedState { ); } - return highlighted; + return data; } diff --git a/packages/x-charts/src/context/HighlightedProvider/useItemHighlighted.ts b/packages/x-charts/src/context/HighlightedProvider/useItemHighlighted.ts index d019b9c661bd..c6465231cf19 100644 --- a/packages/x-charts/src/context/HighlightedProvider/useItemHighlighted.ts +++ b/packages/x-charts/src/context/HighlightedProvider/useItemHighlighted.ts @@ -1,5 +1,5 @@ -import * as React from 'react'; -import { HighlightedContext, HighlightItemData } from './HighlightedContext'; +import { HighlightItemData } from './HighlightedContext'; +import { useHighlighted } from './useHighlighted'; export type ItemHighlightedState = { /** @@ -22,16 +22,7 @@ export type ItemHighlightedState = { * @returns {ItemHighlightedState} the state of the item */ export function useItemHighlighted(item: HighlightItemData | null): ItemHighlightedState { - const highlighted = React.useContext(HighlightedContext); - - if (highlighted === undefined) { - throw new Error( - [ - 'MUI X: Could not find the highlighted ref context.', - 'It looks like you rendered your component outside of a ChartsContainer parent component.', - ].join('\n'), - ); - } + const highlighted = useHighlighted(); if (!item) { return { diff --git a/packages/x-charts/src/context/SeriesContextProvider.tsx b/packages/x-charts/src/context/SeriesContextProvider.tsx index e39746611bd4..63a3055fecb3 100644 --- a/packages/x-charts/src/context/SeriesContextProvider.tsx +++ b/packages/x-charts/src/context/SeriesContextProvider.tsx @@ -13,6 +13,7 @@ import { FormatterResult, } from '../models/seriesType/config'; import { ChartsColorPalette, blueberryTwilightPalette } from '../colorPalettes'; +import { Initializable } from './context.types'; export type SeriesContextProviderProps = { dataset?: DatasetType; @@ -32,7 +33,10 @@ export type SeriesContextProviderProps = { export type FormattedSeries = { [type in ChartSeriesType]?: FormatterResult }; -export const SeriesContext = React.createContext({}); +export const SeriesContext = React.createContext>({ + isInitialized: false, + data: {}, +}); if (process.env.NODE_ENV !== 'production') { SeriesContext.displayName = 'SeriesContext'; @@ -93,12 +97,14 @@ function SeriesContextProvider(props: SeriesContextProviderProps) { const theme = useTheme(); const formattedSeries = React.useMemo( - () => - formatSeries( + () => ({ + isInitialized: true, + data: formatSeries( series, typeof colors === 'function' ? colors(theme.palette.mode) : colors, dataset as DatasetType, ), + }), [series, colors, theme.palette.mode, dataset], ); diff --git a/packages/x-charts/src/context/context.types.ts b/packages/x-charts/src/context/context.types.ts new file mode 100644 index 000000000000..16af92f28381 --- /dev/null +++ b/packages/x-charts/src/context/context.types.ts @@ -0,0 +1,4 @@ +export type Initializable = { + isInitialized: boolean; + data: T; +}; diff --git a/packages/x-charts/src/hooks/useInteractionItemProps.ts b/packages/x-charts/src/hooks/useInteractionItemProps.ts index 633944cdfd2d..07d9dc7dccbd 100644 --- a/packages/x-charts/src/hooks/useInteractionItemProps.ts +++ b/packages/x-charts/src/hooks/useInteractionItemProps.ts @@ -1,11 +1,11 @@ import * as React from 'react'; import { InteractionContext } from '../context/InteractionProvider'; import { SeriesItemIdentifier } from '../models'; -import { HighlightedContext } from '../context'; +import { useHighlighted } from '../context'; export const useInteractionItemProps = (skip?: boolean) => { const { dispatch: dispatchInteraction } = React.useContext(InteractionContext); - const { setHighlighted, clearHighlighted } = React.useContext(HighlightedContext); + const { setHighlighted, clearHighlighted } = useHighlighted(); if (skip) { return () => ({}); diff --git a/packages/x-charts/src/hooks/useSeries.test.tsx b/packages/x-charts/src/hooks/useSeries.test.tsx new file mode 100644 index 000000000000..392bece9582b --- /dev/null +++ b/packages/x-charts/src/hooks/useSeries.test.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { ErrorBoundary, createRenderer } from '@mui/internal-test-utils'; +import { useSeries } from './useSeries'; +import { SeriesContextProvider } from '../context/SeriesContextProvider'; + +function UseSeries() { + const { bar } = useSeries(); + return
{bar?.series['test-id']?.id}
; +} + +describe('useSeries', () => { + const { render } = createRenderer(); + + it('should throw an error when parent context not present', function test() { + if (!/jsdom/.test(window.navigator.userAgent)) { + // can't catch render errors in the browser for unknown reason + // tried try-catch + error boundary + window onError preventDefault + this.skip(); + } + + const errorRef = React.createRef(); + + expect(() => + render( + + + , + ), + ).toErrorDev([ + 'MUI X: Could not find the series ref context.', + 'It looks like you rendered your component outside of a ChartsContainer parent component.', + 'The above error occurred in the component:', + ]); + + expect((errorRef.current as any).errors).to.have.length(1); + expect((errorRef.current as any).errors[0].toString()).to.include( + 'MUI X: Could not find the series ref context.', + ); + }); + + it('should not throw an error when parent context is present', () => { + const { getByText } = render( + + + , + ); + + expect(getByText('test-id')).toBeVisible(); + }); +}); diff --git a/packages/x-charts/src/hooks/useSeries.ts b/packages/x-charts/src/hooks/useSeries.ts index 12817cb944eb..4b4d7fa6e079 100644 --- a/packages/x-charts/src/hooks/useSeries.ts +++ b/packages/x-charts/src/hooks/useSeries.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { SeriesContext } from '../context/SeriesContextProvider'; +import { FormattedSeries, SeriesContext } from '../context/SeriesContextProvider'; /** * Get access to the internal state of series. @@ -7,10 +7,10 @@ import { SeriesContext } from '../context/SeriesContextProvider'; * { seriesType?: { series: { id1: precessedValue, ... }, seriesOrder: [id1, ...] } } * @returns FormattedSeries series */ -export function useSeries() { - const series = React.useContext(SeriesContext); +export function useSeries(): FormattedSeries { + const { isInitialized, data } = React.useContext(SeriesContext); - if (series === undefined) { + if (!isInitialized) { throw new Error( [ 'MUI X: Could not find the series ref context.', @@ -19,7 +19,7 @@ export function useSeries() { ); } - return series; + return data; } /** @@ -29,7 +29,7 @@ export function useSeries() { * - seriesOrder: the array of series ids. * @returns { series: Record; seriesOrder: SeriesId[]; } | undefined pieSeries */ -export function usePieSeries() { +export function usePieSeries(): FormattedSeries['pie'] { const series = useSeries(); return React.useMemo(() => series.pie, [series.pie]); @@ -42,7 +42,7 @@ export function usePieSeries() { * - seriesOrder: the array of series ids. * @returns { series: Record; seriesOrder: SeriesId[]; } | undefined lineSeries */ -export function useLineSeries() { +export function useLineSeries(): FormattedSeries['line'] { const series = useSeries(); return React.useMemo(() => series.line, [series.line]); @@ -55,7 +55,7 @@ export function useLineSeries() { * - seriesOrder: the array of series ids. * @returns { series: Record; seriesOrder: SeriesId[]; } | undefined barSeries */ -export function useBarSeries() { +export function useBarSeries(): FormattedSeries['bar'] { const series = useSeries(); return React.useMemo(() => series.bar, [series.bar]); @@ -68,7 +68,7 @@ export function useBarSeries() { * - seriesOrder: the array of series ids. * @returns { series: Record; seriesOrder: SeriesId[]; } | undefined scatterSeries */ -export function useScatterSeries() { +export function useScatterSeries(): FormattedSeries['scatter'] { const series = useSeries(); return React.useMemo(() => series.scatter, [series.scatter]); diff --git a/packages/x-charts/src/hooks/useSvgRef.test.tsx b/packages/x-charts/src/hooks/useSvgRef.test.tsx new file mode 100644 index 000000000000..a61113fa1e8d --- /dev/null +++ b/packages/x-charts/src/hooks/useSvgRef.test.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { ErrorBoundary, createRenderer } from '@mui/internal-test-utils'; +import { useSvgRef } from './useSvgRef'; +import { DrawingProvider } from '../context/DrawingProvider'; + +function UseSvgRef() { + const ref = useSvgRef(); + return
{ref.current?.id}
; +} + +describe('useSvgRef', () => { + const { render } = createRenderer(); + + it('should throw an error when parent context not present', function test() { + if (!/jsdom/.test(window.navigator.userAgent)) { + // can't catch render errors in the browser for unknown reason + // tried try-catch + error boundary + window onError preventDefault + this.skip(); + } + + const errorRef = React.createRef(); + + expect(() => + render( + + + , + ), + ).toErrorDev([ + 'MUI X: Could not find the svg ref context.', + 'It looks like you rendered your component outside of a ChartsContainer parent component.', + 'The above error occurred in the component:', + ]); + + expect((errorRef.current as any).errors).to.have.length(1); + expect((errorRef.current as any).errors[0].toString()).to.include( + 'MUI X: Could not find the svg ref context.', + ); + }); + + it('should not throw an error when parent context is present', async () => { + function RenderDrawingProvider() { + const ref = React.useRef(null); + + return ( + + + + + + ); + } + + const { findByText, forceUpdate } = render(); + + // Ref is not available on first render. + forceUpdate(); + + expect(await findByText('test-id')).toBeVisible(); + }); +}); diff --git a/packages/x-charts/src/hooks/useSvgRef.ts b/packages/x-charts/src/hooks/useSvgRef.ts index 268222985047..d2540807afa6 100644 --- a/packages/x-charts/src/hooks/useSvgRef.ts +++ b/packages/x-charts/src/hooks/useSvgRef.ts @@ -2,9 +2,9 @@ import * as React from 'react'; import { SvgContext } from '../context/DrawingProvider'; export function useSvgRef(): React.MutableRefObject { - const svgRef = React.useContext(SvgContext); + const { isInitialized, data } = React.useContext(SvgContext); - if (svgRef === undefined) { + if (!isInitialized) { throw new Error( [ 'MUI X: Could not find the svg ref context.', @@ -13,5 +13,5 @@ export function useSvgRef(): React.MutableRefObject { ); } - return svgRef as React.MutableRefObject; + return data as React.MutableRefObject; }