From f255301783605b26967322d358436b7422246291 Mon Sep 17 00:00:00 2001 From: srmukher <120183316+srmukher@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:12:22 +0000 Subject: [PATCH 1/3] Enable multiple legend selection for Horizontal bar chart with axis --- .../HorizontalBarChartWithAxis.base.tsx | 18 ++++++++++++++++-- ...orizontalBarChartWithAxis.Basic.Example.tsx | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.base.tsx b/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.base.tsx index 8cb522e55edd4..8f8a10b64fb23 100644 --- a/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.base.tsx +++ b/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.base.tsx @@ -54,6 +54,7 @@ export interface IHorizontalBarChartWithAxisState extends IBasestate { callOutAccessibilityData?: IAccessibilityProps; // eslint-disable-next-line @typescript-eslint/no-explicit-any tooltipElement?: any; + selectedLegends: string[]; } type ColorScale = (_p?: number) => string; @@ -93,6 +94,7 @@ export class HorizontalBarChartWithAxisBase extends React.Component< activeXdataPoint: null, YValueHover: [], hoverXValue: '', + selectedLegends: [], }; this._calloutId = getId('callout'); this._tooltipId = getId('HBCWATooltipID_'); @@ -337,7 +339,10 @@ export class HorizontalBarChartWithAxisBase extends React.Component< const { YValueHover, hoverXValue } = this._getCalloutContentForBar(point); if ( (this.state.isLegendSelected === false || - (this.state.isLegendSelected && this.state.selectedLegendTitle === point.legend)) && + (this.state.isLegendSelected && + (this.state.selectedLegendTitle === point.legend || + this.state.selectedLegends.length === 0 || + this.state.selectedLegends.includes(point.legend!)))) && this._calloutAnchorPoint !== point ) { this._calloutAnchorPoint = point; @@ -457,7 +462,10 @@ export class HorizontalBarChartWithAxisBase extends React.Component< const bars = sortedBars.map((point: IHorizontalBarChartWithAxisDataPoint, index: number) => { let shouldHighlight = true; if (this.state.isLegendHovered || this.state.isLegendSelected) { - shouldHighlight = this.state.selectedLegendTitle === point.legend; + shouldHighlight = + this.state.selectedLegendTitle === point.legend || + this.state.selectedLegends.length === 0 || + this.state.selectedLegends?.includes(point.legend!); } this._classNames = getClassNames(this.props.styles!, { theme: this.props.theme!, @@ -780,11 +788,17 @@ export class HorizontalBarChartWithAxisBase extends React.Component< focusZonePropsInHoverCard={this.props.focusZonePropsForLegendsInHoverCard} overflowText={this.props.legendsOverflowText} {...this.props.legendProps} + canSelectMultipleLegends={this.props.legendProps?.canSelectMultipleLegends} + onChange={this._onLegendChange} /> ); return legends; }; + private _onLegendChange = (selectedLegends: string[]) => { + this.setState({ selectedLegends }); + }; + private _getAxisData = (yAxisData: IAxisData) => { if (yAxisData && yAxisData.yAxisDomainValues.length) { // For HBCWA x and y Values are swapped diff --git a/packages/react-examples/src/react-charting/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.Basic.Example.tsx b/packages/react-examples/src/react-charting/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.Basic.Example.tsx index 2aeb08145031c..d72aeadce861a 100644 --- a/packages/react-examples/src/react-charting/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.Basic.Example.tsx +++ b/packages/react-examples/src/react-charting/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.Basic.Example.tsx @@ -18,6 +18,7 @@ interface IHorizontalBarChartWithAxisState { useSingleColor: boolean; enableGradient: boolean; roundCorners: boolean; + selectMultipleLegends: boolean; } const options: IChoiceGroupOption[] = [ @@ -38,6 +39,7 @@ export class HorizontalBarChartWithAxisBasicExample extends React.Component< useSingleColor: false, enableGradient: false, roundCorners: false, + selectMultipleLegends: false, }; } @@ -71,6 +73,10 @@ export class HorizontalBarChartWithAxisBasicExample extends React.Component< this.setState({ roundCorners: checked }); }; + private _onToggleRoundMultipleLegendSelection = (ev: React.MouseEvent, checked: boolean) => { + this.setState({ selectMultipleLegends: checked }); + }; + private _basicExample(): JSX.Element { const points: IHorizontalBarChartWithAxisDataPoint[] = [ { @@ -148,6 +154,12 @@ export class HorizontalBarChartWithAxisBasicExample extends React.Component<    +
@@ -168,6 +180,9 @@ export class HorizontalBarChartWithAxisBasicExample extends React.Component< enableReflow={true} enableGradient={this.state.enableGradient} roundCorners={this.state.roundCorners} + legendProps={{ + canSelectMultipleLegends: this.state.selectMultipleLegends, + }} /> From 5d4b9914bebff8bb19f04a51bb6a90f5507f5426 Mon Sep 17 00:00:00 2001 From: srmukher <120183316+srmukher@users.noreply.github.com> Date: Wed, 18 Dec 2024 07:54:12 +0000 Subject: [PATCH 2/3] Adding handlers for selected legends --- .../HorizontalBarChartWithAxis.base.tsx | 100 ++++++++++-------- 1 file changed, 58 insertions(+), 42 deletions(-) diff --git a/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.base.tsx b/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.base.tsx index 8f8a10b64fb23..0804b922d5b24 100644 --- a/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.base.tsx +++ b/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.base.tsx @@ -338,11 +338,11 @@ export class HorizontalBarChartWithAxisBase extends React.Component< const { YValueHover, hoverXValue } = this._getCalloutContentForBar(point); if ( - (this.state.isLegendSelected === false || - (this.state.isLegendSelected && - (this.state.selectedLegendTitle === point.legend || - this.state.selectedLegends.length === 0 || - this.state.selectedLegends.includes(point.legend!)))) && + (!this._isLegendSelected() || + (this._isLegendSelected() && + (this._isSpecificLegendTitleSelected(point.legend!) || + this._noLegendsSelected() || + this._isSpecificLegendSelected(point.legend!)))) && this._calloutAnchorPoint !== point ) { this._calloutAnchorPoint = point; @@ -383,10 +383,7 @@ export class HorizontalBarChartWithAxisBase extends React.Component< refArrayIndexNumber: number, color: string, ): void => { - if ( - this.state.isLegendSelected === false || - (this.state.isLegendSelected && this.state.selectedLegendTitle === point.legend) - ) { + if (!this._isLegendSelected() || (this._isLegendSelected() && this._isSpecificLegendTitleSelected(point.legend!))) { const { YValueHover, hoverXValue } = this._getCalloutContentForBar(point); this._refArray.forEach((obj: IRefArrayData, index: number) => { if (refArrayIndexNumber === index) { @@ -461,11 +458,9 @@ export class HorizontalBarChartWithAxisBase extends React.Component< const bars = sortedBars.map((point: IHorizontalBarChartWithAxisDataPoint, index: number) => { let shouldHighlight = true; - if (this.state.isLegendHovered || this.state.isLegendSelected) { + if (this._isLegendHovered() || this._isLegendSelected()) { shouldHighlight = - this.state.selectedLegendTitle === point.legend || - this.state.selectedLegends.length === 0 || - this.state.selectedLegends?.includes(point.legend!); + this._isSpecificLegendTitleSelected(point.legend!) || this._isSpecificLegendSelected(point.legend!); } this._classNames = getClassNames(this.props.styles!, { theme: this.props.theme!, @@ -592,8 +587,8 @@ export class HorizontalBarChartWithAxisBase extends React.Component< const { useSingleColor = false } = this.props; const bars = this._points.map((point: IHorizontalBarChartWithAxisDataPoint, index: number) => { let shouldHighlight = true; - if (this.state.isLegendHovered || this.state.isLegendSelected) { - shouldHighlight = this.state.selectedLegendTitle === point.legend; + if (this._isLegendHovered() || this._isLegendSelected()) { + shouldHighlight = this._isSpecificLegendTitleSelected(point.legend!); } this._classNames = getClassNames(this.props.styles!, { theme: this.props.theme!, @@ -706,28 +701,8 @@ export class HorizontalBarChartWithAxisBase extends React.Component< }); }; - private _onLegendClick(customMessage: string): void { - if (this.state.isLegendSelected) { - if (this.state.selectedLegendTitle === customMessage) { - this.setState({ - isLegendSelected: false, - selectedLegendTitle: customMessage, - }); - } else { - this.setState({ - selectedLegendTitle: customMessage, - }); - } - } else { - this.setState({ - isLegendSelected: true, - selectedLegendTitle: customMessage, - }); - } - } - private _onLegendHover(customMessage: string): void { - if (this.state.isLegendSelected === false) { + if (!this._isLegendSelected()) { this.setState({ isLegendHovered: true, selectedLegendTitle: customMessage, @@ -736,11 +711,11 @@ export class HorizontalBarChartWithAxisBase extends React.Component< } private _onLegendLeave(isLegendFocused?: boolean): void { - if (!!isLegendFocused || this.state.isLegendSelected === false) { + if (!!isLegendFocused || !this._isLegendSelected()) { this.setState({ isLegendHovered: false, selectedLegendTitle: '', - isLegendSelected: isLegendFocused ? false : this.state.isLegendSelected, + isLegendSelected: isLegendFocused ? false : this._isLegendSelected(), }); } } @@ -767,9 +742,6 @@ export class HorizontalBarChartWithAxisBase extends React.Component< const legend: ILegend = { title: point.legend!, color, - action: () => { - this._onLegendClick(point.legend!); - }, hoverAction: () => { this._handleChartMouseLeave(); this._onLegendHover(point.legend!); @@ -795,8 +767,52 @@ export class HorizontalBarChartWithAxisBase extends React.Component< return legends; }; - private _onLegendChange = (selectedLegends: string[]) => { + private _isLegendSelected = (): boolean => { + return this.state.isLegendSelected!; + }; + + private _isSpecificLegendTitleSelected = (legend: string): boolean => { + return this.state.selectedLegendTitle === legend; + }; + + private _isLegendHovered = (): boolean => { + return this.state.isLegendHovered!; + }; + + private _isSpecificLegendSelected = (legend: string): boolean => { + return this.state.selectedLegends.indexOf(legend) > -1; + }; + + private _noLegendsSelected = (): boolean => { + return this.state.selectedLegends.length === 0; + }; + + private _onLegendChange = ( + selectedLegends: string[], + event: React.MouseEvent, + currentLegend?: ILegend, + ) => { this.setState({ selectedLegends }); + if (this._isLegendSelected()) { + if (this._isSpecificLegendTitleSelected(currentLegend?.title!)) { + this.setState({ + isLegendSelected: false, + selectedLegendTitle: currentLegend?.title!, + }); + } else { + this.setState({ + selectedLegendTitle: currentLegend?.title!, + }); + } + } else { + this.setState({ + isLegendSelected: true, + selectedLegendTitle: currentLegend?.title!, + }); + } + if (this.props.legendProps?.onChange) { + this.props.legendProps.onChange(selectedLegends, event, currentLegend); + } }; private _getAxisData = (yAxisData: IAxisData) => { From be94cea41a158fead3b01b4ef270c6a49b8d5b16 Mon Sep 17 00:00:00 2001 From: srmukher <120183316+srmukher@users.noreply.github.com> Date: Fri, 27 Dec 2024 14:21:06 +0000 Subject: [PATCH 3/3] Adding tests for legend multiselect and updating handlers --- .../HorizontalBarChartWithAxis.base.tsx | 79 +++++++++---------- .../HorizontalBarChartWithAxisRTL.test.tsx | 25 ++++++ packages/react/etc/react.api.md | 44 +++++------ 3 files changed, 84 insertions(+), 64 deletions(-) diff --git a/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.base.tsx b/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.base.tsx index 8a4440a7f3c36..5e3144bfd4300 100644 --- a/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.base.tsx +++ b/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxis.base.tsx @@ -357,11 +357,7 @@ export class HorizontalBarChartWithAxisBase const { YValueHover, hoverXValue } = this._getCalloutContentForBar(point); if ( - (!this._isLegendSelected() || - (this._isLegendSelected() && - (this._isSpecificLegendTitleSelected(point.legend!) || - this._noLegendsSelected() || - this._isSpecificLegendSelected(point.legend!)))) && + (this.state.isLegendSelected === false || this._isLegendHighlighted(point.legend)) && this._calloutAnchorPoint !== point ) { this._calloutAnchorPoint = point; @@ -402,7 +398,10 @@ export class HorizontalBarChartWithAxisBase refArrayIndexNumber: number, color: string, ): void => { - if (!this._isLegendSelected() || (this._isLegendSelected() && this._isSpecificLegendTitleSelected(point.legend!))) { + if ( + (this.state.isLegendSelected === false || this._isLegendHighlighted(point.legend)) && + this._calloutAnchorPoint !== point + ) { const { YValueHover, hoverXValue } = this._getCalloutContentForBar(point); this._refArray.forEach((obj: IRefArrayData, index: number) => { if (refArrayIndexNumber === index) { @@ -477,9 +476,8 @@ export class HorizontalBarChartWithAxisBase const bars = sortedBars.map((point: IHorizontalBarChartWithAxisDataPoint, index: number) => { let shouldHighlight = true; - if (this._isLegendHovered() || this._isLegendSelected()) { - shouldHighlight = - this._isSpecificLegendTitleSelected(point.legend!) || this._isSpecificLegendSelected(point.legend!); + if (this.state.isLegendHovered || this.state.isLegendSelected) { + shouldHighlight = this._isLegendHighlighted(point.legend); } this._classNames = getClassNames(this.props.styles!, { theme: this.props.theme!, @@ -606,8 +604,8 @@ export class HorizontalBarChartWithAxisBase const { useSingleColor = false } = this.props; const bars = this._points.map((point: IHorizontalBarChartWithAxisDataPoint, index: number) => { let shouldHighlight = true; - if (this._isLegendHovered() || this._isLegendSelected()) { - shouldHighlight = this._isSpecificLegendTitleSelected(point.legend!); + if (this._getHighlightedLegend().length > 0) { + shouldHighlight = this._isLegendHighlighted(point.legend); } this._classNames = getClassNames(this.props.styles!, { theme: this.props.theme!, @@ -779,8 +777,7 @@ export class HorizontalBarChartWithAxisBase focusZonePropsInHoverCard={this.props.focusZonePropsForLegendsInHoverCard} overflowText={this.props.legendsOverflowText} {...this.props.legendProps} - canSelectMultipleLegends={this.props.legendProps?.canSelectMultipleLegends} - onChange={this._onLegendChange} + onChange={this._onLegendSelectionChange.bind(this)} /> ); return legends; @@ -790,49 +787,47 @@ export class HorizontalBarChartWithAxisBase return this.state.isLegendSelected!; }; - private _isSpecificLegendTitleSelected = (legend: string): boolean => { - return this.state.selectedLegendTitle === legend; - }; - - private _isLegendHovered = (): boolean => { - return this.state.isLegendHovered!; - }; - - private _isSpecificLegendSelected = (legend: string): boolean => { - return this.state.selectedLegends.indexOf(legend) > -1; + /** + * This function checks if the given legend is highlighted or not. + * A legend can be highlighted in 2 ways: + * 1. selection: if the user clicks on it + * 2. hovering: if there is no selected legend and the user hovers over it + */ + private _isLegendHighlighted = (legend?: string) => { + return this._getHighlightedLegend().includes(legend!); }; - private _noLegendsSelected = (): boolean => { - return this.state.selectedLegends.length === 0; - }; + private _getHighlightedLegend() { + return this.state.selectedLegends.length > 0 + ? this.state.selectedLegends + : this.state.selectedLegendTitle + ? [this.state.selectedLegendTitle] + : []; + } - private _onLegendChange = ( + private _onLegendSelectionChange( selectedLegends: string[], event: React.MouseEvent, currentLegend?: ILegend, - ) => { - this.setState({ selectedLegends }); - if (this._isLegendSelected()) { - if (this._isSpecificLegendTitleSelected(currentLegend?.title!)) { - this.setState({ - isLegendSelected: false, - selectedLegendTitle: currentLegend?.title!, - }); - } else { - this.setState({ - selectedLegendTitle: currentLegend?.title!, - }); - } + ): void { + if (this.props.legendProps?.canSelectMultipleLegends) { + this.setState({ + selectedLegends, + selectedLegendTitle: currentLegend?.title!, + }); } else { this.setState({ - isLegendSelected: true, + selectedLegends: selectedLegends.slice(-1), selectedLegendTitle: currentLegend?.title!, }); } + this.setState({ + isLegendSelected: selectedLegends.length > 0, + }); if (this.props.legendProps?.onChange) { this.props.legendProps.onChange(selectedLegends, event, currentLegend); } - }; + } private _getAxisData = (yAxisData: IAxisData) => { if (yAxisData && yAxisData.yAxisDomainValues.length) { diff --git a/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisRTL.test.tsx b/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisRTL.test.tsx index 6975206a6447a..f19db37b54b68 100644 --- a/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisRTL.test.tsx +++ b/packages/charts/react-charting/src/components/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisRTL.test.tsx @@ -169,6 +169,31 @@ describe('Horizontal bar chart with axis- Subcomponent Legends', () => { expect(legendsAfterClickEvent[3]).toHaveAttribute('aria-selected', 'false'); }, ); + + testWithoutWait( + 'Should select multiple legends on multiple mouse click on legends', + HorizontalBarChartWithAxis, + { data: chartPointsHBCWA, legendProps: { canSelectMultipleLegends: true } }, + container => { + // const legends = screen.getAllByText((content, element) => element!.tagName.toLowerCase() === 'button'); + const legend1 = screen.getByText('Grapes')?.closest('button'); + const legend2 = screen.getByText('Apples')?.closest('button'); + + expect(legend1).toBeDefined(); + expect(legend2).toBeDefined(); + + fireEvent.click(legend1!); + fireEvent.click(legend2!); + const legendsAfterClickEvent = screen.getAllByText( + (content, element) => element!.tagName.toLowerCase() === 'button', + ); + // Assert + expect(legendsAfterClickEvent[0]).toHaveAttribute('aria-selected', 'false'); + expect(legendsAfterClickEvent[1]).toHaveAttribute('aria-selected', 'true'); + expect(legendsAfterClickEvent[2]).toHaveAttribute('aria-selected', 'true'); + expect(legendsAfterClickEvent[3]).toHaveAttribute('aria-selected', 'false'); + }, + ); }); describe('Horizontal bar chart with axis - Subcomponent callout', () => { diff --git a/packages/react/etc/react.api.md b/packages/react/etc/react.api.md index 94b006c369575..2cf141de1c40e 100644 --- a/packages/react/etc/react.api.md +++ b/packages/react/etc/react.api.md @@ -1604,49 +1604,49 @@ export { FabricPerformance } // @public (undocumented) export enum FabricSlots { // (undocumented) - black = 21,// BaseSlots.primaryColor, Shade[Shade.Unshaded]); + black = 21, // (undocumented) - neutralDark = 20,// BaseSlots.primaryColor, Shade[Shade.Shade1]); + neutralDark = 20, // (undocumented) - neutralLight = 11,// BaseSlots.primaryColor, Shade[Shade.Shade2]); + neutralLight = 11, // (undocumented) - neutralLighter = 10,// BaseSlots.primaryColor, Shade[Shade.Shade3]); + neutralLighter = 10, // (undocumented) - neutralLighterAlt = 9,// BaseSlots.primaryColor, Shade[Shade.Shade4]); + neutralLighterAlt = 9, // (undocumented) - neutralPrimary = 19,// BaseSlots.primaryColor, Shade[Shade.Shade5]); + neutralPrimary = 19, // (undocumented) - neutralPrimaryAlt = 18,// BaseSlots.primaryColor, Shade[Shade.Shade6]); + neutralPrimaryAlt = 18, // (undocumented) - neutralQuaternary = 13,// BaseSlots.primaryColor, Shade[Shade.Shade7]); + neutralQuaternary = 13, // (undocumented) - neutralQuaternaryAlt = 12,// BaseSlots.primaryColor, Shade[Shade.Shade8]); + neutralQuaternaryAlt = 12, // (undocumented) - neutralSecondary = 17,// BaseSlots.backgroundColor, Shade[Shade.Shade1]); + neutralSecondary = 17, // (undocumented) - neutralSecondaryAlt = 16,// BaseSlots.backgroundColor, Shade[Shade.Shade2]); + neutralSecondaryAlt = 16, // (undocumented) - neutralTertiary = 15,// BaseSlots.backgroundColor, Shade[Shade.Shade3]); + neutralTertiary = 15, // (undocumented) - neutralTertiaryAlt = 14,// BaseSlots.backgroundColor, Shade[Shade.Shade4]); + neutralTertiaryAlt = 14, // (undocumented) - themeDark = 7,// BaseSlots.backgroundColor, Shade[Shade.Shade5]); + themeDark = 7, // (undocumented) - themeDarkAlt = 6,// BaseSlots.backgroundColor, Shade[Shade.Shade6]); // bg6 or fg2 + themeDarkAlt = 6, // (undocumented) - themeDarker = 8,// BaseSlots.foregroundColor, Shade[Shade.Shade3]); + themeDarker = 8, // (undocumented) - themeLight = 3,// BaseSlots.foregroundColor, Shade[Shade.Shade4]); + themeLight = 3, // (undocumented) - themeLighter = 2,// BaseSlots.foregroundColor, Shade[Shade.Shade5]); + themeLighter = 2, // (undocumented) - themeLighterAlt = 1,// BaseSlots.foregroundColor, Shade[Shade.Shade6]); + themeLighterAlt = 1, // (undocumented) - themePrimary = 0,// BaseSlots.foregroundColor, Shade[Shade.Unshaded]); + themePrimary = 0, // (undocumented) - themeSecondary = 5,// BaseSlots.foregroundColor, Shade[Shade.Shade7]); + themeSecondary = 5, // (undocumented) - themeTertiary = 4,// BaseSlots.foregroundColor, Shade[Shade.Shade8]); + themeTertiary = 4, // (undocumented) white = 22 }