Skip to content

Commit

Permalink
refactor: DefaultTooltipContent to be functional component (#3618)
Browse files Browse the repository at this point in the history
## Description

1. The DefaultTooltipComponent is now a functional component.
2. The result of formatter function is either
   `[React.ReactNode, React.ReactNode]` or `React.ReactNode`, instead of
   being the same type as the inputs, because inputs can be transformed
   and the result of the format is eventually is rendered in the JSX, so
   its meaningful to le the user return anything that react accepts for
   rendering.
3. A story is added for a case of Tooltips that use click to show
    tooltip instead of hover with pointer.

## Related Issue

Fixes: #2976

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->

## How Has This Been Tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->

## Screenshots (if appropriate):

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all
the boxes that apply: -->

- [X] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)

## Checklist:

<!--- Go over all the following points, and put an `x` in all the boxes
that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're
here to help! -->

- [X] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
- [X] All new and existing tests passed.
  • Loading branch information
akamfoad committed Jun 12, 2023
1 parent 36b75d4 commit f1c52c1
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 57 deletions.
113 changes: 58 additions & 55 deletions src/component/DefaultTooltipContent.tsx
Expand Up @@ -2,7 +2,7 @@
* @fileOverview Default Tooltip Content
*/
import _ from 'lodash';
import React, { PureComponent, CSSProperties, ReactNode } from 'react';
import React, { CSSProperties, ReactNode } from 'react';
import classNames from 'classnames';
import { isNumOrStr } from '../util/DataUtils';

Expand All @@ -19,7 +19,7 @@ export type Formatter<TValue extends ValueType, TName extends NameType> = (
item: Payload<TValue, TName>,
index: number,
payload: Array<Payload<TValue, TName>>,
) => [TValue, TName] | TValue;
) => [React.ReactNode, TName] | React.ReactNode;

export interface Payload<TValue extends ValueType, TName extends NameType> {
type?: TooltipType;
Expand Down Expand Up @@ -50,21 +50,24 @@ export interface Props<TValue extends ValueType, TName extends NameType> {
itemSorter?: (item: Payload<TValue, TName>) => number | string;
}

export class DefaultTooltipContent<TValue extends ValueType, TName extends NameType> extends PureComponent<
Props<TValue, TName>
> {
static displayName = 'DefaultTooltipContent';

static defaultProps = {
separator: ' : ',
contentStyle: {},
itemStyle: {},
labelStyle: {},
};

renderContent() {
const { payload, separator, formatter, itemStyle, itemSorter } = this.props;
export const DefaultTooltipContent = <TValue extends ValueType, TName extends NameType>(
props: Props<TValue, TName>,
) => {
const {
separator = ' : ',
contentStyle = {},
itemStyle = {},
labelStyle = {},
payload,
formatter,
itemSorter,
wrapperClassName,
labelClassName,
label,
labelFormatter,
} = props;

const renderContent = () => {
if (payload && payload.length) {
const listStyle = { padding: 0, margin: 0 };

Expand All @@ -81,21 +84,24 @@ export class DefaultTooltipContent<TValue extends ValueType, TName extends NameT
...itemStyle,
};
const finalFormatter = entry.formatter || formatter || defaultFormatter;
let { value, name } = entry;
if (finalFormatter && value != null && name != null) {
const { value, name } = entry;
let finalValue: React.ReactNode = value;
let finalName: React.ReactNode = name;
if (finalFormatter && finalValue != null && finalName != null) {
const formatted = finalFormatter(value, name, entry, i, payload);
if (Array.isArray(formatted)) {
[value, name] = formatted as [TValue, TName];
[finalValue, finalName] = formatted;
} else {
value = formatted;
finalValue = formatted;
}
}

return (
// eslint-disable-next-line react/no-array-index-key
<li className="recharts-tooltip-item" key={`tooltip-item-${i}`} style={finalItemStyle}>
{isNumOrStr(name) ? <span className="recharts-tooltip-item-name">{name}</span> : null}
{isNumOrStr(name) ? <span className="recharts-tooltip-item-separator">{separator}</span> : null}
<span className="recharts-tooltip-item-value">{value}</span>
{isNumOrStr(finalName) ? <span className="recharts-tooltip-item-name">{finalName}</span> : null}
{isNumOrStr(finalName) ? <span className="recharts-tooltip-item-separator">{separator}</span> : null}
<span className="recharts-tooltip-item-value">{finalValue}</span>
<span className="recharts-tooltip-item-unit">{entry.unit || ''}</span>
</li>
);
Expand All @@ -109,38 +115,35 @@ export class DefaultTooltipContent<TValue extends ValueType, TName extends NameT
}

return null;
}

render() {
const { wrapperClassName, contentStyle, labelClassName, labelStyle, label, labelFormatter, payload } = this.props;
const finalStyle: CSSProperties = {
margin: 0,
padding: 10,
backgroundColor: '#fff',
border: '1px solid #ccc',
whiteSpace: 'nowrap',
...contentStyle,
};
const finalLabelStyle = {
margin: 0,
...labelStyle,
};
const hasLabel = !_.isNil(label);
let finalLabel = hasLabel ? label : '';
const wrapperCN = classNames('recharts-default-tooltip', wrapperClassName);
const labelCN = classNames('recharts-tooltip-label', labelClassName);
};

if (hasLabel && labelFormatter && payload !== undefined && payload !== null) {
finalLabel = labelFormatter(label, payload);
}
const finalStyle: React.CSSProperties = {
margin: 0,
padding: 10,
backgroundColor: '#fff',
border: '1px solid #ccc',
whiteSpace: 'nowrap',
...contentStyle,
};
const finalLabelStyle = {
margin: 0,
...labelStyle,
};
const hasLabel = !_.isNil(label);
let finalLabel = hasLabel ? label : '';
const wrapperCN = classNames('recharts-default-tooltip', wrapperClassName);
const labelCN = classNames('recharts-tooltip-label', labelClassName);

return (
<div className={wrapperCN} style={finalStyle}>
<p className={labelCN} style={finalLabelStyle}>
{React.isValidElement(finalLabel) ? finalLabel : `${finalLabel}`}
</p>
{this.renderContent()}
</div>
);
if (hasLabel && labelFormatter && payload !== undefined && payload !== null) {
finalLabel = labelFormatter(label, payload);
}
}

return (
<div className={wrapperCN} style={finalStyle}>
<p className={labelCN} style={finalLabelStyle}>
{React.isValidElement(finalLabel) ? finalLabel : `${finalLabel}`}
</p>
{renderContent()}
</div>
);
};
17 changes: 17 additions & 0 deletions storybook/stories/API/tooltip/Tooltip.stories.tsx
Expand Up @@ -88,6 +88,23 @@ export const MultipleValuedTooltip = {
},
};

export const TriggerTooltipByClick = {
render: (tooltipArgs: Record<string, any>) => {
return (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={subjectData}>
<Tooltip {...tooltipArgs} trigger="click" />
<XAxis dataKey="subject" />
<YAxis />
{Marks.map(({ marks, fill }) => (
<Line key={marks} dataKey={marks} fill={fill} />
))}
</LineChart>
</ResponsiveContainer>
);
},
};

const CustomMultipleValueTooltip = ({ active, payload }: CustomTooltipProps) => {
if (active && payload && payload.length > 0) {
return (
Expand Down
6 changes: 4 additions & 2 deletions storybook/stories/Examples/Tooltip.stories.tsx
Expand Up @@ -15,9 +15,11 @@ export const LockedByClick = {
// Their update is interrupted by the click event, so we need to store them in a state.
const [tooltipData, setTooltipData] = React.useState<{ payload?: unknown[]; label?: string; x?: number }>({});

// A custom Tooltip that updates the payload of the tooltip if the chart is locked, and either way always renders using the normal Tooltip.
// A custom Tooltip that updates the payload of the tooltip if the
// chart is locked, and either way always renders using the normal Tooltip.
const CustomTooltip = (props: any) => {
// If the chart is locked, and the payload is not empty, and the x position of the tooltip has changed, update the tooltipData.
// If the chart is locked, and the payload is not empty, and the
// x position of the tooltip has changed, update the tooltipData.
if (!isLocked && props.payload && props.payload.length > 0 && props.coordinate.x !== tooltipData.x) {
setTooltipData({ payload: props.payload, x: props.coordinate.x, label: props.label });
}
Expand Down

0 comments on commit f1c52c1

Please sign in to comment.