Skip to content

Commit

Permalink
Merge pull request #1619 from Shopify/legend-overflow
Browse files Browse the repository at this point in the history
Add overflow legend styles to LineChart
  • Loading branch information
susiekims authored Jan 30, 2024
2 parents 2f4a7d8 + fba7688 commit 1733682
Show file tree
Hide file tree
Showing 26 changed files with 850 additions and 34 deletions.
7 changes: 6 additions & 1 deletion packages/polaris-viz/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

<!-- ## Unreleased -->
## Unreleased

### Changed

- Hide overflowing LegendItems in LineChart, VerticalBarChart, and StackedAreaChart legends.
- Update useColorVisionEvents to accept a root prop

## [10.3.2] - 2024-01-22

Expand Down
2 changes: 1 addition & 1 deletion packages/polaris-viz/src/components/DonutChart/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export function Chart({
width && height && isLegendMounted,
);

useColorVisionEvents(shouldUseColorVisionEvents);
useColorVisionEvents({enabled: shouldUseColorVisionEvents});

useWatchColorVisionEvents({
type: COLOR_VISION_SINGLE_ITEM,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export function Chart({
xAxisOptions,
yAxisOptions,
}: ChartProps) {
useColorVisionEvents(data.length > 1);
useColorVisionEvents({enabled: data.length > 1});

const selectedTheme = useTheme();
const id = useMemo(() => uniqueId('HorizontalBarChart'), []);
Expand Down
21 changes: 19 additions & 2 deletions packages/polaris-viz/src/components/Legend/Legend.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
import {Fragment} from 'react';
import type {RefObject} from 'react';
import {DEFAULT_THEME_NAME} from '@shopify/polaris-viz-core';

import {useExternalHideEvents} from '../../hooks';
import type {LegendData} from '../../types';

import {LegendItem} from './components/';
import {LegendItem} from './components';
import type {LegendItemDimension} from './components';

export interface LegendProps {
data: LegendData[];
activeIndex?: number;
colorVisionType?: string;
theme?: string;
itemDimensions?: RefObject<LegendItemDimension[]>;
backgroundColor?: string;
indexOffset?: number;
truncate?: boolean;
}

export function Legend({
activeIndex = -1,
colorVisionType,
data,
theme = DEFAULT_THEME_NAME,
itemDimensions,
indexOffset = 0,
backgroundColor,
truncate = false,
}: LegendProps) {
const {hiddenIndexes} = useExternalHideEvents();

Expand All @@ -32,8 +42,15 @@ export function Legend({
{...legend}
activeIndex={activeIndex}
colorVisionType={colorVisionType}
index={index}
index={index + indexOffset}
theme={theme}
backgroundColor={backgroundColor}
onDimensionChange={(dimensions) => {
if (itemDimensions?.current) {
itemDimensions.current[index + indexOffset] = dimensions;
}
}}
truncate={truncate}
/>
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
margin: -2px 0;
font-size: 12px;
font-family: $font-stack-base;
white-space: nowrap;
min-width: 0;
}

.Text {
overflow: hidden;
text-overflow: ellipsis;
}

.IconContainer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
getColorVisionStylesForActiveIndex,
} from '@shopify/polaris-viz-core';
import type {ReactNode} from 'react';
import {useEffect, useRef, useState} from 'react';

import {
LEGEND_ITEM_LEFT_PADDING,
Expand All @@ -16,12 +17,23 @@ import {useTheme} from '../../../../hooks';

import style from './LegendItem.scss';

export interface LegendItemDimension {
width: number;
height: number;
}

export const MINIMUM_LEGEND_ITEM_WIDTH = 100;
export const MINIMUM_LEGEND_ITEM_WITH_VALUE_WIDTH = 200;

export interface LegendItemProps extends LegendData {
index: number;
activeIndex?: number;
colorVisionType?: string;
renderSeriesIcon?: () => ReactNode;
theme?: string;
onDimensionChange?: ({width, height}: LegendItemDimension) => void;
backgroundColor?: string;
truncate?: boolean;
}

export function LegendItem({
Expand All @@ -35,8 +47,26 @@ export function LegendItem({
shape,
theme,
value,
onDimensionChange,
backgroundColor,
truncate = false,
}: LegendItemProps) {
const selectedTheme = useTheme(theme);
const ref = useRef<HTMLButtonElement | null>(null);
const [width, setWidth] = useState(0);

const minWidth =
value != null
? MINIMUM_LEGEND_ITEM_WITH_VALUE_WIDTH
: MINIMUM_LEGEND_ITEM_WIDTH;

useEffect(() => {
if (onDimensionChange && ref.current != null) {
const {width, height} = ref.current.getBoundingClientRect();
setWidth(width);
onDimensionChange({width: Math.min(minWidth, Math.round(width)), height});
}
}, [onDimensionChange, ref, minWidth]);

const colorBlindAttrs =
colorVisionType == null
Expand All @@ -46,20 +76,27 @@ export function LegendItem({
index,
});

const background = backgroundColor ?? selectedTheme.legend.backgroundColor;

return (
<button
{...colorBlindAttrs}
style={{
background: selectedTheme.legend.backgroundColor,
background,
...getColorVisionStylesForActiveIndex({
activeIndex,
index,
}),
paddingLeft: LEGEND_ITEM_LEFT_PADDING,
paddingRight: LEGEND_ITEM_RIGHT_PADDING,
gap: LEGEND_ITEM_GAP,
// if there is overflow, add a max width and truncate with ellipsis
maxWidth: truncate ? minWidth : undefined,
// if the item width is less than the minWidth, don't set a min width
minWidth: width < minWidth ? undefined : minWidth,
}}
className={style.Legend}
ref={ref}
>
{renderSeriesIcon == null ? (
<span
Expand All @@ -72,9 +109,21 @@ export function LegendItem({
renderSeriesIcon()
)}
<span className={style.TextContainer}>
<span style={{color: selectedTheme.legend.labelColor}}>{name}</span>
<span
className={style.Text}
style={{
color: selectedTheme.legend.labelColor,
}}
>
{name}
</span>
{value == null ? null : (
<span style={{color: selectedTheme.legend.valueColor}}>{value}</span>
<span
className={style.Text}
style={{color: selectedTheme.legend.valueColor}}
>
{value}
</span>
)}
</span>
</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export {LegendItem} from './LegendItem';
export type {LegendItemDimension} from './LegendItem';
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import {mount} from '@shopify/react-testing';

import type {LegendItemProps} from '../LegendItem';
import {LegendItem} from '../LegendItem';
import {
LegendItem,
MINIMUM_LEGEND_ITEM_WIDTH,
MINIMUM_LEGEND_ITEM_WITH_VALUE_WIDTH,
} from '../LegendItem';

const mockProps: LegendItemProps = {
activeIndex: 2,
colorVisionType: 'someType',
index: 0,
name: 'Legend Name',
color: 'red',
onDimensionChange: jest.fn(),
};

describe('<LegendItem />', () => {
afterEach(() => {
jest.restoreAllMocks();
});

it('renders a button', () => {
const item = mount(<LegendItem {...mockProps} />);

Expand Down Expand Up @@ -68,4 +77,86 @@ describe('<LegendItem />', () => {
expect(button?.props?.style?.opacity).toStrictEqual(0.3);
});
});

describe('onDimensionChange', () => {
it('calls onDimensionChange if passed in', () => {
jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(
() =>
({
width: 50,
height: 50,
} as DOMRect),
);

const onDimensionChangeSpy = jest.fn();
mount(
<LegendItem {...mockProps} onDimensionChange={onDimensionChangeSpy} />,
);

expect(onDimensionChangeSpy).toHaveBeenCalledWith({
width: 50,
height: 50,
});
});
});

describe('max and min width', () => {
it('sets a maxWidth if truncate is true', () => {
const item = mount(<LegendItem {...mockProps} truncate />);

expect(item.find('button')).toHaveReactProps({
style: expect.objectContaining({
maxWidth: MINIMUM_LEGEND_ITEM_WIDTH,
}),
});
});

it('sets a maxWidth for items with values if truncate is true', () => {
const item = mount(
<LegendItem {...mockProps} truncate value="$100.00" />,
);

expect(item.find('button')).toHaveReactProps({
style: expect.objectContaining({
maxWidth: MINIMUM_LEGEND_ITEM_WITH_VALUE_WIDTH,
}),
});
});

it('does not set a minWidth if the width is smaller than MINIMUM_LEGEND_ITEM_WIDTH', () => {
jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(
() =>
({
width: MINIMUM_LEGEND_ITEM_WIDTH - 1,
height: 0,
} as DOMRect),
);

const item = mount(<LegendItem {...mockProps} />);

expect(item.find('button')).toHaveReactProps({
style: expect.objectContaining({
minWidth: undefined,
}),
});
});

it('sets a minWidth if the item width is greater than MINIMUM_LEGEND_ITEM_WIDTH', () => {
jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(
() =>
({
width: MINIMUM_LEGEND_ITEM_WIDTH + 1,
height: 0,
} as DOMRect),
);

const item = mount(<LegendItem {...mockProps} />);

expect(item.find('button')).toHaveReactProps({
style: expect.objectContaining({
minWidth: MINIMUM_LEGEND_ITEM_WIDTH,
}),
});
});
});
});
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export {LegendItem} from './LegendItem';
export type {LegendItemDimension} from './LegendItem';
1 change: 1 addition & 0 deletions packages/polaris-viz/src/components/Legend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export {Legend} from './Legend';
export {LegendItem} from './components';
export type {LegendProps} from './Legend';
export {estimateLegendItemWidth} from './utilities/estimateLegendItemWidth';
export type {LegendItemDimension} from './components';
24 changes: 24 additions & 0 deletions packages/polaris-viz/src/components/Legend/tests/Legend.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {createRef} from 'react';
import {mount} from '@shopify/react-testing';

import type {LegendProps} from '../Legend';
import {Legend} from '../Legend';
import {LegendItem} from '../../Legend/components';
import type {LegendItemDimension} from '../../Legend/components';

const mockProps: LegendProps = {
data: [
Expand All @@ -17,4 +19,26 @@ describe('<Legend />', () => {

expect(component).toContainReactComponentTimes(LegendItem, 2);
});

it('adds the indexOffset to the index if provided', () => {
const component = mount(<Legend {...mockProps} indexOffset={3} />);
const legendItems = component.findAll(LegendItem);
expect(legendItems[0]).toHaveReactProps({
index: 3,
});
expect(legendItems[1]).toHaveReactProps({
index: 4,
});
});

it('updates the item dimensions', () => {
const ref = createRef<LegendItemDimension[]>();
ref.current = [];

const component = mount(<Legend {...mockProps} itemDimensions={ref} />);
const newDimensions = {width: 50, height: 50};

component.find(LegendItem)?.trigger('onDimensionChange', newDimensions);
expect(ref.current[0]).toStrictEqual(newDimensions);
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
.Container {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
Loading

0 comments on commit 1733682

Please sign in to comment.