Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: react-charting legends onchange and defaultSelectedLegends #29799

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "add defaultSelectedLegeands and onChange for Legends",
"packageName": "@fluentui/react-charting",
"email": "NewFuture@users.noreply.github.com",
"dependentChangeType": "patch"
}
4 changes: 3 additions & 1 deletion packages/react-charting/etc/react-charting.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -787,13 +787,15 @@ export interface ILegendsProps {
canSelectMultipleLegends?: boolean;
centerLegends?: boolean;
className?: string;
defaultSelectedLegend?: string;
defaultSelectedLegends?: string[];
NewFuture marked this conversation as resolved.
Show resolved Hide resolved
enabledWrapLines?: boolean;
focusZonePropsInHoverCard?: IFocusZoneProps;
legends: ILegend[];
onChange?: (selectedLegends: string[], event: React_2.MouseEvent<HTMLButtonElement>, currentLegend?: ILegend) => void;
onLegendHoverCardLeave?: VoidFunction;
overflowProps?: Partial<IOverflowSetProps>;
overflowText?: string;
selectedLegend?: string;
AtishayMsft marked this conversation as resolved.
Show resolved Hide resolved
styles?: IStyleFunctionOrObject<ILegendStyleProps, ILegendsStyles>;
theme?: ITheme;
}
Expand Down
63 changes: 36 additions & 27 deletions packages/react-charting/src/components/Legends/Legends.base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,9 @@ interface ILegendItem extends React.ButtonHTMLAttributes<HTMLButtonElement> {
}

export interface ILegendState {
selectedLegend: string;
activeLegend: string;
isHoverCardVisible: boolean;
/** Set of legends selected when multiple selection is allowed */
/** Set of legends selected, both for multiple selection and single selection */
selectedLegends: { [key: string]: boolean };
AtishayMsft marked this conversation as resolved.
Show resolved Hide resolved
}
export class LegendsBase extends React.Component<ILegendsProps, ILegendState> {
Expand All @@ -49,11 +48,18 @@ export class LegendsBase extends React.Component<ILegendsProps, ILegendState> {

public constructor(props: ILegendsProps) {
super(props);
NewFuture marked this conversation as resolved.
Show resolved Hide resolved
let defaultSelectedLegends = {};
if (props.canSelectMultipleLegends) {
defaultSelectedLegends =
props.defaultSelectedLegends?.reduce((combinedDict, key) => ({ [key]: true, ...combinedDict }), {}) || {};
} else if (props.defaultSelectedLegend) {
defaultSelectedLegends = { [props.defaultSelectedLegend]: true };
}

this.state = {
selectedLegend: '',
activeLegend: '',
isHoverCardVisible: false,
selectedLegends: {},
selectedLegends: defaultSelectedLegends,
};
}

Expand All @@ -63,7 +69,7 @@ export class LegendsBase extends React.Component<ILegendsProps, ILegendState> {
theme: theme!,
className,
});
this._isLegendSelected = this.state.selectedLegend !== '' || Object.keys(this.state.selectedLegends).length > 0;
this._isLegendSelected = Object.keys(this.state.selectedLegends).length > 0;
const dataToRender = this._generateData();
return (
<div className={this._classNames.root}>
Expand Down Expand Up @@ -162,7 +168,7 @@ export class LegendsBase extends React.Component<ILegendsProps, ILegendState> {
* select multiple legends
* @param legend ILegend
*/
private _canSelectMultipleLegends = (legend: ILegend): void => {
private _canSelectMultipleLegends = (legend: ILegend): { [key: string]: boolean } => {
let selectedLegends = { ...this.state.selectedLegends };
if (selectedLegends[legend.title]) {
// Delete entry for the deselected legend to make
Expand All @@ -176,6 +182,7 @@ export class LegendsBase extends React.Component<ILegendsProps, ILegendState> {
}
}
this.setState({ selectedLegends });
return selectedLegends;
};

/**
Expand All @@ -184,24 +191,28 @@ export class LegendsBase extends React.Component<ILegendsProps, ILegendState> {
* @param legend ILegend
*/

private _canSelectOnlySingleLegend = (legend: ILegend): void => {
if (this.state.selectedLegend === legend.title) {
this.setState({ selectedLegend: '' });
private _canSelectOnlySingleLegend = (legend: ILegend): boolean => {
if (this.state.selectedLegends[legend.title]) {
this.setState({ selectedLegends: {} });
return false;
} else {
this.setState({ selectedLegend: legend.title });
this.setState({ selectedLegends: { [legend.title]: true } });
return true;
}
};

private _onClick = (legend: ILegend): void => {
if (legend.action) {
const { canSelectMultipleLegends = false } = this.props;
if (canSelectMultipleLegends) {
this._canSelectMultipleLegends(legend);
} else {
this._canSelectOnlySingleLegend(legend);
}
legend.action();
private _onClick = (legend: ILegend, event: React.MouseEvent<HTMLButtonElement>): void => {
const { canSelectMultipleLegends = false } = this.props;
let selectedLegends: string[] = [];
if (canSelectMultipleLegends) {
const nextSelectedLegends = this._canSelectMultipleLegends(legend);
selectedLegends = Object.keys(nextSelectedLegends);
} else {
const isSelected = this._canSelectOnlySingleLegend(legend);
selectedLegends = isSelected ? [legend.title] : [];
}
this.props.onChange?.(selectedLegends, event, legend);
legend.action?.();
};

private _onRenderCompactCard = (expandingCard: IExpandingCardProps): JSX.Element => {
Expand Down Expand Up @@ -324,19 +335,19 @@ export class LegendsBase extends React.Component<ILegendsProps, ILegendState> {
opacity: data.opacity,
};
const color = this._getColor(legend.title, legend.color);
const { theme, className, styles, canSelectMultipleLegends = false } = this.props;
const { theme, className, styles } = this.props;
const classNames = getClassNames(styles!, {
theme: theme!,
className,
colorOnSelectedState: color,
borderColor: legend.color,
overflow: overflow,
overflow,
stripePattern: legend.stripePattern,
isLineLegendInBarChart: legend.isLineLegendInBarChart,
opacity: legend.opacity,
});
const onClickHandler = () => {
this._onClick(legend);
const onClickHandler = (event: React.MouseEvent<HTMLButtonElement>) => {
this._onClick(legend, event);
};
const onHoverHandler = () => {
this._onHoverOverLegend(legend);
Expand All @@ -348,9 +359,7 @@ export class LegendsBase extends React.Component<ILegendsProps, ILegendState> {
return (
<button
{...(allowFocusOnLegends && {
'aria-selected': canSelectMultipleLegends
? !!this.state.selectedLegends[legend.title]
: this.state.selectedLegend === legend.title,
'aria-selected': !!this.state.selectedLegends[legend.title],
role: 'option',
'aria-label': `${legend.title}`,
'aria-setsize': data['aria-setsize'],
Expand Down Expand Up @@ -403,7 +412,7 @@ export class LegendsBase extends React.Component<ILegendsProps, ILegendState> {
// if one or more legends are selected
if (this._isLegendSelected) {
// if the given legend (title) is one of the selected legends
if (this.state.selectedLegend === title || this.state.selectedLegends[title]) {
if (this.state.selectedLegends[title]) {
legendColor = color;
}
// if the given legend is unselected
Expand Down
16 changes: 16 additions & 0 deletions packages/react-charting/src/components/Legends/Legends.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,19 @@ describe('Render calling with respective to props', () => {
renderMock.mockRestore();
});
});

describe('Legends - multi Legends', () => {
beforeEach(sharedBeforeEach);
afterEach(sharedAfterEach);
it('Should render defaultSelectedLegends', () => {
wrapper = mount(
<Legends
legends={legends}
canSelectMultipleLegends={true}
defaultSelectedLegends={[legends[0].title, legends[2].title]}
/>,
);
const renderedLegends = wrapper.getDOMNode().querySelectorAll('button[aria-selected="true"]');
expect(renderedLegends?.length).toBe(2);
});
});
27 changes: 20 additions & 7 deletions packages/react-charting/src/components/Legends/Legends.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,6 @@ export interface ILegendsProps {
*/
overflowText?: string;

/**
* Prop that takes the active legend
*/
selectedLegend?: string;

/**
* prop that decides if legends are focusable
* @default true
Expand All @@ -220,10 +215,28 @@ export interface ILegendsProps {

/**
* Defines the function that is executed upon hiding of hover card
* make sure to send prop when the prop is canSelectMultipleLegends is set to ture
* and empty the selecetd state legends
* make sure to send prop when the prop is canSelectMultipleLegends is set to true
* and empty the selected state legends
*/
onLegendHoverCardLeave?: VoidFunction;

/**
* Callback issued when the selected option changes.
*/
onChange?: (selectedLegends: string[], event: React.MouseEvent<HTMLButtonElement>, currentLegend?: ILegend) => void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be renamed to something like onLegendSelectionChange for better clarity

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onChange is a common naming for input controllers.


/**
* Keys (title) that will be initially used to set selected items.
* This prop is used for multiSelect scenarios.
* In other cases, defaultSelectedLegend should be used.
*/
defaultSelectedLegends?: string[];

/**
* Key that will be initially used to set selected item.
* This prop is used for singleSelect scenarios.
*/
defaultSelectedLegend?: string;
AtishayMsft marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as React from 'react';
import { Legends, ILegend, DataVizPalette, getColorFromToken } from '@fluentui/react-charting';
import { Toggle } from '@fluentui/react';
import { useBoolean } from '@fluentui/react-hooks';

const legends: ILegend[] = [
{
title: 'Legend 1',
color: getColorFromToken(DataVizPalette.color1),
},
{
title: 'Legend 2',
color: getColorFromToken(DataVizPalette.color2),
},
{
title: 'Legend 3',
color: getColorFromToken(DataVizPalette.color3),
shape: 'diamond',
},
{
title: 'Legend 4',
color: getColorFromToken(DataVizPalette.color4),
shape: 'triangle',
},
];

export const LegendsOnChangeExample: React.FunctionComponent = () => {
const defaultSelectedLegends = ['Legend 1', 'Legend 3'];
const [isMulti, { toggle: toggleIsMulti }] = useBoolean(true);

const onChange = (keys: string[]) => {
alert(keys.length ? `Selected: ${keys.join()}` : 'Empty');
};
return (
<div>
<Toggle
label="Can select multiple legends"
onText="Multiple"
offText="Single"
checked={isMulti}
onChange={toggleIsMulti}
/>
<Legends
legends={legends}
canSelectMultipleLegends={isMulti}
defaultSelectedLegends={defaultSelectedLegends}
// eslint-disable-next-line react/jsx-no-bind
onChange={onChange}
/>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { LegendOverflowExample } from './Legends.Overflow.Example';
import { LegendBasicExample } from './Legends.Basic.Example';
import { LegendWrapLinesExample } from './Legends.WrapLines.Example';
import { LegendStyledExample } from './Legends.Styled.Example';
import { LegendsOnChangeExample } from './Legends.OnChange.Example';

const LegendsOverflowExampleCode =
require('!raw-loader?esModule=false!@fluentui/react-examples/src/react-charting/Legends/Legends.Overflow.Example.tsx') as string;
Expand All @@ -21,6 +22,8 @@ const LegendsBasicExampleCode =
require('!raw-loader?esModule=false!@fluentui/react-examples/src/react-charting/Legends/Legends.Basic.Example.tsx') as string;
const LegendsStyledExampleCode =
require('!raw-loader?esModule=false!@fluentui/react-examples/src/react-charting/Legends/Legends.Styled.Example.tsx') as string;
const LegendsOnChangeExampleCode =
require('!raw-loader?esModule=false!@fluentui/react-examples/src/react-charting/Legends/Legends.OnChange.Example.tsx') as string;

export class LegendsPage extends React.Component<IComponentDemoPageProps, {}> {
public render(): JSX.Element {
Expand All @@ -45,6 +48,10 @@ export class LegendsPage extends React.Component<IComponentDemoPageProps, {}> {
<ExampleCard title="Legend styled" code={LegendsStyledExampleCode}>
<LegendStyledExample />
</ExampleCard>

<ExampleCard title="Legends onChange" code={LegendsOnChangeExampleCode}>
<LegendsOnChangeExample />
</ExampleCard>
</div>
}
propertiesTables={
Expand Down
Loading