From 6eaf0faef1d5affb3350af983685be2330033f2d Mon Sep 17 00:00:00 2001 From: Atishay Jain Date: Tue, 15 Sep 2026 21:34:33 +0530 Subject: [PATCH 1/4] fix(react-charts): improve chart callout behavior --- ...-44bddcc6-c2b6-4612-b1cc-b5872295c51a.json | 7 ++++ .../library/etc/react-charts.api.md | 8 ++++ .../CommonComponents/CartesianChart.tsx | 7 ++++ .../components/GaugeChart/GaugeChart.test.tsx | 39 ++++++++++------- .../src/components/GaugeChart/GaugeChart.tsx | 39 +++++++++++------ .../components/GaugeChart/GaugeChart.types.ts | 26 ++++++++++++ .../__snapshots__/GaugeChart.test.tsx.snap | 42 +++++++++---------- .../components/LineChart/LineChart.test.tsx | 18 ++++++++ .../GaugeChart/GaugeChartDefault.stories.tsx | 39 +++++++++++------ 9 files changed, 165 insertions(+), 60 deletions(-) create mode 100644 change/@fluentui-react-charts-44bddcc6-c2b6-4612-b1cc-b5872295c51a.json diff --git a/change/@fluentui-react-charts-44bddcc6-c2b6-4612-b1cc-b5872295c51a.json b/change/@fluentui-react-charts-44bddcc6-c2b6-4612-b1cc-b5872295c51a.json new file mode 100644 index 0000000000000..365cd70c8dbe8 --- /dev/null +++ b/change/@fluentui-react-charts-44bddcc6-c2b6-4612-b1cc-b5872295c51a.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: improve LineChart and GaugeChart callout behavior and formatting", + "packageName": "@fluentui/react-charts", + "email": "atisjai@microsoft.com", + "dependentChangeType": "none" +} \ No newline at end of file diff --git a/packages/charts/react-charts/library/etc/react-charts.api.md b/packages/charts/react-charts/library/etc/react-charts.api.md index c23df75798703..e3e09e3dea0d5 100644 --- a/packages/charts/react-charts/library/etc/react-charts.api.md +++ b/packages/charts/react-charts/library/etc/react-charts.api.md @@ -897,14 +897,22 @@ export const GaugeChart: React_2.FunctionComponent; // @public export interface GaugeChartCalloutData { + chartTitle?: string; chartValue: number; chartValueLabel: string; legend: string; maxValue: number; minValue: number; + segments: GaugeChartCalloutSegment[]; segmentValues: YValueHover[]; } +// @public +export interface GaugeChartCalloutSegment extends GaugeChartSegment { + end: number; + start: number; +} + // @public export interface GaugeChartProps { calloutProps?: Partial; diff --git a/packages/charts/react-charts/library/src/components/CommonComponents/CartesianChart.tsx b/packages/charts/react-charts/library/src/components/CommonComponents/CartesianChart.tsx index e878b0320fbb2..6475ab285b2e0 100644 --- a/packages/charts/react-charts/library/src/components/CommonComponents/CartesianChart.tsx +++ b/packages/charts/react-charts/library/src/components/CommonComponents/CartesianChart.tsx @@ -531,6 +531,12 @@ export const CartesianChart: React.FunctionComponent): void { + if (!event.relatedTarget || !event.currentTarget.contains(event.relatedTarget as Node)) { + _onChartLeave(); + } + } + function _calculateChartMinWidth(): number { // Adding 10px for padding on both sides const labelWidth = _calcMaxLabelWidthWithTransform(_tickLabels) + 10; @@ -746,6 +752,7 @@ export const CartesianChart: React.FunctionComponent { chartContainer.current = rootElem; }} + onBlur={_onChartBlur} onMouseLeave={_onChartLeave} >
diff --git a/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.test.tsx b/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.test.tsx index 5b05e8d8d7368..5c6b39b9b8d0d 100644 --- a/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.test.tsx +++ b/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.test.tsx @@ -392,19 +392,26 @@ describe('GaugeChart custom callout', () => { { segments, chartValue: 30, + chartTitle: 'Server tick time', minValue: 10, maxValue: 110, - onRenderCallout: (calloutData?: GaugeChartCalloutData) => ( -
- {calloutData?.legend}: {calloutData?.chartValue} blocks ({calloutData?.minValue}-{calloutData?.maxValue}) -
- ), + onRenderCallout: (calloutData?: GaugeChartCalloutData) => { + const lastSegment = calloutData?.segments.at(-1); + return ( +
+ {calloutData?.chartTitle}: {calloutData?.legend}: {calloutData?.chartValue} blocks ({calloutData?.minValue}- + {calloutData?.maxValue}); last range {lastSegment?.start}-{lastSegment?.end} +
+ ); + }, }, () => { const chartSegments = screen.getAllByText((content, element) => element!.tagName.toLowerCase() === 'path'); fireEvent.mouseOver(chartSegments[0]); - expect(screen.getByTestId('custom-gauge-callout')).toHaveTextContent('Low Risk: 30 blocks (10-110)'); + expect(screen.getByTestId('custom-gauge-callout')).toHaveTextContent( + 'Server tick time: Low Risk: 30 blocks (10-110); last range 77-110', + ); }, ); @@ -414,13 +421,14 @@ describe('GaugeChart custom callout', () => { { segments, chartValue: 30, + chartValueFormat: ([value]) => `${value}ms`, onRenderCallout: renderDefaultCallout, }, () => { const chartSegments = screen.getAllByText((content, element) => element!.tagName.toLowerCase() === 'path'); fireEvent.mouseOver(chartSegments[0]); - expect(screen.getByTestId('wrapped-gauge-callout')).toHaveTextContent('Current value is 30/100'); + expect(screen.getByTestId('wrapped-gauge-callout')).toHaveTextContent('Current value is 30ms'); expect(screen.getByTestId('wrapped-gauge-callout')).toHaveTextContent('Low Risk'); }, ); @@ -504,30 +512,33 @@ describe('GaugeChart rendering and behavior tests', () => { it('should render the chart value correctly', () => { const customChartValue = 'Custom chart value'; + const formatMilliseconds = ([value]: [number, number]) => (value === 0 ? 'offline' : `${value}ms`); expect(getChartValueLabel(25, 0, 100)).toBe('25%'); - expect(getChartValueLabel(25, 0, 100, undefined, true)).toBe('25/100'); + expect(getChartValueLabel(25, 0, 100, undefined, true)).toBe('25%'); expect(getChartValueLabel(25, 0, 100, GaugeValueFormat.Percentage)).toBe('25%'); - expect(getChartValueLabel(25, 0, 100, GaugeValueFormat.Percentage, true)).toBe('25/100'); + expect(getChartValueLabel(25, 0, 100, GaugeValueFormat.Percentage, true)).toBe('25%'); expect(getChartValueLabel(25, 0, 100, GaugeValueFormat.Fraction)).toBe('25/100'); - expect(getChartValueLabel(25, 0, 100, GaugeValueFormat.Fraction, true)).toBe('25%'); + expect(getChartValueLabel(25, 0, 100, GaugeValueFormat.Fraction, true)).toBe('25/100'); expect(getChartValueLabel(25, 0, 100, () => customChartValue)).toBe(customChartValue); - expect(getChartValueLabel(25, 0, 100, () => customChartValue, true)).toBe('25/100'); + expect(getChartValueLabel(25, 0, 100, () => customChartValue, true)).toBe(customChartValue); + expect(getChartValueLabel(50, 0, 200, formatMilliseconds, true)).toBe('50ms'); + expect(getChartValueLabel(0, 0, 200, formatMilliseconds, true)).toBe('offline'); expect(getChartValueLabel(125, 100, 200)).toBe('125'); expect(getChartValueLabel(125, 100, 200, undefined, true)).toBe('125'); - expect(getChartValueLabel(125, 100, 200, GaugeValueFormat.Percentage)).toBe('125'); - expect(getChartValueLabel(125, 100, 200, GaugeValueFormat.Percentage, true)).toBe('125'); + expect(getChartValueLabel(125, 100, 200, GaugeValueFormat.Percentage)).toBe('25%'); + expect(getChartValueLabel(125, 100, 200, GaugeValueFormat.Percentage, true)).toBe('25%'); expect(getChartValueLabel(125, 100, 200, GaugeValueFormat.Fraction)).toBe('125'); expect(getChartValueLabel(125, 100, 200, GaugeValueFormat.Fraction, true)).toBe('125'); expect(getChartValueLabel(125, 100, 200, () => customChartValue)).toBe(customChartValue); - expect(getChartValueLabel(125, 100, 200, () => customChartValue, true)).toBe('125'); + expect(getChartValueLabel(125, 100, 200, () => customChartValue, true)).toBe(customChartValue); }); }); diff --git a/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.tsx b/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.tsx index e4443925c2c30..af7d0864cb27a 100644 --- a/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.tsx +++ b/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.tsx @@ -83,25 +83,38 @@ export const getChartValueLabel = ( chartValueFormat?: GaugeValueFormat | ((sweepFraction: [number, number]) => string), forCallout: boolean = false, ): string => { - if (forCallout) { - // When displaying the chart value as a percentage, use fractions in the callout, and vice versa. - // This helps clarify the actual value and avoid repetition. - return minValue !== 0 - ? chartValue.toString() - : chartValueFormat === 'fraction' - ? `${((chartValue / maxValue) * 100).toFixed()}%` - : `${chartValue}/${maxValue}`; + if (typeof chartValueFormat === 'function') { + return chartValueFormat([chartValue - minValue, maxValue - minValue]); } - return typeof chartValueFormat === 'function' - ? chartValueFormat([chartValue - minValue, maxValue - minValue]) - : minValue !== 0 + if (chartValueFormat === 'percentage') { + return `${(((chartValue - minValue) / (maxValue - minValue)) * 100).toFixed()}%`; + } + + return minValue !== 0 ? chartValue.toString() : chartValueFormat === 'fraction' ? `${chartValue}/${maxValue}` : `${((chartValue / maxValue) * 100).toFixed()}%`; }; +const getCalloutSegmentLabel = ( + segment: ExtendedSegment, + minValue: number, + maxValue: number, + variant: GaugeChartVariant | undefined, + chartValueFormat: GaugeChartProps['chartValueFormat'], +): string => { + if (chartValueFormat === 'percentage' || (!chartValueFormat && minValue === 0)) { + const range = maxValue - minValue; + const startPercentage = (((segment.start - minValue) / range) * 100).toFixed(); + const endPercentage = (((segment.end - minValue) / range) * 100).toFixed(); + return `${startPercentage}% - ${endPercentage}%`; + } + + return getSegmentLabel(segment, minValue, maxValue, variant); +}; + interface YValue extends Omit { y?: string | number; } @@ -396,7 +409,7 @@ export const GaugeChart: React.FunctionComponent = React.forwar .map(segment => { const yValue: YValue = { legend: segment.legend, - y: getSegmentLabel(segment, _minValue, _maxValue, props.variant), + y: getCalloutSegmentLabel(segment, _minValue, _maxValue, props.variant, props.chartValueFormat), color: segment.color, }; return yValue; @@ -726,6 +739,7 @@ export const GaugeChart: React.FunctionComponent = React.forwar (() => { const calloutData: GaugeChartCalloutData = { legend: calloutLegend, + chartTitle: props.chartTitle, chartValue: props.chartValue, minValue: _minValue, maxValue: _maxValue, @@ -736,6 +750,7 @@ export const GaugeChart: React.FunctionComponent = React.forwar props.chartValueFormat, true, ), + segments: _segments, segmentValues: hoverYValues, }; diff --git a/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.types.ts b/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.types.ts index 1b5c41c4c27ba..e54276a0fd9ca 100644 --- a/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.types.ts +++ b/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.types.ts @@ -46,6 +46,22 @@ export type GaugeValueFormat = 'percentage' | 'fraction'; */ export type GaugeChartVariant = 'single-segment' | 'multiple-segments'; +/** + * GaugeChart segment data with its calculated range. + * {@docCategory GaugeChart} + */ +export interface GaugeChartCalloutSegment extends GaugeChartSegment { + /** + * Start of the segment range. + */ + start: number; + + /** + * End of the segment range. + */ + end: number; +} + /** * Data provided to a custom GaugeChart callout renderer. * {@docCategory GaugeChart} @@ -56,6 +72,11 @@ export interface GaugeChartCalloutData { */ legend: string; + /** + * Title of the gauge. + */ + chartTitle?: string; + /** * Current value of the gauge. */ @@ -76,6 +97,11 @@ export interface GaugeChartCalloutData { */ chartValueLabel: string; + /** + * Gauge segments with their calculated ranges. + */ + segments: GaugeChartCalloutSegment[]; + /** * Segment values displayed in the default callout. */ diff --git a/packages/charts/react-charts/library/src/components/GaugeChart/__snapshots__/GaugeChart.test.tsx.snap b/packages/charts/react-charts/library/src/components/GaugeChart/__snapshots__/GaugeChart.test.tsx.snap index 7a592e63f9960..35d0e9c27ea6e 100644 --- a/packages/charts/react-charts/library/src/components/GaugeChart/__snapshots__/GaugeChart.test.tsx.snap +++ b/packages/charts/react-charts/library/src/components/GaugeChart/__snapshots__/GaugeChart.test.tsx.snap @@ -953,7 +953,7 @@ exports[`Gauge chart interactions Should hide callout on mouse leave 1`] = ` data-is-focusable="false" role="text" > - Current value is 30/100 + Current value is 30%
@@ -964,7 +964,7 @@ exports[`Gauge chart interactions Should hide callout on mouse leave 1`] = `
@@ -977,7 +977,7 @@ exports[`Gauge chart interactions Should hide callout on mouse leave 1`] = `
- 0 - 33 + 0% - 33%
@@ -990,7 +990,7 @@ exports[`Gauge chart interactions Should hide callout on mouse leave 1`] = `
@@ -1003,7 +1003,7 @@ exports[`Gauge chart interactions Should hide callout on mouse leave 1`] = `
- 33 - 67 + 33% - 67%
@@ -1016,7 +1016,7 @@ exports[`Gauge chart interactions Should hide callout on mouse leave 1`] = `
@@ -1029,7 +1029,7 @@ exports[`Gauge chart interactions Should hide callout on mouse leave 1`] = `
- 67 - 100 + 67% - 100%
@@ -1684,7 +1684,7 @@ exports[`Gauge chart interactions Should show callout on focus 1`] = ` data-is-focusable="false" role="text" > - Current value is 30/100 + Current value is 30%
@@ -1695,7 +1695,7 @@ exports[`Gauge chart interactions Should show callout on focus 1`] = `
@@ -1708,7 +1708,7 @@ exports[`Gauge chart interactions Should show callout on focus 1`] = `
- 0 - 33 + 0% - 33%
@@ -1721,7 +1721,7 @@ exports[`Gauge chart interactions Should show callout on focus 1`] = `
@@ -1734,7 +1734,7 @@ exports[`Gauge chart interactions Should show callout on focus 1`] = `
- 33 - 67 + 33% - 67%
@@ -1747,7 +1747,7 @@ exports[`Gauge chart interactions Should show callout on focus 1`] = `
@@ -1760,7 +1760,7 @@ exports[`Gauge chart interactions Should show callout on focus 1`] = `
- 67 - 100 + 67% - 100%
@@ -1999,7 +1999,7 @@ exports[`Gauge chart interactions Should show callout on mouse over 1`] = ` data-is-focusable="false" role="text" > - Current value is 30/100 + Current value is 30%
@@ -2010,7 +2010,7 @@ exports[`Gauge chart interactions Should show callout on mouse over 1`] = `
@@ -2023,7 +2023,7 @@ exports[`Gauge chart interactions Should show callout on mouse over 1`] = `
- 0 - 33 + 0% - 33%
@@ -2036,7 +2036,7 @@ exports[`Gauge chart interactions Should show callout on mouse over 1`] = `
@@ -2049,7 +2049,7 @@ exports[`Gauge chart interactions Should show callout on mouse over 1`] = `
- 33 - 67 + 33% - 67%
@@ -2062,7 +2062,7 @@ exports[`Gauge chart interactions Should show callout on mouse over 1`] = `
@@ -2075,7 +2075,7 @@ exports[`Gauge chart interactions Should show callout on mouse over 1`] = `
- 67 - 100 + 67% - 100%
diff --git a/packages/charts/react-charts/library/src/components/LineChart/LineChart.test.tsx b/packages/charts/react-charts/library/src/components/LineChart/LineChart.test.tsx index 717a2acaa42f6..15074044df82b 100644 --- a/packages/charts/react-charts/library/src/components/LineChart/LineChart.test.tsx +++ b/packages/charts/react-charts/library/src/components/LineChart/LineChart.test.tsx @@ -1016,4 +1016,22 @@ describe('LineChart - mouse events', () => { expect(container.querySelector('pre')).toBeDefined(); expect(container).toMatchSnapshot(); }); + + it('Should dismiss the callout when focus leaves the chart', () => { + const { container } = render( + <> + + + , + { container: root! }, + ); + const dataPoint = container.querySelector('[id^="circle"][tabindex="0"]'); + + expect(dataPoint).not.toBeNull(); + fireEvent.focus(dataPoint!); + expect(getByClass(container, /calloutContentRoot/i)).toHaveLength(1); + + fireEvent.blur(dataPoint!, { relatedTarget: screen.getByTestId('before-chart') }); + expect(getByClass(container, /calloutContentRoot/i)).toHaveLength(0); + }); }); diff --git a/packages/charts/react-charts/stories/src/GaugeChart/GaugeChartDefault.stories.tsx b/packages/charts/react-charts/stories/src/GaugeChart/GaugeChartDefault.stories.tsx index 5c71fb65e65f6..551409529885f 100644 --- a/packages/charts/react-charts/stories/src/GaugeChart/GaugeChartDefault.stories.tsx +++ b/packages/charts/react-charts/stories/src/GaugeChart/GaugeChartDefault.stories.tsx @@ -14,6 +14,13 @@ const useStyles = makeStyles({ fontSize: tokens.fontSizeBase400, fontWeight: tokens.fontWeightSemibold, }, + calloutTitle: { + fontSize: tokens.fontSizeBase500, + fontWeight: tokens.fontWeightSemibold, + }, + calloutDescription: { + color: tokens.colorNeutralForeground2, + }, }); export const GaugeChartBasic = (): JSXElement => { @@ -63,11 +70,15 @@ export const GaugeChartBasic = (): JSXElement => { return (
- {data.legend} - Risk score: {data.chartValue} - - Range: {data.minValue} to {data.maxValue} - + {data.chartTitle} + Average time required to process one server tick. + Current value: {data.chartValueLabel} + {data.segments.map((segment, index) => ( + + {segment.legend}: {segment.start} + {index === data.segments.length - 1 ? '+' : `-${segment.end}`} ms + + ))}
); }; @@ -109,11 +120,11 @@ export const GaugeChartBasic = (): JSXElement => { type="range" value={chartValue} min={0} - max={100} + max={200} id="value-slider" onChange={_onValueChange} aria-label="Change Current Value" - aria-valuetext={`current value ${chartValue}', Minimum 0 and Maximum 100`} + aria-valuetext={`current value ${chartValue}, Minimum 0 and Maximum 200`} /> {chartValue}
@@ -152,22 +163,24 @@ export const GaugeChartBasic = (): JSXElement => { height={height} segments={[ { - size: 33, + size: 50, color: getColorFromToken(DataVizPalette.success), - legend: 'Low Risk', + legend: 'Healthy', }, { - size: 34, + size: 100, color: getColorFromToken(DataVizPalette.warning), - legend: 'Medium Risk', + legend: 'Elevated', }, { - size: 33, + size: 50, color: getColorFromToken(DataVizPalette.error), - legend: 'High Risk', + legend: 'Critical', }, ]} + chartTitle="Server tick time" chartValue={chartValue} + chartValueFormat={useCustomCallout ? ([value]) => (value === 0 ? 'offline' : `${value}ms`) : 'percentage'} hideMinMax={hideMinMax} variant={'multiple-segments'} enableGradient={enableGradient} From ce2b5ce93741847b764d63908595b280d697e0fa Mon Sep 17 00:00:00 2001 From: Atishay Jain Date: Tue, 15 Sep 2026 21:34:39 +0530 Subject: [PATCH 2/4] fix(storybook): preserve addon options on Windows --- ...-c1886082-d846-42f3-8a23-e405cf1c76aa.json | 7 ++++ .../src/webpack.spec.ts | 34 ++++++++++++++++--- .../src/webpack.ts | 3 +- 3 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 change/@fluentui-react-storybook-addon-export-to-sandbox-c1886082-d846-42f3-8a23-e405cf1c76aa.json diff --git a/change/@fluentui-react-storybook-addon-export-to-sandbox-c1886082-d846-42f3-8a23-e405cf1c76aa.json b/change/@fluentui-react-storybook-addon-export-to-sandbox-c1886082-d846-42f3-8a23-e405cf1c76aa.json new file mode 100644 index 0000000000000..36f99848d1a38 --- /dev/null +++ b/change/@fluentui-react-storybook-addon-export-to-sandbox-c1886082-d846-42f3-8a23-e405cf1c76aa.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: preserve export-to-sandbox options on Windows Storybook paths", + "packageName": "@fluentui/react-storybook-addon-export-to-sandbox", + "email": "atisjai@microsoft.com", + "dependentChangeType": "none" +} \ No newline at end of file diff --git a/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.spec.ts b/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.spec.ts index bfc9fc0e4dbf0..c0dee54e4ea7e 100644 --- a/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.spec.ts +++ b/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.spec.ts @@ -23,7 +23,7 @@ describe(`webpack`, () => { plugins: [ [ expect.stringContaining('babel-preset-storybook-full-source'), - { importMappings: undefined, cssModules: false }, + { importMappings: {}, cssModules: false }, ], ], }, @@ -76,6 +76,33 @@ describe(`webpack`, () => { ]); }); + it(`should register user provided options from a Windows preset path`, () => { + const actual = webpack({ module: { rules: [] } }, { + presetsList: [ + { + name: 'node_modules\\@fluentui\\react-storybook-addon-export-to-sandbox\\lib\\preset.js', + preset: {}, + options: { + importMappings: { '@proj/foo': { replace: '@proj/moo' } }, + } as PresetConfig, + }, + ], + } as WebpackFinalOptions); + + expect(actual.module?.rules?.[0]).toMatchObject({ + use: { + options: { + plugins: [ + [ + expect.stringContaining('babel-preset-storybook-full-source'), + { importMappings: { '@proj/foo': { replace: '@proj/moo' } }, cssModules: false }, + ], + ], + }, + }, + }); + }); + it.each([ ['boolean true', true as const], ['object with tokensFilePath', { tokensFilePath: '/path/to/tokens.css' }], @@ -99,10 +126,7 @@ describe(`webpack`, () => { options: { parserOpts: { plugins: ['typescript', 'jsx'] }, plugins: [ - [ - expect.stringContaining('babel-preset-storybook-full-source'), - { importMappings: undefined, cssModules }, - ], + [expect.stringContaining('babel-preset-storybook-full-source'), { importMappings: {}, cssModules }], ], }, }, diff --git a/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.ts b/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.ts index 8ebe39c87ae43..6691cda351875 100644 --- a/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.ts +++ b/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.ts @@ -16,8 +16,9 @@ export function webpack(config: WebpackFinalConfig, options: WebpackFinalOptions } const identity = (value: T) => value; -const addonFilePattern = /react-storybook-addon-export-to-sandbox\/[a-z/]+.[jt]s$/; +const addonFilePattern = /react-storybook-addon-export-to-sandbox[\\/][a-z\\/]+\.[jt]s$/; const defaultOptions = { + importMappings: {}, webpackRule: {}, babelLoaderOptionsUpdater: identity, cssModules: false, From 8711cce1de84fc6babb9117de89c9eec3dfab1f6 Mon Sep 17 00:00:00 2001 From: Atishay Jain Date: Tue, 15 Sep 2026 21:37:15 +0530 Subject: [PATCH 3/4] Revert "fix(storybook): preserve addon options on Windows" This reverts commit ce2b5ce93741847b764d63908595b280d697e0fa. --- ...-c1886082-d846-42f3-8a23-e405cf1c76aa.json | 7 ---- .../src/webpack.spec.ts | 34 +++---------------- .../src/webpack.ts | 3 +- 3 files changed, 6 insertions(+), 38 deletions(-) delete mode 100644 change/@fluentui-react-storybook-addon-export-to-sandbox-c1886082-d846-42f3-8a23-e405cf1c76aa.json diff --git a/change/@fluentui-react-storybook-addon-export-to-sandbox-c1886082-d846-42f3-8a23-e405cf1c76aa.json b/change/@fluentui-react-storybook-addon-export-to-sandbox-c1886082-d846-42f3-8a23-e405cf1c76aa.json deleted file mode 100644 index 36f99848d1a38..0000000000000 --- a/change/@fluentui-react-storybook-addon-export-to-sandbox-c1886082-d846-42f3-8a23-e405cf1c76aa.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "patch", - "comment": "fix: preserve export-to-sandbox options on Windows Storybook paths", - "packageName": "@fluentui/react-storybook-addon-export-to-sandbox", - "email": "atisjai@microsoft.com", - "dependentChangeType": "none" -} \ No newline at end of file diff --git a/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.spec.ts b/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.spec.ts index c0dee54e4ea7e..bfc9fc0e4dbf0 100644 --- a/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.spec.ts +++ b/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.spec.ts @@ -23,7 +23,7 @@ describe(`webpack`, () => { plugins: [ [ expect.stringContaining('babel-preset-storybook-full-source'), - { importMappings: {}, cssModules: false }, + { importMappings: undefined, cssModules: false }, ], ], }, @@ -76,33 +76,6 @@ describe(`webpack`, () => { ]); }); - it(`should register user provided options from a Windows preset path`, () => { - const actual = webpack({ module: { rules: [] } }, { - presetsList: [ - { - name: 'node_modules\\@fluentui\\react-storybook-addon-export-to-sandbox\\lib\\preset.js', - preset: {}, - options: { - importMappings: { '@proj/foo': { replace: '@proj/moo' } }, - } as PresetConfig, - }, - ], - } as WebpackFinalOptions); - - expect(actual.module?.rules?.[0]).toMatchObject({ - use: { - options: { - plugins: [ - [ - expect.stringContaining('babel-preset-storybook-full-source'), - { importMappings: { '@proj/foo': { replace: '@proj/moo' } }, cssModules: false }, - ], - ], - }, - }, - }); - }); - it.each([ ['boolean true', true as const], ['object with tokensFilePath', { tokensFilePath: '/path/to/tokens.css' }], @@ -126,7 +99,10 @@ describe(`webpack`, () => { options: { parserOpts: { plugins: ['typescript', 'jsx'] }, plugins: [ - [expect.stringContaining('babel-preset-storybook-full-source'), { importMappings: {}, cssModules }], + [ + expect.stringContaining('babel-preset-storybook-full-source'), + { importMappings: undefined, cssModules }, + ], ], }, }, diff --git a/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.ts b/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.ts index 6691cda351875..8ebe39c87ae43 100644 --- a/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.ts +++ b/packages/react-components/react-storybook-addon-export-to-sandbox/src/webpack.ts @@ -16,9 +16,8 @@ export function webpack(config: WebpackFinalConfig, options: WebpackFinalOptions } const identity = (value: T) => value; -const addonFilePattern = /react-storybook-addon-export-to-sandbox[\\/][a-z\\/]+\.[jt]s$/; +const addonFilePattern = /react-storybook-addon-export-to-sandbox\/[a-z/]+.[jt]s$/; const defaultOptions = { - importMappings: {}, webpackRule: {}, babelLoaderOptionsUpdater: identity, cssModules: false, From 740429c34d54e322fa1557ac334793263f239242 Mon Sep 17 00:00:00 2001 From: Atishay Jain Date: Wed, 16 Sep 2026 08:54:38 +0530 Subject: [PATCH 4/4] fix(react-charts): address callout review feedback --- .../library/etc/react-charts.api.md | 2 +- .../components/GaugeChart/GaugeChart.test.tsx | 13 ++++---- .../src/components/GaugeChart/GaugeChart.tsx | 12 +++---- .../components/GaugeChart/GaugeChart.types.ts | 4 ++- .../components/LineChart/LineChart.test.tsx | 32 +++++++++++++++++++ .../GaugeChart/GaugeChartDefault.stories.tsx | 2 +- 6 files changed, 50 insertions(+), 15 deletions(-) diff --git a/packages/charts/react-charts/library/etc/react-charts.api.md b/packages/charts/react-charts/library/etc/react-charts.api.md index e3e09e3dea0d5..bd7931fd75870 100644 --- a/packages/charts/react-charts/library/etc/react-charts.api.md +++ b/packages/charts/react-charts/library/etc/react-charts.api.md @@ -903,7 +903,7 @@ export interface GaugeChartCalloutData { legend: string; maxValue: number; minValue: number; - segments: GaugeChartCalloutSegment[]; + segments?: GaugeChartCalloutSegment[]; segmentValues: YValueHover[]; } diff --git a/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.test.tsx b/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.test.tsx index 5c6b39b9b8d0d..0c629d6813bd1 100644 --- a/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.test.tsx +++ b/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.test.tsx @@ -394,9 +394,9 @@ describe('GaugeChart custom callout', () => { chartValue: 30, chartTitle: 'Server tick time', minValue: 10, - maxValue: 110, + maxValue: 120, onRenderCallout: (calloutData?: GaugeChartCalloutData) => { - const lastSegment = calloutData?.segments.at(-1); + const lastSegment = calloutData?.segments?.at(-1); return (
{calloutData?.chartTitle}: {calloutData?.legend}: {calloutData?.chartValue} blocks ({calloutData?.minValue}- @@ -410,8 +410,9 @@ describe('GaugeChart custom callout', () => { fireEvent.mouseOver(chartSegments[0]); expect(screen.getByTestId('custom-gauge-callout')).toHaveTextContent( - 'Server tick time: Low Risk: 30 blocks (10-110); last range 77-110', + 'Server tick time: Low Risk: 30 blocks (10-120); last range 77-110', ); + expect(screen.getByTestId('custom-gauge-callout')).not.toHaveTextContent('Unknown'); }, ); @@ -421,7 +422,7 @@ describe('GaugeChart custom callout', () => { { segments, chartValue: 30, - chartValueFormat: ([value]) => `${value}ms`, + chartValueFormat: ([value]: [number, number]) => `${value}ms`, onRenderCallout: renderDefaultCallout, }, () => { @@ -534,8 +535,8 @@ describe('GaugeChart rendering and behavior tests', () => { expect(getChartValueLabel(125, 100, 200, GaugeValueFormat.Percentage)).toBe('25%'); expect(getChartValueLabel(125, 100, 200, GaugeValueFormat.Percentage, true)).toBe('25%'); - expect(getChartValueLabel(125, 100, 200, GaugeValueFormat.Fraction)).toBe('125'); - expect(getChartValueLabel(125, 100, 200, GaugeValueFormat.Fraction, true)).toBe('125'); + expect(getChartValueLabel(125, 100, 200, GaugeValueFormat.Fraction)).toBe('25/100'); + expect(getChartValueLabel(125, 100, 200, GaugeValueFormat.Fraction, true)).toBe('25/100'); expect(getChartValueLabel(125, 100, 200, () => customChartValue)).toBe(customChartValue); expect(getChartValueLabel(125, 100, 200, () => customChartValue, true)).toBe(customChartValue); diff --git a/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.tsx b/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.tsx index af7d0864cb27a..ddcf6bd2242cd 100644 --- a/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.tsx +++ b/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.tsx @@ -91,11 +91,11 @@ export const getChartValueLabel = ( return `${(((chartValue - minValue) / (maxValue - minValue)) * 100).toFixed()}%`; } - return minValue !== 0 - ? chartValue.toString() - : chartValueFormat === 'fraction' - ? `${chartValue}/${maxValue}` - : `${((chartValue / maxValue) * 100).toFixed()}%`; + if (chartValueFormat === 'fraction') { + return `${chartValue - minValue}/${maxValue - minValue}`; + } + + return minValue !== 0 ? chartValue.toString() : `${((chartValue / maxValue) * 100).toFixed()}%`; }; const getCalloutSegmentLabel = ( @@ -750,7 +750,7 @@ export const GaugeChart: React.FunctionComponent = React.forwar props.chartValueFormat, true, ), - segments: _segments, + segments: _segments.slice(0, props.segments.length).map(segment => ({ ...segment })), segmentValues: hoverYValues, }; diff --git a/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.types.ts b/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.types.ts index e54276a0fd9ca..9c1f5eb202ef1 100644 --- a/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.types.ts +++ b/packages/charts/react-charts/library/src/components/GaugeChart/GaugeChart.types.ts @@ -100,7 +100,7 @@ export interface GaugeChartCalloutData { /** * Gauge segments with their calculated ranges. */ - segments: GaugeChartCalloutSegment[]; + segments?: GaugeChartCalloutSegment[]; /** * Segment values displayed in the default callout. @@ -167,6 +167,8 @@ export interface GaugeChartProps { /** * Format of the chart value + * A custom formatter applies to the chart value and the current value in the callout. + * Use `onRenderCallout` to customize units for segment ranges. * @defaultvalue GaugeValueFormat.Percentage */ chartValueFormat?: GaugeValueFormat | ((sweepFraction: [number, number]) => string); diff --git a/packages/charts/react-charts/library/src/components/LineChart/LineChart.test.tsx b/packages/charts/react-charts/library/src/components/LineChart/LineChart.test.tsx index 15074044df82b..197299d9809cc 100644 --- a/packages/charts/react-charts/library/src/components/LineChart/LineChart.test.tsx +++ b/packages/charts/react-charts/library/src/components/LineChart/LineChart.test.tsx @@ -1034,4 +1034,36 @@ describe('LineChart - mouse events', () => { fireEvent.blur(dataPoint!, { relatedTarget: screen.getByTestId('before-chart') }); expect(getByClass(container, /calloutContentRoot/i)).toHaveLength(0); }); + + it('Should keep the callout open when focus moves within the chart', () => { + const { container } = render(, { container: root! }); + const dataPoints = container.querySelectorAll('[id^="circle"][tabindex="0"]'); + + expect(dataPoints.length).toBeGreaterThan(1); + fireEvent.focus(dataPoints[0]); + expect(getByClass(container, /calloutContentRoot/i)).toHaveLength(1); + + fireEvent.blur(dataPoints[0], { relatedTarget: dataPoints[1] }); + expect(getByClass(container, /calloutContentRoot/i)).toHaveLength(1); + }); + + it('Should keep the callout open when focus moves into custom callout content', () => { + const onRenderCalloutPerDataPoint = () => ; + const { container } = render( + , + { container: root! }, + ); + const dataPoint = container.querySelector('[id^="circle"][tabindex="0"]'); + + expect(dataPoint).not.toBeNull(); + fireEvent.focus(dataPoint!); + const calloutAction = screen.getByTestId('callout-action'); + + fireEvent.blur(dataPoint!, { relatedTarget: calloutAction }); + expect(screen.getByTestId('callout-action')).toBeInTheDocument(); + }); }); diff --git a/packages/charts/react-charts/stories/src/GaugeChart/GaugeChartDefault.stories.tsx b/packages/charts/react-charts/stories/src/GaugeChart/GaugeChartDefault.stories.tsx index 551409529885f..faf33ab7c9461 100644 --- a/packages/charts/react-charts/stories/src/GaugeChart/GaugeChartDefault.stories.tsx +++ b/packages/charts/react-charts/stories/src/GaugeChart/GaugeChartDefault.stories.tsx @@ -73,7 +73,7 @@ export const GaugeChartBasic = (): JSXElement => { {data.chartTitle} Average time required to process one server tick. Current value: {data.chartValueLabel} - {data.segments.map((segment, index) => ( + {data.segments?.map((segment, index) => ( {segment.legend}: {segment.start} {index === data.segments.length - 1 ? '+' : `-${segment.end}`} ms