diff --git a/packages/charts/src/components/legend/legend.tsx b/packages/charts/src/components/legend/legend.tsx index 5b27baacc0..2a72546d13 100644 --- a/packages/charts/src/components/legend/legend.tsx +++ b/packages/charts/src/components/legend/legend.tsx @@ -7,7 +7,7 @@ */ import classNames from 'classnames'; -import React from 'react'; +import React, { useCallback } from 'react'; import { connect } from 'react-redux'; import { Dispatch, bindActionCreators } from 'redux'; @@ -17,12 +17,14 @@ import { LegendTable } from './legend_table'; import { getLegendPositionConfig, legendPositionStyle } from './position_style'; import { getLegendStyle, getLegendListStyle } from './style_utils'; import { LegendItem, LegendItemExtraValues, LegendValue } from '../../common/legend'; +import { SeriesIdentifier } from '../../common/series_id'; import { DEFAULT_LEGEND_CONFIG, LegendSpec } from '../../specs'; import { clearTemporaryColors, setTemporaryColor, setPersistedColor } from '../../state/actions/colors'; import { onToggleDeselectSeriesAction, onLegendItemOutAction, onLegendItemOverAction, + LegendPath, } from '../../state/actions/legend'; import { GlobalChartState } from '../../state/chart_state'; import { getChartThemeSelector } from '../../state/selectors/get_chart_theme'; @@ -73,6 +75,28 @@ function LegendComponent(props: LegendStateProps & LegendDispatchProps) { config, } = props; + const { onLegendItemOut, onLegendItemOver } = config; + const { onItemOutAction, onItemOverAction } = props; + + const onLegendItemMouseOver = useCallback( + (seriesIdentifiers: SeriesIdentifier[], path: LegendPath) => { + // call the settings listener directly if available + if (onLegendItemOver) { + onLegendItemOver(seriesIdentifiers); + } + onItemOverAction(path); + }, + [onItemOverAction, onLegendItemOver], + ); + + const onLegendItemMouseOut = useCallback(() => { + // call the settings listener directly if available + if (onLegendItemOut) { + onLegendItemOut(); + } + onItemOutAction(); + }, [onLegendItemOut, onItemOutAction]); + if (items.every(({ isItemHidden }) => isItemHidden)) { return null; } @@ -100,14 +124,12 @@ function LegendComponent(props: LegendStateProps & LegendDispatchProps) { hiddenItems: items.filter(({ isSeriesHidden }) => isSeriesHidden).length, extraValues: props.extraValues, legendValues: config.legendValues, - onMouseOut: config.onLegendItemOut, - onMouseOver: config.onLegendItemOver, + onLegendItemMouseOver, + onLegendItemMouseOut, onClick: config.onLegendItemClick, clearTemporaryColorsAction: props.clearTemporaryColors, setPersistedColorAction: props.setPersistedColor, setTemporaryColorAction: props.setTemporaryColor, - mouseOutAction: props.onItemOutAction, - mouseOverAction: props.onItemOverAction, toggleDeselectSeriesAction: props.onToggleDeselectSeriesAction, colorPicker: config.legendColorPicker, action: config.legendAction, @@ -129,8 +151,8 @@ function LegendComponent(props: LegendStateProps & LegendDispatchProps) { seriesIdentifiers, path, extraValue: itemProps.extraValues.get(seriesIdentifiers[0]?.key ?? '')?.get(childId ?? ''), - onItemOutAction: itemProps.mouseOutAction, - onItemOverActon: () => itemProps.mouseOverAction(path), + onItemOutAction, + onItemOverActon: () => onItemOverAction(path), onItemClickAction: (negate: boolean) => itemProps.toggleDeselectSeriesAction(seriesIdentifiers, negate), }))} /> diff --git a/packages/charts/src/components/legend/legend_color_picker.tsx b/packages/charts/src/components/legend/legend_color_picker.tsx index 452d322539..f3d711b3a8 100644 --- a/packages/charts/src/components/legend/legend_color_picker.tsx +++ b/packages/charts/src/components/legend/legend_color_picker.tsx @@ -13,7 +13,7 @@ import { LegendItemProps } from './legend_item'; import { Color } from '../../common/colors'; /** @internal */ -export const LegendColorPicker = ({ +export const useLegendColorPicker = ({ item: { color, isSeriesHidden, label, pointStyle, seriesIdentifiers }, colorPicker: ColorPickerRenderer, clearTemporaryColorsAction, @@ -51,26 +51,30 @@ export const LegendColorPicker = ({ const hasColorPicker = Boolean(ColorPickerRenderer); - return ( - <> - ( + + ); + + const renderColorPickerPopup = () => + ColorPickerRenderer && + isOpen && + colorRef.current && ( + - {ColorPickerRenderer && isOpen && colorRef.current && ( - - )} - - ); + ); + + return { renderItemColor, renderColorPickerPopup }; }; diff --git a/packages/charts/src/components/legend/legend_item.tsx b/packages/charts/src/components/legend/legend_item.tsx index f10e72056c..01dec4398d 100644 --- a/packages/charts/src/components/legend/legend_item.tsx +++ b/packages/charts/src/components/legend/legend_item.tsx @@ -7,14 +7,13 @@ */ import classNames from 'classnames'; -import React, { CSSProperties, useRef, useState, useCallback } from 'react'; +import React, { CSSProperties, useCallback } from 'react'; -import { Color as ItemColor } from './color'; import { Label as ItemLabel } from './label'; import { LegendActionComponent } from './legend_action'; +import { useLegendColorPicker } from './legend_color_picker'; import { SharedLegendItemProps } from './types'; import { getExtra } from './utils'; -import { Color } from '../../common/colors'; import { LegendItem, LegendItemExtraValues, LegendValue } from '../../common/legend'; import { SeriesIdentifier } from '../../common/series_id'; import { LayoutDirection } from '../../utils/common'; @@ -27,7 +26,8 @@ export interface LegendItemProps extends SharedLegendItemProps { item: LegendItem; } -const prepareLegendValue = ( +/** @internal */ +export const prepareLegendValues = ( item: LegendItem, legendValues: LegendValue[], totalItems: number, @@ -36,10 +36,10 @@ const prepareLegendValue = ( if (legendValues.length === 0) { return undefined; } - if (legendValues[0] === LegendValue.CurrentAndLastValue) { - return getExtra(extraValues, item, totalItems); + if (legendValues.length === 1 && legendValues[0] === LegendValue.CurrentAndLastValue) { + return [getExtra(extraValues, item, totalItems)]; } - return item.values[0]; + return item.values; }; /** @internal */ export const LegendListItem: React.FC = (props) => { @@ -47,7 +47,6 @@ export const LegendListItem: React.FC = (props) => { extraValues, item, legendValues, - colorPicker, totalItems, action: Action, positionConfig, @@ -56,25 +55,18 @@ export const LegendListItem: React.FC = (props) => { flatLegend, onClick, toggleDeselectSeriesAction, - onMouseOver, - mouseOverAction, - onMouseOut, - mouseOutAction, - setPersistedColorAction, - clearTemporaryColorsAction, - setTemporaryColorAction, - colorPicker: ColorPicker, hiddenItems, + onLegendItemMouseOver, + onLegendItemMouseOut, } = props; - const { color, isSeriesHidden, isItemHidden, seriesIdentifiers, label, pointStyle, depth, path, isToggleable } = item; + const { color, isSeriesHidden, isItemHidden, seriesIdentifiers, label, depth, path, isToggleable } = item; const itemClassNames = classNames('echLegendItem', { 'echLegendItem--hidden': isSeriesHidden, 'echLegendItem--vertical': positionConfig.direction === LayoutDirection.Vertical, }); - // only the first for now until https://github.com/elastic/elastic-charts/issues/2096 - const legendValue = prepareLegendValue(item, legendValues, totalItems, extraValues); + const legendValueItems = prepareLegendValues(item, legendValues, totalItems, extraValues); const style: CSSProperties = flatLegend ? {} @@ -82,30 +74,6 @@ export const LegendListItem: React.FC = (props) => { [isMostlyRTL ? 'marginRight' : 'marginLeft']: LEGEND_HIERARCHY_MARGIN * (depth ?? 0), }; - const colorRef = useRef(null); - const [isOpen, setIsOpen] = useState(false); - const shouldClearPersistedColor = useRef(false); - - const toggleIsOpen = useCallback(() => { - setIsOpen((prevIsOpen) => !prevIsOpen); - }, []); - - const onLegendItemMouseOver = useCallback(() => { - // call the settings listener directly if available - if (onMouseOver) { - onMouseOver(seriesIdentifiers); - } - mouseOverAction(path); - }, [mouseOverAction, onMouseOver, path, seriesIdentifiers]); - - const onLegendItemMouseOut = useCallback(() => { - // call the settings listener directly if available - if (onMouseOut) { - onMouseOut(); - } - mouseOutAction(); - }, [onMouseOut, mouseOutAction]); - const onLabelToggle = useCallback( (legendItemId: SeriesIdentifier[]) => (negate: boolean) => { if (totalItems <= 1 || (!isToggleable && !onClick)) { @@ -122,41 +90,7 @@ export const LegendListItem: React.FC = (props) => { }, [onClick, toggleDeselectSeriesAction, isToggleable, totalItems], ); - - const renderColorPicker = useCallback(() => { - if (!ColorPicker || !isOpen || !colorRef.current) { - return null; - } - - const seriesKeys = seriesIdentifiers.map(({ key }) => key); - - return ( - { - setPersistedColorAction(seriesKeys, shouldClearPersistedColor.current ? null : color); - clearTemporaryColorsAction(); - requestAnimationFrame(() => colorRef?.current?.focus()); - toggleIsOpen(); - }} - onChange={(c: Color | null) => { - shouldClearPersistedColor.current = c === null; - setTemporaryColorAction(seriesKeys, c); - }} - seriesIdentifiers={seriesIdentifiers} - /> - ); - }, [ - ColorPicker, - toggleIsOpen, - color, - seriesIdentifiers, - setPersistedColorAction, - clearTemporaryColorsAction, - setTemporaryColorAction, - isOpen, - ]); + const { renderItemColor, renderColorPickerPopup } = useLegendColorPicker(props); if (isItemHidden) return null; @@ -164,27 +98,14 @@ export const LegendListItem: React.FC = (props) => { <>
  • onLegendItemMouseOver(seriesIdentifiers, path)} onMouseLeave={onLegendItemMouseOut} style={style} dir={isMostlyRTL ? 'rtl' : 'ltr'} data-ech-series-name={label} >
    -
    - { - event.stopPropagation(); - toggleIsOpen(); - }} - pointStyle={pointStyle} - > -
    +
    {renderItemColor()}
    = (props) => { totalSeriesCount={totalItems} hiddenSeriesCount={hiddenItems} /> - {legendValue && legendValue.label !== '' && !isSeriesHidden && ( -
    - {legendValue.label} + {/* // only the first for now until https://github.com/elastic/elastic-charts/issues/2096 */} + {legendValueItems?.[0] && legendValueItems[0].label !== '' && !isSeriesHidden && ( +
    + {legendValueItems[0].label}
    )} {Action && }
  • - {renderColorPicker()} + {renderColorPickerPopup()} ); }; diff --git a/packages/charts/src/components/legend/legend_table/_legend_single_item.scss b/packages/charts/src/components/legend/legend_table/_legend_single_item.scss index ef1af6753b..633b6ccfe7 100644 --- a/packages/charts/src/components/legend/legend_table/_legend_single_item.scss +++ b/packages/charts/src/components/legend/legend_table/_legend_single_item.scss @@ -1,7 +1,7 @@ @import '../variables'; @import '../../mixins'; -.echLegendSingleItem { +.echLegendTable__item { color: $euiTextColor; &:last-child .echLegendTable__cell { diff --git a/packages/charts/src/components/legend/legend_table/legend_table.tsx b/packages/charts/src/components/legend/legend_table/legend_table.tsx index 3ab9b9878f..d0602aefcf 100644 --- a/packages/charts/src/components/legend/legend_table/legend_table.tsx +++ b/packages/charts/src/components/legend/legend_table/legend_table.tsx @@ -8,11 +8,10 @@ import React from 'react'; -import { LayoutDirection } from '@elastic/charts'; - import { LegendTableBody } from './legend_table_body'; import { LegendTableHeader } from './legend_table_header'; import { LegendItem } from '../../../common/legend'; +import { LayoutDirection } from '../../../utils/common'; import { SharedLegendItemProps } from '../types'; /** @internal */ diff --git a/packages/charts/src/components/legend/legend_table/legend_table_header.tsx b/packages/charts/src/components/legend/legend_table/legend_table_header.tsx index d01d268dbc..56acde0102 100644 --- a/packages/charts/src/components/legend/legend_table/legend_table_header.tsx +++ b/packages/charts/src/components/legend/legend_table/legend_table_header.tsx @@ -38,7 +38,7 @@ export const LegendTableHeader = ({ return (
    - + {legendTitle} {legendValues.map((l) => ( diff --git a/packages/charts/src/components/legend/legend_table/legend_table_item.tsx b/packages/charts/src/components/legend/legend_table/legend_table_item.tsx index 1152438b5b..0c023a430b 100644 --- a/packages/charts/src/components/legend/legend_table/legend_table_item.tsx +++ b/packages/charts/src/components/legend/legend_table/legend_table_item.tsx @@ -7,132 +7,101 @@ */ import classNames from 'classnames'; -import React, { Component, CSSProperties } from 'react'; +import React, { useCallback } from 'react'; import { LegendTableCell } from './legend_table_cell'; import { LegendTableRow } from './legend_table_row'; import { LegendValueComponent } from './legend_value'; -import { nonNullable } from '../../../chart_types/xy_chart/state/utils/get_legend_values'; -import { LegendItem, LegendValue } from '../../../common/legend'; +import { LegendItem } from '../../../common/legend'; import { SeriesIdentifier } from '../../../common/series_id'; import { LayoutDirection } from '../../../utils/common'; -import { deepEqual } from '../../../utils/fast_deep_equal'; import { Label as ItemLabel } from '../label'; import { LegendActionComponent } from '../legend_action'; -import { LegendColorPicker as LegendColorPickerComponent } from '../legend_color_picker'; +import { useLegendColorPicker } from '../legend_color_picker'; +import { prepareLegendValues } from '../legend_item'; import { SharedLegendItemProps } from '../types'; -import { getExtra } from '../utils'; /** @internal */ export const LEGEND_HIERARCHY_MARGIN = 10; +/** @internal */ +export function nonNullable(v: T): v is NonNullable { + return v !== null || v !== undefined; +} + /** @internal */ export interface LegendItemProps extends SharedLegendItemProps { item: LegendItem; } /** @internal */ -export class LegendListItem extends Component { - static displayName = 'LegendItem'; - - shouldComponentUpdate(nextProps: LegendItemProps) { - return !deepEqual(this.props, nextProps); - } - - onLegendItemMouseOver = () => { - const { onMouseOver, mouseOverAction, item } = this.props; - // call the settings listener directly if available - if (onMouseOver) { - onMouseOver(item.seriesIdentifiers); - } - mouseOverAction(item.path); - }; - - onLegendItemMouseOut = () => { - const { onMouseOut, mouseOutAction } = this.props; - // call the settings listener directly if available - if (onMouseOut) { - onMouseOut(); - } - mouseOutAction(); - }; - - /** - * Returns click function only if toggleable or click listener is provided - */ - onLabelToggle = (legendItemId: SeriesIdentifier[]): ((negate: boolean) => void) | undefined => { - const { item, onClick, toggleDeselectSeriesAction, totalItems } = this.props; - if (totalItems <= 1 || (!item.isToggleable && !onClick)) { - return; - } +export const LegendListItem: React.FC = (props) => { + const { + extraValues, + item, + legendValues, + totalItems, + action: Action, + positionConfig, + labelOptions, + isMostlyRTL, + onClick, + toggleDeselectSeriesAction, + onLegendItemMouseOver, + onLegendItemMouseOut, + hiddenItems, + } = props; + const { color, isSeriesHidden, isItemHidden, seriesIdentifiers, label, path, isToggleable } = item; + + const itemClassNames = classNames('echLegendTable__item', 'echLegendTable__item--highlightable', { + 'echLegendTable__item--hidden': isSeriesHidden, + 'echLegendTable__item--vertical': positionConfig.direction === LayoutDirection.Vertical, + }); + + const legendValueItems = prepareLegendValues(item, legendValues, totalItems, extraValues)?.filter(nonNullable); + + const onLabelToggle = useCallback( + (legendItemId: SeriesIdentifier[]) => (negate: boolean) => { + if (totalItems <= 1 || (!isToggleable && !onClick)) { + return; + } - return (negate) => { if (onClick) { onClick(legendItemId); } - if (item.isToggleable) { + if (isToggleable) { toggleDeselectSeriesAction(legendItemId, negate); } - }; - }; - - render() { - const { - extraValues, - item, - totalItems, - action: Action, - positionConfig, - labelOptions, - isMostlyRTL, - flatLegend, - } = this.props; - const { color, isSeriesHidden, isItemHidden, seriesIdentifiers, label } = item; + }, + [onClick, toggleDeselectSeriesAction, isToggleable, totalItems], + ); - if (isItemHidden) return null; + const { renderItemColor, renderColorPickerPopup } = useLegendColorPicker(props); - const itemClassNames = classNames('echLegendSingleItem', 'echLegendSingleItem--highlightable', { - 'echLegendSingleItem--hidden': isSeriesHidden, - 'echLegendSingleItem--vertical': positionConfig.direction === LayoutDirection.Vertical, - }); + if (isItemHidden) return null; - const legendValueItems = item.values - .map((v) => { - if (v.type === LegendValue.CurrentAndLastValue || (v && !v.type)) { - return getExtra(extraValues, item, totalItems); - } - return v; - }) - .filter(nonNullable); - - const style: CSSProperties = flatLegend - ? {} - : { - [isMostlyRTL ? 'marginRight' : 'marginLeft']: LEGEND_HIERARCHY_MARGIN * (item.depth ?? 0), - }; - - return ( + return ( + <> onLegendItemMouseOver(seriesIdentifiers, path)} + onMouseLeave={onLegendItemMouseOut} dir={isMostlyRTL ? 'rtl' : 'ltr'} data-ech-series-name={label} > - + {renderItemColor()} 1 && item.isToggleable} - onToggle={this.onLabelToggle(seriesIdentifiers)} + onToggle={onLabelToggle(seriesIdentifiers)} isSeriesHidden={isSeriesHidden} totalSeriesCount={totalItems} - hiddenSeriesCount={this.props.hiddenItems} + hiddenSeriesCount={hiddenItems} /> @@ -149,6 +118,7 @@ export class LegendListItem extends Component { )} - ); - } -} + {renderColorPickerPopup()} + + ); +}; diff --git a/packages/charts/src/components/legend/types.tsx b/packages/charts/src/components/legend/types.tsx index 83cd3199b7..0e3616fccd 100644 --- a/packages/charts/src/components/legend/types.tsx +++ b/packages/charts/src/components/legend/types.tsx @@ -7,6 +7,7 @@ */ import { LegendItemExtraValues, LegendValue } from '../../common/legend'; +import { SeriesIdentifier } from '../../common/series_id'; import { LegendItemListener, BasicListener, @@ -19,11 +20,7 @@ import { setTemporaryColor as setTemporaryColorAction, setPersistedColor as setPersistedColorAction, } from '../../state/actions/colors'; -import { - onLegendItemOutAction, - onLegendItemOverAction, - onToggleDeselectSeriesAction, -} from '../../state/actions/legend'; +import { LegendPath, onToggleDeselectSeriesAction } from '../../state/actions/legend'; import { LegendLabelOptions } from '../../utils/themes/theme'; /** @internal */ @@ -38,10 +35,8 @@ export interface SharedLegendItemProps { colorPicker?: LegendColorPicker; action?: LegendAction; onClick?: LegendItemListener; - onMouseOut?: BasicListener; - onMouseOver?: LegendItemListener; - mouseOutAction: typeof onLegendItemOutAction; - mouseOverAction: typeof onLegendItemOverAction; + onLegendItemMouseOver: (seriesIdentifiers: SeriesIdentifier[], path: LegendPath) => void; + onLegendItemMouseOut: BasicListener; clearTemporaryColorsAction: typeof clearTemporaryColorsAction; setTemporaryColorAction: typeof setTemporaryColorAction; setPersistedColorAction: typeof setPersistedColorAction;