Skip to content

Conversation

@annacmc
Copy link
Contributor

@annacmc annacmc commented Dec 6, 2025

Proposed changes:

  • Add new TrendIndicator component for displaying directional trends with values
  • Support three directions: up (green), down (red), neutral (gray)
  • Include arrow icon for up/down directions, no icon for neutral
  • Export component and types from main package entry
  • Add package.json export paths for tree-shaking support

Other information:

  • Have you written new tests for your changes, if applicable?
  • Have you checked the E2E test CI results, and verified that your changes do not break them?
  • Have you tested your changes on WordPress.com, if applicable (if so, you'll see a generated comment below with a script to run)?

Does this pull request change what data or activity we track or use?

No, it does not.

Testing instructions:

  • Go to Storybook -> Charts -> Components -> Trend Indicator
  • Test the three stories:
    • Up: Should display green arrow pointing up with "+14%"
    • Down: Should display red arrow pointing down with "-5%"
    • Neutral: Should display gray text "0%" with no icon
  • Verify the component accepts className and style props for customization
  • Run tests: pnpm test -- src/components/trend-indicator
  • Build package: pnpm build and verify trend-indicator outputs are generated

@annacmc annacmc added [Type] Enhancement Changes to an existing feature — removing, adding, or changing parts of it [Status] In Progress labels Dec 6, 2025
@annacmc annacmc self-assigned this Dec 6, 2025
@annacmc annacmc requested a review from Copilot December 6, 2025 03:43
@github-actions
Copy link
Contributor

github-actions bot commented Dec 6, 2025

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack), and enable the add/charts-new-trend-indicator branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack add/charts-new-trend-indicator

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions
Copy link
Contributor

github-actions bot commented Dec 6, 2025

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add a "[Type]" label (Bug, Enhancement, Janitorial, Task).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a new TrendIndicator component to the Charts package for displaying directional trend information with visual indicators (arrows) and values. The component supports three trend directions (up, down, neutral) with corresponding color coding and optional icons.

Key Changes:

  • New reusable TrendIndicator component with TypeScript types and SCSS styling
  • Package.json exports configuration for tree-shaking support
  • Comprehensive test coverage and Storybook stories for visual documentation

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/index.ts Exports TrendIndicator component and types from main package entry
src/components/trend-indicator/types.ts Defines TypeScript types for TrendDirection and TrendIndicatorProps
src/components/trend-indicator/trend-indicator.tsx Implements core component with Icon subcomponent, color mapping, and rendering logic
src/components/trend-indicator/trend-indicator.module.scss Provides responsive styling with flexbox layout and BEM naming convention
src/components/trend-indicator/test/trend-indicator.test.tsx Adds unit tests covering rendering, icons, and className prop handling
src/components/trend-indicator/stories/index.stories.tsx Creates Storybook stories for Up, Down, and Neutral trend states
src/components/trend-indicator/index.ts Re-exports component and types for clean import paths
package.json Adds export paths and TypeScript type definitions for the new component
changelog/add-charts-new-trend-indicator Documents the addition as a minor version change

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +53
import clsx from 'clsx';
import styles from './trend-indicator.module.scss';
import type { TrendIndicatorProps, TrendDirection } from './types';

const COLORS: Record< TrendDirection, string > = {
up: '#1a8917',
down: '#d63638',
neutral: '#646970',
};

const Icon = ( { direction }: { direction: TrendDirection } ) => {
if ( direction === 'neutral' ) {
return null;
}

const isUp = direction === 'up';
return (
<svg
className={ styles[ 'trend-indicator__icon' ] }
viewBox="0 0 16 16"
fill="none"
role="img"
aria-hidden="true"
>
<path
d={ isUp ? 'M8 13V3M4 7l4-4 4 4' : 'M8 3v10M4 9l4 4 4-4' }
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};

/**
* TrendIndicator displays a directional trend with a value.
* Used to show percentage changes or growth metrics.
*
* @param {TrendIndicatorProps} props - Component props
* @return {JSX.Element} The rendered trend indicator
*/
export function TrendIndicator( { direction, value, className, style }: TrendIndicatorProps ) {
return (
<span
className={ clsx( styles[ 'trend-indicator' ], className ) }
style={ { ...style, color: COLORS[ direction ] } }
>
<Icon direction={ direction } />
<span className={ styles[ 'trend-indicator__value' ] }>{ value }</span>
</span>
);
}
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

The component should use internationalization (i18n) for the accessible label text. Other components in this package use the @wordpress/i18n library's __() function for translatable strings (see bar-chart.tsx, line-chart.tsx).

Example implementation:

import { __ } from '@wordpress/i18n';

// In the component:
const getDirectionLabel = ( direction: TrendDirection ): string => {
  switch ( direction ) {
    case 'up':
      return __( 'Increase', 'jetpack-charts' );
    case 'down':
      return __( 'Decrease', 'jetpack-charts' );
    case 'neutral':
      return __( 'No change', 'jetpack-charts' );
  }
};

This ensures the component works correctly for non-English users.

Copilot uses AI. Check for mistakes.
className={ styles[ 'trend-indicator__icon' ] }
viewBox="0 0 16 16"
fill="none"
role="img"
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

The SVG icon has conflicting ARIA attributes. Setting both role="img" and aria-hidden="true" is contradictory - role="img" exposes the element to assistive technologies while aria-hidden="true" hides it.

Since the icon is decorative and the semantic meaning should be conveyed through the parent component's aria-label, the icon should only have aria-hidden="true":

<svg
  className={ styles[ 'trend-indicator__icon' ] }
  viewBox="0 0 16 16"
  fill="none"
  aria-hidden="true"
>

Remove the role="img" attribute.

Suggested change
role="img"

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +38
render( <TrendIndicator direction="up" value="+10%" /> );

expect( screen.getByRole( 'img', { hidden: true } ) ).toBeInTheDocument();
} );

it( 'renders icon for down direction', () => {
render( <TrendIndicator direction="down" value="-10%" /> );

expect( screen.getByRole( 'img', { hidden: true } ) ).toBeInTheDocument();
} );

it( 'does not render icon for neutral direction', () => {
render( <TrendIndicator direction="neutral" value="0%" /> );

expect( screen.queryByRole( 'img', { hidden: true } ) ).not.toBeInTheDocument();
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

This test is querying for role="img" which should be removed from the component (see the accessibility issue in the main component file). After removing role="img" from the SVG, these tests will need to be updated to use a different query method.

Consider using container.querySelector('svg') instead:

it( 'renders icon for up direction', () => {
  const { container } = render( <TrendIndicator direction="up" value="+10%" /> );
  
  const svg = container.querySelector( 'svg' );
  expect( svg ).toBeInTheDocument();
  expect( svg ).toHaveAttribute( 'aria-hidden', 'true' );
} );
Suggested change
render( <TrendIndicator direction="up" value="+10%" /> );
expect( screen.getByRole( 'img', { hidden: true } ) ).toBeInTheDocument();
} );
it( 'renders icon for down direction', () => {
render( <TrendIndicator direction="down" value="-10%" /> );
expect( screen.getByRole( 'img', { hidden: true } ) ).toBeInTheDocument();
} );
it( 'does not render icon for neutral direction', () => {
render( <TrendIndicator direction="neutral" value="0%" /> );
expect( screen.queryByRole( 'img', { hidden: true } ) ).not.toBeInTheDocument();
const { container } = render( <TrendIndicator direction="up" value="+10%" /> );
const svg = container.querySelector('svg');
expect( svg ).toBeInTheDocument();
expect( svg ).toHaveAttribute( 'aria-hidden', 'true' );
} );
it( 'renders icon for down direction', () => {
const { container } = render( <TrendIndicator direction="down" value="-10%" /> );
const svg = container.querySelector('svg');
expect( svg ).toBeInTheDocument();
expect( svg ).toHaveAttribute( 'aria-hidden', 'true' );
} );
it( 'does not render icon for neutral direction', () => {
const { container } = render( <TrendIndicator direction="neutral" value="0%" /> );
const svg = container.querySelector('svg');
expect( svg ).not.toBeInTheDocument();

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +38
render( <TrendIndicator direction="up" value="+10%" /> );

expect( screen.getByRole( 'img', { hidden: true } ) ).toBeInTheDocument();
} );

it( 'renders icon for down direction', () => {
render( <TrendIndicator direction="down" value="-10%" /> );

expect( screen.getByRole( 'img', { hidden: true } ) ).toBeInTheDocument();
} );

it( 'does not render icon for neutral direction', () => {
render( <TrendIndicator direction="neutral" value="0%" /> );

expect( screen.queryByRole( 'img', { hidden: true } ) ).not.toBeInTheDocument();
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

This test is querying for role="img" which should be removed from the component (see the accessibility issue in the main component file). After removing role="img" from the SVG, these tests will need to be updated to use a different query method.

Consider using container.querySelector('svg') instead:

it( 'does not render icon for neutral direction', () => {
  const { container } = render( <TrendIndicator direction="neutral" value="0%" /> );
  
  expect( container.querySelector( 'svg' ) ).not.toBeInTheDocument();
} );
Suggested change
render( <TrendIndicator direction="up" value="+10%" /> );
expect( screen.getByRole( 'img', { hidden: true } ) ).toBeInTheDocument();
} );
it( 'renders icon for down direction', () => {
render( <TrendIndicator direction="down" value="-10%" /> );
expect( screen.getByRole( 'img', { hidden: true } ) ).toBeInTheDocument();
} );
it( 'does not render icon for neutral direction', () => {
render( <TrendIndicator direction="neutral" value="0%" /> );
expect( screen.queryByRole( 'img', { hidden: true } ) ).not.toBeInTheDocument();
const { container } = render( <TrendIndicator direction="up" value="+10%" /> );
expect( container.querySelector('svg') ).toBeInTheDocument();
} );
it( 'renders icon for down direction', () => {
const { container } = render( <TrendIndicator direction="down" value="-10%" /> );
expect( container.querySelector('svg') ).toBeInTheDocument();
} );
it( 'does not render icon for neutral direction', () => {
const { container } = render( <TrendIndicator direction="neutral" value="0%" /> );
expect( container.querySelector('svg') ).not.toBeInTheDocument();

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +51
<span
className={ clsx( styles[ 'trend-indicator' ], className ) }
style={ { ...style, color: COLORS[ direction ] } }
>
<Icon direction={ direction } />
<span className={ styles[ 'trend-indicator__value' ] }>{ value }</span>
</span>
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

The component lacks an aria-label or accessible text to convey the meaning of the trend to screen reader users. Users relying on assistive technologies would only hear the value (e.g., "+14%") without understanding whether it represents an increase, decrease, or neutral trend.

Consider adding an aria-label that describes the full meaning, such as:

<span
  className={ clsx( styles[ 'trend-indicator' ], className ) }
  style={ { ...style, color: COLORS[ direction ] } }
  aria-label={ `${ direction === 'up' ? 'Increase' : direction === 'down' ? 'Decrease' : 'No change' }: ${ value }` }
>

Alternatively, use a visually hidden text element to provide context alongside the visual indicator.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +9
const COLORS: Record< TrendDirection, string > = {
up: '#1a8917',
down: '#d63638',
neutral: '#646970',
};
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

[nitpick] The hardcoded color values don't integrate with the existing chart theming system. The charts package uses a theme provider (GlobalChartsProvider) that allows consumers to customize colors through themes (see themes.ts).

For consistency with other components like ConversionFunnelChart (which uses positiveChangeColor and negativeChangeColor from theme settings), consider:

  1. Adding trendIndicator colors to the theme configuration in themes.ts:
trendIndicator: {
  upColor: '#1a8917',
  downColor: '#d63638', 
  neutralColor: '#646970',
}
  1. Accessing theme colors via the context:
const { trendIndicatorSettings } = useChartTheme();
const COLORS: Record< TrendDirection, string > = {
  up: trendIndicatorSettings?.upColor || '#1a8917',
  down: trendIndicatorSettings?.downColor || '#d63638',
  neutral: trendIndicatorSettings?.neutralColor || '#646970',
};

This would allow consumers to customize trend indicator colors to match their design system while maintaining sensible defaults.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +49
import { render, screen } from '@testing-library/react';
import { TrendIndicator } from '../trend-indicator';

describe( 'TrendIndicator', () => {
it( 'renders up trend with value', () => {
render( <TrendIndicator direction="up" value="+14%" /> );

expect( screen.getByText( '+14%' ) ).toBeInTheDocument();
} );

it( 'renders down trend with value', () => {
render( <TrendIndicator direction="down" value="-5%" /> );

expect( screen.getByText( '-5%' ) ).toBeInTheDocument();
} );

it( 'renders neutral trend with value', () => {
render( <TrendIndicator direction="neutral" value="0%" /> );

expect( screen.getByText( '0%' ) ).toBeInTheDocument();
} );

it( 'renders icon for up direction', () => {
render( <TrendIndicator direction="up" value="+10%" /> );

expect( screen.getByRole( 'img', { hidden: true } ) ).toBeInTheDocument();
} );

it( 'renders icon for down direction', () => {
render( <TrendIndicator direction="down" value="-10%" /> );

expect( screen.getByRole( 'img', { hidden: true } ) ).toBeInTheDocument();
} );

it( 'does not render icon for neutral direction', () => {
render( <TrendIndicator direction="neutral" value="0%" /> );

expect( screen.queryByRole( 'img', { hidden: true } ) ).not.toBeInTheDocument();
} );

it( 'applies custom className', () => {
const { container } = render(
<TrendIndicator direction="up" value="+10%" className="custom-class" />
);

// eslint-disable-next-line testing-library/no-node-access
expect( container.firstChild ).toHaveClass( 'custom-class' );
} );
} );
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

Tests are missing for the accessibility concern of providing semantic meaning to screen reader users. Consider adding a test to verify that the component provides appropriate accessible text:

it( 'provides accessible label for screen readers', () => {
  const { container } = render(
    <TrendIndicator direction="up" value="+14%" />
  );
  
  const indicator = container.firstChild as HTMLElement;
  expect( indicator ).toHaveAttribute( 'aria-label' );
  expect( indicator.getAttribute( 'aria-label' ) ).toMatch( /increase/i );
} );

This would ensure the component remains accessible as it evolves.

Copilot uses AI. Check for mistakes.
@jp-launch-control
Copy link

Code Coverage Summary

3 files are newly checked for coverage.

File Coverage
projects/js-packages/charts/src/components/trend-indicator/trend-indicator.tsx 7/7 (100.00%) 💚
projects/js-packages/charts/src/components/trend-indicator/index.ts 0/0 (—%) 🤷

Full summary · PHP report · JS report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[JS Package] Charts RNA [Status] In Progress [Tests] Includes Tests [Type] Enhancement Changes to an existing feature — removing, adding, or changing parts of it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants