From b13403283679930c2e5d7f7e0880bfe558e1279e Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:55:45 +1300 Subject: [PATCH 01/14] Premium Analytics: Let the chart wrappers take an accessible name The charts package can now name a chart for screen readers. The toolkit wrappers dropped the prop, so widgets had no way to supply a name. --- .../chart-bar/__tests__/bar-chart.test.tsx | 38 +++++++++++++++++++ .../src/components/chart-bar/bar-chart.tsx | 5 +++ .../__tests__/comparative-bar-chart.test.tsx | 14 +++++++ .../comparative-bar-chart.tsx | 5 +++ .../__tests__/comparative-line-chart.test.tsx | 14 +++++++ .../comparative-line-chart.tsx | 2 + .../__tests__/metric-tabs-chart.test.tsx | 18 +++++++++ .../metric-tabs-chart/metric-tabs-chart.tsx | 10 +++++ .../widgets-toolkit-chart-mock.test.tsx | 1 + 9 files changed, 107 insertions(+) create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/__tests__/bar-chart.test.tsx diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/__tests__/bar-chart.test.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/__tests__/bar-chart.test.tsx new file mode 100644 index 000000000000..9e9f2b6e5da5 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/__tests__/bar-chart.test.tsx @@ -0,0 +1,38 @@ +/** + * External dependencies + */ +import { render } from '@testing-library/react'; +/** + * Internal dependencies + */ +import { mockBarChartSpy, resetMockCharts } from '../../../../../../tests/js/chart-test-utils'; +import { BarChart } from '../bar-chart'; + +jest.mock( '@jetpack-premium-analytics/externals', () => + jest.requireActual( '../../../../../../tests/js/chart-test-utils' ).mockChartExternals() +); + +jest.mock( + '@wordpress/compose', + () => jest.requireActual( '../../../../../../tests/js/chart-test-utils' ).mockWordPressCompose +); + +describe( 'BarChart', () => { + beforeEach( () => { + resetMockCharts(); + } ); + + it( 'names the chart with ariaLabel', () => { + render( + + ); + + expect( mockBarChartSpy ).toHaveBeenLastCalledWith( + expect.objectContaining( { ariaLabel: 'Sales by device' } ) + ); + } ); +} ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx index d0b18bc150ff..5047dc84a2a8 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx @@ -48,6 +48,9 @@ export type BarChartProps = { className?: string; + /** Accessible name of the chart; name it after what it shows, such as the widget title. */ + ariaLabel?: string; + emptyStateIcon?: React.ComponentProps< typeof Icon >[ 'icon' ]; emptyStateText?: string; @@ -116,6 +119,7 @@ export function BarChart( { dataFormat, styles: stylesProp, className, + ariaLabel, emptyStateIcon, emptyStateText, showZeroValues = true, @@ -192,6 +196,7 @@ export function BarChart( { return ( { setSettings( originalSettings ); } ); + it( 'names the chart with ariaLabel', () => { + render( + + ); + + expect( mockBarChartSpy ).toHaveBeenLastCalledWith( + expect.objectContaining( { ariaLabel: 'Traffic summary' } ) + ); + } ); + it( 'passes no x tickFormat when no tick format is requested', () => { render( ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/comparative-bar-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/comparative-bar-chart.tsx index cbce086d31e8..c9d1f1eeb7d0 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/comparative-bar-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/comparative-bar-chart.tsx @@ -55,6 +55,9 @@ export type ComparativeBarChartProps = { className?: string; + /** Accessible name of the chart; name it after what it shows, such as the widget title. */ + ariaLabel?: string; + /** Format for chart values: y-axis ticks and tooltips. */ dataFormat: DataFormat; @@ -124,6 +127,7 @@ export function ComparativeBarChart( { series, className, chartId: providedChartId, + ariaLabel, dataFormat, tickFormat: xTickFormatType, tickResolution, @@ -317,6 +321,7 @@ export function ComparativeBarChart( { { setSettings( originalSettings ); } ); + it( 'names the chart with ariaLabel', () => { + render( + + ); + + expect( mockLineChartSpy ).toHaveBeenLastCalledWith( + expect.objectContaining( { ariaLabel: 'Traffic summary' } ) + ); + } ); + // `useChartMargin` sizes the gutters itself; overriding them here clipped the edge dates. it.each( [ [ 'by default', DATA_FORMAT ], diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx index 8597a460ccc5..6e6f55dd69b0 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx @@ -158,6 +158,7 @@ export function ComparativeLineChart( { styles: stylesProp, className, chartId, + ariaLabel, dataFormat, tickFormat: xTickFormatType, tickResolution, @@ -305,6 +306,7 @@ export function ComparativeLineChart( { { ); } ); + it.each( [ + [ 'line', mockLineSpy ], + [ 'bar', mockBarSpy ], + ] as const )( 'names the %s chart with ariaLabel', ( type, spy ) => { + render( + + ); + + expect( spy ).toHaveBeenLastCalledWith( + expect.objectContaining( { ariaLabel: 'Traffic summary' } ) + ); + } ); + it( 'keeps the previous period as a same-group comparison series in both chart types', () => { render( ); render( ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/metric-tabs-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/metric-tabs-chart.tsx index 52adefd6afd2..4d1931f207a2 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/metric-tabs-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/metric-tabs-chart.tsx @@ -140,6 +140,8 @@ export interface MetricTabsChartProps { controls?: ReactNode; /** Accessible label for the metric tab list. */ groupLabel?: string; + /** Accessible name of the chart; name it after what it shows, such as the widget title. */ + ariaLabel?: string; /** * The series' bucket size, declared to the x-axis so tick formats follow the * known granularity rather than being inferred from point spacing. @@ -222,6 +224,7 @@ function MetricChart( { dataFormat, chartType, chartId, + ariaLabel, tickResolution, onDatumClick, baseline, @@ -235,6 +238,7 @@ function MetricChart( { chartType: MetricTabsChartType; baseline?: ChartBaseline; chartId: string; + ariaLabel?: string; tickResolution?: TickResolution; onDatumClick?: ( date: Date ) => void; empty?: ReactNode; @@ -364,6 +368,7 @@ function MetricChart( { return chartType === 'bar' ? ( Date: Thu, 8 Oct 2026 12:08:37 +1300 Subject: [PATCH 02/14] Premium Analytics: Name the metric charts after their widgets Screen reader users can tell the dashboard's charts apart. --- .../premium-analytics/widgets/author-performance/render.tsx | 1 + .../premium-analytics/widgets/email-time-series/render.tsx | 1 + .../packages/premium-analytics/widgets/post-views/render.tsx | 1 + .../premium-analytics/widgets/subscribers-chart/render.tsx | 1 + .../packages/premium-analytics/widgets/traffic-chart/render.tsx | 1 + .../widgets/video-detail-views-performance/render.tsx | 1 + .../woocommerce-stats/widgets/store-performance/render.tsx | 1 + 7 files changed, 7 insertions(+) diff --git a/projects/packages/premium-analytics/widgets/author-performance/render.tsx b/projects/packages/premium-analytics/widgets/author-performance/render.tsx index 7d7c44339ccc..a97d077ac756 100644 --- a/projects/packages/premium-analytics/widgets/author-performance/render.tsx +++ b/projects/packages/premium-analytics/widgets/author-performance/render.tsx @@ -135,6 +135,7 @@ function AuthorPerformanceInner( { chartType }: AuthorPerformanceInnerProps ) { metrics={ metricTabs } dataFormat={ DATA_FORMAT } chartType={ chartType } + ariaLabel={ __( 'Author performance', 'jetpack-premium-analytics-pkg' ) } empty={ } /> diff --git a/projects/packages/premium-analytics/widgets/email-time-series/render.tsx b/projects/packages/premium-analytics/widgets/email-time-series/render.tsx index 4ea715ee4b18..24f68ffea4e1 100644 --- a/projects/packages/premium-analytics/widgets/email-time-series/render.tsx +++ b/projects/packages/premium-analytics/widgets/email-time-series/render.tsx @@ -167,6 +167,7 @@ function EmailTimeSeriesReport( { metric, chartType }: EmailTimeSeriesReportProp metrics={ metricTabs } dataFormat={ DATA_FORMAT } chartType={ chartType } + ariaLabel={ __( 'Email performance', 'jetpack-premium-analytics-pkg' ) } empty={ } /> diff --git a/projects/packages/premium-analytics/widgets/post-views/render.tsx b/projects/packages/premium-analytics/widgets/post-views/render.tsx index 1ea46713463e..691fdb71468f 100644 --- a/projects/packages/premium-analytics/widgets/post-views/render.tsx +++ b/projects/packages/premium-analytics/widgets/post-views/render.tsx @@ -98,6 +98,7 @@ function PostViewsInner( { chartType }: PostViewsInnerProps ) { metrics={ metricTabs } dataFormat={ DATA_FORMAT } chartType={ chartType } + ariaLabel={ __( 'Post views', 'jetpack-premium-analytics-pkg' ) } empty={ } /> diff --git a/projects/packages/premium-analytics/widgets/subscribers-chart/render.tsx b/projects/packages/premium-analytics/widgets/subscribers-chart/render.tsx index afedfa67bca6..2e37fdd300cb 100644 --- a/projects/packages/premium-analytics/widgets/subscribers-chart/render.tsx +++ b/projects/packages/premium-analytics/widgets/subscribers-chart/render.tsx @@ -135,6 +135,7 @@ function SubscribersChartInner( { chartType }: SubscribersChartInnerProps ) { chartType={ chartType } groupLabel={ groupLabel } baseline="padded" + ariaLabel={ __( 'Subscriber summary', 'jetpack-premium-analytics-pkg' ) } empty={ } /> diff --git a/projects/packages/premium-analytics/widgets/traffic-chart/render.tsx b/projects/packages/premium-analytics/widgets/traffic-chart/render.tsx index 8d7957629e04..8f2cb852709f 100644 --- a/projects/packages/premium-analytics/widgets/traffic-chart/render.tsx +++ b/projects/packages/premium-analytics/widgets/traffic-chart/render.tsx @@ -118,6 +118,7 @@ function TrafficChartInner( { chartType, interval }: TrafficChartInnerProps ) { groupLabel={ groupLabel } tickResolution={ period } onDatumClick={ openBucket } + ariaLabel={ __( 'Traffic summary', 'jetpack-premium-analytics-pkg' ) } empty={ } /> diff --git a/projects/packages/premium-analytics/widgets/video-detail-views-performance/render.tsx b/projects/packages/premium-analytics/widgets/video-detail-views-performance/render.tsx index d1382d389fc1..5cda8707a2c3 100644 --- a/projects/packages/premium-analytics/widgets/video-detail-views-performance/render.tsx +++ b/projects/packages/premium-analytics/widgets/video-detail-views-performance/render.tsx @@ -82,6 +82,7 @@ function VideoDetailViewsPerformanceInner( { chartType }: VideoDetailViewsPerfor dataFormat={ COUNT_FORMAT } chartType={ chartType } groupLabel={ groupLabel } + ariaLabel={ __( 'Video performance', 'jetpack-premium-analytics-pkg' ) } empty={ } /> diff --git a/projects/packages/woocommerce-stats/widgets/store-performance/render.tsx b/projects/packages/woocommerce-stats/widgets/store-performance/render.tsx index cff1713de953..d2352ce9ef08 100644 --- a/projects/packages/woocommerce-stats/widgets/store-performance/render.tsx +++ b/projects/packages/woocommerce-stats/widgets/store-performance/render.tsx @@ -112,6 +112,7 @@ function StorePerformance() { metrics={ metrics } dataFormat={ DEFAULT_DATA_FORMAT } groupLabel={ __( 'Store metric', 'jetpack-woocommerce-stats-pkg' ) } + ariaLabel={ __( 'Store performance', 'jetpack-woocommerce-stats-pkg' ) } empty={ } /> From 05881ef189cd1d48b2dc01870f6e77e87c49ee44 Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:08:46 +1300 Subject: [PATCH 03/14] Premium Analytics: Name the store bar charts after their widgets Screen reader users can tell the dashboard's charts apart. --- .../revenue-by-customer-type-widget.tsx | 17 ++++++++++++++--- .../sales-by-coupon/sales-by-coupon-widget.tsx | 3 ++- .../sales-by-device/sales-by-device-widget.tsx | 4 ++++ .../total-returns/total-returns-widget.tsx | 1 + .../widgets/bookings-by-device/render.tsx | 1 + .../widgets/sales-by-coupon-usage/render.tsx | 5 ++++- .../widgets/sales-by-coupon/render.tsx | 3 ++- 7 files changed, 28 insertions(+), 6 deletions(-) diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx index 62012273d479..c1db4c8f5c07 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx @@ -21,6 +21,7 @@ type CustomerTypeRevenueWidgetProps = { * @see BOOKINGS_FILTER for booking products (booking, bookable-event, bookable-service) */ filter?: FilterCondition; + ariaLabel: string; }; /** @@ -29,7 +30,7 @@ type CustomerTypeRevenueWidgetProps = { * * Must be used within a WidgetRoot which provides reportParams via context. */ -function CustomerTypeRevenueWidget( { filter }: CustomerTypeRevenueWidgetProps ) { +function CustomerTypeRevenueWidget( { filter, ariaLabel }: CustomerTypeRevenueWidgetProps ) { const { reportParams } = useWidgetRootContext(); const { primary, comparison, isLoading, isFetching, hasData, isError, refetch } = @@ -73,6 +74,7 @@ function CustomerTypeRevenueWidget( { filter }: CustomerTypeRevenueWidgetProps ) type: 'currency', options: { useMultipliers: true }, } } + ariaLabel={ ariaLabel } /> ); @@ -83,7 +85,11 @@ function CustomerTypeRevenueWidget( { filter }: CustomerTypeRevenueWidgetProps ) * No product type filtering applied. */ export function RevenueByCustomerTypeWidget() { - return ; + return ( + + ); } /** @@ -91,5 +97,10 @@ export function RevenueByCustomerTypeWidget() { * Filters to: booking, bookable-event, and bookable-service product types. */ export function BookingsRevenueByCustomerTypeWidget() { - return ; + return ( + + ); } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx index 47b8dbf2fe12..4e1dbf0eebed 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx @@ -22,7 +22,7 @@ const TOP_COUPON_SEGMENTS = 3; * * Must be used within a WidgetRoot which provides reportParams via context. */ -export function SalesByCouponWidget() { +export function SalesByCouponWidget( { ariaLabel }: { ariaLabel?: string } = {} ) { const { reportParams } = useWidgetRootContext(); const { primary, comparison, isLoading, isFetching, hasData, isError, refetch } = @@ -64,6 +64,7 @@ export function SalesByCouponWidget() { type: 'currency', options: { useMultipliers: true }, } } + ariaLabel={ ariaLabel } /> ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx index 48289f51ddec..74abc0901cce 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx @@ -25,6 +25,8 @@ type SalesByDeviceWidgetProps = { emptyStateText?: string; errorText?: string; + + ariaLabel?: string; }; /** @@ -36,6 +38,7 @@ export function SalesByDeviceWidget( { filter, emptyStateText, errorText, + ariaLabel = __( 'Sales by device', 'jetpack-premium-analytics-pkg' ), }: SalesByDeviceWidgetProps ) { const { reportParams } = useWidgetRootContext(); @@ -89,6 +92,7 @@ export function SalesByDeviceWidget( { type: 'currency', options: { useMultipliers: true }, } } + ariaLabel={ ariaLabel } /> ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx index cf002e81f523..c642ae9d156d 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx @@ -60,6 +60,7 @@ export function TotalReturnsWidget() { type: 'currency', options: { useMultipliers: true }, } } + ariaLabel={ __( 'Total returns', 'jetpack-premium-analytics-pkg' ) } /> ); diff --git a/projects/packages/premium-analytics/widgets/bookings-by-device/render.tsx b/projects/packages/premium-analytics/widgets/bookings-by-device/render.tsx index 9e1150880174..1e94fd85788a 100644 --- a/projects/packages/premium-analytics/widgets/bookings-by-device/render.tsx +++ b/projects/packages/premium-analytics/widgets/bookings-by-device/render.tsx @@ -33,6 +33,7 @@ function BookingsByDeviceWidget() { "We couldn't load booking data by device. Please try again in a moment.", 'jetpack-premium-analytics-pkg' ) } + ariaLabel={ __( 'Bookings by device', 'jetpack-premium-analytics-pkg' ) } /> ); } diff --git a/projects/packages/premium-analytics/widgets/sales-by-coupon-usage/render.tsx b/projects/packages/premium-analytics/widgets/sales-by-coupon-usage/render.tsx index 5e13ffb00f29..9e60b8017a4e 100644 --- a/projects/packages/premium-analytics/widgets/sales-by-coupon-usage/render.tsx +++ b/projects/packages/premium-analytics/widgets/sales-by-coupon-usage/render.tsx @@ -6,6 +6,7 @@ import { WidgetRoot, type ReportParamsFieldAttributes, } from '@jetpack-premium-analytics/widgets-toolkit'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ @@ -28,7 +29,9 @@ export default function SalesByCouponUsageRender( { }: SalesByCouponUsageWidgetProps ) { return ( - + ); } diff --git a/projects/packages/premium-analytics/widgets/sales-by-coupon/render.tsx b/projects/packages/premium-analytics/widgets/sales-by-coupon/render.tsx index ce7d666c7a08..9802c7830d66 100644 --- a/projects/packages/premium-analytics/widgets/sales-by-coupon/render.tsx +++ b/projects/packages/premium-analytics/widgets/sales-by-coupon/render.tsx @@ -6,6 +6,7 @@ import { WidgetRoot, type ReportParamsFieldAttributes, } from '@jetpack-premium-analytics/widgets-toolkit'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ @@ -28,7 +29,7 @@ export default function SalesByCouponRender( { }: SalesByCouponWidgetProps ) { return ( - + ); } From 94ae77bf4b098eefac7ff77200022c08c8ee0b12 Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:11:11 +1300 Subject: [PATCH 04/14] Changelog: Add entries for the Premium Analytics chart names Screen reader users on the package and both plugins that ship the dashboard need to see the fix in their changelogs. --- ...remium-analytics-name-each-widget-chart-for-screen-readers | 4 ++++ ...remium-analytics-name-each-widget-chart-for-screen-readers | 4 ++++ ...remium-analytics-name-each-widget-chart-for-screen-readers | 4 ++++ 3 files changed, 12 insertions(+) create mode 100644 projects/packages/premium-analytics/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers create mode 100644 projects/plugins/jetpack/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers create mode 100644 projects/plugins/premium-analytics/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers diff --git a/projects/packages/premium-analytics/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers b/projects/packages/premium-analytics/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers new file mode 100644 index 000000000000..3358582b6a2d --- /dev/null +++ b/projects/packages/premium-analytics/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Name each widget chart after its widget, so screen readers can tell the charts on a dashboard apart. diff --git a/projects/plugins/jetpack/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers b/projects/plugins/jetpack/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers new file mode 100644 index 000000000000..16db14ce3753 --- /dev/null +++ b/projects/plugins/jetpack/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers @@ -0,0 +1,4 @@ +Significance: patch +Type: bugfix + +Premium Analytics: Name each widget chart after its widget, so screen readers can tell the charts on a dashboard apart. diff --git a/projects/plugins/premium-analytics/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers b/projects/plugins/premium-analytics/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers new file mode 100644 index 000000000000..3358582b6a2d --- /dev/null +++ b/projects/plugins/premium-analytics/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Name each widget chart after its widget, so screen readers can tell the charts on a dashboard apart. From 9338aa460d2bd80c0aadbcb3fb41c75ce46384e7 Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:12:35 +1300 Subject: [PATCH 05/14] Changelog: Add the wpcomsh entry for the Premium Analytics chart names Atomic sites reach the Premium Analytics dashboard through wpcomsh, so its users need the entry too. --- ...remium-analytics-name-each-widget-chart-for-screen-readers | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/plugins/wpcomsh/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers diff --git a/projects/plugins/wpcomsh/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers b/projects/plugins/wpcomsh/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers new file mode 100644 index 000000000000..c72fda7353c2 --- /dev/null +++ b/projects/plugins/wpcomsh/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Premium Analytics: Name each widget chart after its widget, so screen readers can tell the charts on a dashboard apart. From 4ceb00e47bc55c81f031f6a0558ccc2e7b6bc11f Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:15:37 +1300 Subject: [PATCH 06/14] Address final review: Cover the tabbed chart name and document ariaLabel The tabbed branch passes ariaLabel separately and production widgets use it, so it needs its own coverage; the READMEs and a redundant default are tidied alongside. --- .../widgets-toolkit/src/components/chart-bar/README.md | 1 + .../src/components/chart-comparative-bar/README.md | 1 + .../src/components/chart-comparative-line/README.md | 1 + .../__tests__/metric-tabs-chart.test.tsx | 10 ++++++---- .../widgets/sales-by-coupon/sales-by-coupon-widget.tsx | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md index fa1a8bc72dd3..ec91cb07d41b 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md @@ -149,6 +149,7 @@ function MyWidget( { chartData } ) { | `dataFormat` | `DataFormat` | Yes | Format for values (tooltips): currency, number, percentage | | `styles` | `BarChartStyle[]` | No | Styles for each series (by index) | | `className` | `string` | No | CSS class for the chart container | +| `ariaLabel` | `string` | No | Accessible name of the chart; name it after what it shows, such as the widget title. | ## BarChartStyle Type diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/README.md index 3445bf267051..7e6f68111592 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/README.md @@ -40,6 +40,7 @@ second current-period series keeps its own dates. | `compactWhenShort` | `boolean` | No | Degrade to a sparkline under 140px of chart area | | `maxWidth` | `number` | No | Maximum chart width | | `className` | `string` | No | CSS class for the chart container | +| `ariaLabel` | `string` | No | Accessible name of the chart; name it after what it shows, such as the widget title. | | `chartId` | `string` | No | Identity the charts provider keys visibility on; generated when omitted. Change it whenever `defaultHiddenSeries` should be applied again | | `defaultHiddenSeries` | `readonly string[]` | No | Labels of series hidden until revealed from the legend. Applied once per `chartId`, so only useful with `legendInteractive` | | `legendInteractive` | `boolean` | No | Let the reader click legend items to show and hide series; the first item stays locked. Defaults to `false` | diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/README.md index 06de5b311ad2..aad6c428f17a 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/README.md @@ -131,6 +131,7 @@ function MyWidget( { series } ) { | `dataFormat` | `DataFormat` | Yes | Format for values (Y-axis ticks and tooltips) | | `tickFormat` | `DateFormatName` | No | Named X-axis date format; uses the chart default when omitted | | `className` | `string` | No | CSS class for the chart container | +| `ariaLabel` | `string` | No | Accessible name of the chart; name it after what it shows, such as the widget title. | | `chartId` | `string` | No | Identity the charts provider keys visibility on; generated when omitted. Change it whenever `defaultHiddenSeries` should be applied again | | `defaultHiddenSeries` | `readonly string[]` | No | Labels of series hidden until revealed from the legend. Applied once per `chartId`, so only useful with `legendInteractive` | | `legendInteractive` | `boolean` | No | Let the reader click legend items to show and hide series; the first item stays locked. Defaults to `false` | diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/__tests__/metric-tabs-chart.test.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/__tests__/metric-tabs-chart.test.tsx index b291ce40da7a..a7faeb15eb30 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/__tests__/metric-tabs-chart.test.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/__tests__/metric-tabs-chart.test.tsx @@ -181,12 +181,14 @@ describe( 'MetricTabsChart', () => { } ); it.each( [ - [ 'line', mockLineSpy ], - [ 'bar', mockBarSpy ], - ] as const )( 'names the %s chart with ariaLabel', ( type, spy ) => { + [ 'single-metric', 'line', mockLineSpy, [ METRIC ] ], + [ 'single-metric', 'bar', mockBarSpy, [ METRIC ] ], + [ 'tabbed', 'line', mockLineSpy, [ METRIC, VISITORS ] ], + [ 'tabbed', 'bar', mockBarSpy, [ METRIC, VISITORS ] ], + ] as const )( 'names the %s %s chart with ariaLabel', ( _layout, type, spy, metrics ) => { render( Date: Thu, 8 Oct 2026 12:20:18 +1300 Subject: [PATCH 07/14] Address final review: Cover the dropdown chart name The dropdown branch passes ariaLabel separately from the tabs branch, so it needs its own coverage, which the mocked resize observer can now reach by reporting a width. --- .../__tests__/metric-tabs-chart.test.tsx | 11 +++++++++-- .../tests/js/chart-test-utils.tsx | 17 ++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/__tests__/metric-tabs-chart.test.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/__tests__/metric-tabs-chart.test.tsx index a7faeb15eb30..874842d02253 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/__tests__/metric-tabs-chart.test.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/__tests__/metric-tabs-chart.test.tsx @@ -6,7 +6,7 @@ import { _n } from '@wordpress/i18n'; /** * Internal dependencies */ -import { resetMockCharts } from '../../../../../../tests/js/chart-test-utils'; +import { resetMockCharts, setMockChartWidth } from '../../../../../../tests/js/chart-test-utils'; import * as comparativeBarChart from '../../chart-comparative-bar/comparative-bar-chart'; import * as comparativeLineChart from '../../chart-comparative-line/comparative-line-chart'; import { MetricTabsChart } from '../metric-tabs-chart'; @@ -185,7 +185,13 @@ describe( 'MetricTabsChart', () => { [ 'single-metric', 'bar', mockBarSpy, [ METRIC ] ], [ 'tabbed', 'line', mockLineSpy, [ METRIC, VISITORS ] ], [ 'tabbed', 'bar', mockBarSpy, [ METRIC, VISITORS ] ], - ] as const )( 'names the %s %s chart with ariaLabel', ( _layout, type, spy, metrics ) => { + [ 'dropdown', 'line', mockLineSpy, [ METRIC, VISITORS ] ], + [ 'dropdown', 'bar', mockBarSpy, [ METRIC, VISITORS ] ], + ] as const )( 'names the %s %s chart with ariaLabel', ( layout, type, spy, metrics ) => { + if ( layout === 'dropdown' ) { + setMockChartWidth( 100 ); + } + render( { /> ); + expect( screen.queryByRole( 'tablist' ) !== null ).toBe( layout === 'tabbed' ); expect( spy ).toHaveBeenLastCalledWith( expect.objectContaining( { ariaLabel: 'Traffic summary' } ) ); diff --git a/projects/packages/premium-analytics/tests/js/chart-test-utils.tsx b/projects/packages/premium-analytics/tests/js/chart-test-utils.tsx index 3e01d168e7c6..7967ac75c790 100644 --- a/projects/packages/premium-analytics/tests/js/chart-test-utils.tsx +++ b/projects/packages/premium-analytics/tests/js/chart-test-utils.tsx @@ -29,6 +29,7 @@ export const mockSparklineMargin = { top: 2, right: 2, bottom: 2, left: 2 }; let elementStyles = defaultElementStyles; let hiddenSeries = new Set< string >(); let chartHeight: number | undefined; +let chartWidth: number | undefined; /** * Set the theme styles the charts context hands out. @@ -59,6 +60,15 @@ export function setMockChartHeight( height?: number ): void { chartHeight = height; } +/** + * Set the width the mocked resize observer reports; omit it to report none. + * + * @param width - Measured width in px. + */ +export function setMockChartWidth( width?: number ): void { + chartWidth = width; +} + /** Clear the chart spies and restore the default theme, visibility and size. */ export function resetMockCharts(): void { [ mockBarChartSpy, mockBarChartLegendSpy, mockLineChartSpy, mockLineChartLegendSpy ].forEach( @@ -67,6 +77,7 @@ export function resetMockCharts(): void { setMockElementStyles(); setMockHiddenSeries(); setMockChartHeight(); + setMockChartWidth(); } const BarChart = ( props: { children?: ReactNode } ) => { @@ -123,10 +134,10 @@ export const mockWordPressCompose = { // `@wordpress/data` needs the rest of the real module. ...jest.requireActual( '@wordpress/compose' ), useResizeObserver: - ( onResize: ( entries: { contentRect: { height: number } }[] ) => void ) => + ( onResize: ( entries: { contentRect: { height?: number; width?: number } }[] ) => void ) => ( element: HTMLElement | null ) => { - if ( element && chartHeight !== undefined ) { - onResize( [ { contentRect: { height: chartHeight } } ] ); + if ( element && ( chartHeight !== undefined || chartWidth !== undefined ) ) { + onResize( [ { contentRect: { height: chartHeight, width: chartWidth } } ] ); } }, }; From 0a715d9aca9d28654d1ca745b81406d067b8ecf9 Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:54:48 +1300 Subject: [PATCH 08/14] Name each widget chart " chart" The chart's role is application, so a screen reader never says it is a chart, and a bare widget title repeats the name of the section around it. The wrappers now take chartTitle and build the name through one translatable string, so each locale can place the word for chart where its grammar needs it. --- .../src/components/chart-bar/README.md | 2 +- .../chart-bar/__tests__/bar-chart.test.tsx | 11 +++++----- .../src/components/chart-bar/bar-chart.tsx | 10 ++++----- .../chart-comparative-bar/README.md | 2 +- .../__tests__/comparative-bar-chart.test.tsx | 6 ++--- .../comparative-bar-chart.tsx | 15 ++++++++----- .../chart-comparative-line/README.md | 2 +- .../__tests__/comparative-line-chart.test.tsx | 6 ++--- .../comparative-line-chart.tsx | 9 ++++++-- .../__tests__/metric-tabs-chart.test.tsx | 6 ++--- .../metric-tabs-chart/metric-tabs-chart.tsx | 20 ++++++++--------- .../src/helpers/format-chart-name.ts | 22 +++++++++++++++++++ .../widgets-toolkit/src/helpers/index.ts | 1 + .../revenue-by-customer-type-widget.tsx | 10 ++++----- .../sales-by-coupon-widget.tsx | 4 ++-- .../sales-by-device-widget.tsx | 6 ++--- .../total-returns/total-returns-widget.tsx | 2 +- .../widgets/author-performance/render.tsx | 2 +- .../widgets/bookings-by-device/render.tsx | 2 +- .../widgets/email-time-series/render.tsx | 2 +- .../widgets/post-views/render.tsx | 2 +- .../widgets/sales-by-coupon-usage/render.tsx | 2 +- .../widgets/sales-by-coupon/render.tsx | 4 +++- .../widgets/subscribers-chart/render.tsx | 2 +- .../widgets/traffic-chart/render.tsx | 2 +- .../video-detail-views-performance/render.tsx | 2 +- .../widgets/store-performance/render.tsx | 2 +- 27 files changed, 96 insertions(+), 60 deletions(-) create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-chart-name.ts diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md index ec91cb07d41b..ee7e32fd4840 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md @@ -149,7 +149,7 @@ function MyWidget( { chartData } ) { | `dataFormat` | `DataFormat` | Yes | Format for values (tooltips): currency, number, percentage | | `styles` | `BarChartStyle[]` | No | Styles for each series (by index) | | `className` | `string` | No | CSS class for the chart container | -| `ariaLabel` | `string` | No | Accessible name of the chart; name it after what it shows, such as the widget title. | +| `chartTitle` | `string` | No | What the chart shows, such as the widget title; the chart is named "<title> chart" | ## BarChartStyle Type diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/__tests__/bar-chart.test.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/__tests__/bar-chart.test.tsx index 9e9f2b6e5da5..f3d96a072489 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/__tests__/bar-chart.test.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/__tests__/bar-chart.test.tsx @@ -22,17 +22,18 @@ describe( 'BarChart', () => { resetMockCharts(); } ); - it( 'names the chart with ariaLabel', () => { + it.each( [ + [ 'Sales by device', 'Sales by device chart' ], + [ undefined, undefined ], + ] )( 'names the chart from chartTitle %s', ( chartTitle, ariaLabel ) => { render( <BarChart chartData={ [ { label: 'Sales', data: [ { label: 'Mobile', value: 3 } ] } ] } dataFormat={ { type: 'number' } } - ariaLabel="Sales by device" + chartTitle={ chartTitle } /> ); - expect( mockBarChartSpy ).toHaveBeenLastCalledWith( - expect.objectContaining( { ariaLabel: 'Sales by device' } ) - ); + expect( mockBarChartSpy ).toHaveBeenLastCalledWith( expect.objectContaining( { ariaLabel } ) ); } ); } ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx index 5047dc84a2a8..b2af1e0f43c7 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx @@ -8,7 +8,7 @@ import { useCallback, useMemo, useId } from 'react'; * Internal dependencies */ import { RESIZE_DEBOUNCE_MS } from '../../constants'; -import { isEmptyChartData, getEmptyChartDomain } from '../../helpers'; +import { isEmptyChartData, formatChartName, getEmptyChartDomain } from '../../helpers'; import { ChartEmptyState } from '../chart-empty-state'; import { ChartTooltip } from '../chart-tooltip'; import styles from './bar-chart.module.scss'; @@ -48,8 +48,8 @@ export type BarChartProps = { className?: string; - /** Accessible name of the chart; name it after what it shows, such as the widget title. */ - ariaLabel?: string; + /** What the chart shows, such as the widget title; the chart is named "<title> chart". */ + chartTitle?: string; emptyStateIcon?: React.ComponentProps< typeof Icon >[ 'icon' ]; @@ -119,7 +119,7 @@ export function BarChart( { dataFormat, styles: stylesProp, className, - ariaLabel, + chartTitle, emptyStateIcon, emptyStateText, showZeroValues = true, @@ -196,7 +196,7 @@ export function BarChart( { return ( <BarChartBase chartId={ chartId } - ariaLabel={ ariaLabel } + ariaLabel={ formatChartName( chartTitle ) } data={ styledChartData } className={ clsx( styles.chart, className ) } resizeDebounceTime={ RESIZE_DEBOUNCE_MS } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/README.md index 7e6f68111592..94df738d5489 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/README.md @@ -40,7 +40,7 @@ second current-period series keeps its own dates. | `compactWhenShort` | `boolean` | No | Degrade to a sparkline under 140px of chart area | | `maxWidth` | `number` | No | Maximum chart width | | `className` | `string` | No | CSS class for the chart container | -| `ariaLabel` | `string` | No | Accessible name of the chart; name it after what it shows, such as the widget title. | +| `chartTitle` | `string` | No | What the chart shows, such as the widget title; the chart is named "<title> chart" | | `chartId` | `string` | No | Identity the charts provider keys visibility on; generated when omitted. Change it whenever `defaultHiddenSeries` should be applied again | | `defaultHiddenSeries` | `readonly string[]` | No | Labels of series hidden until revealed from the legend. Applied once per `chartId`, so only useful with `legendInteractive` | | `legendInteractive` | `boolean` | No | Let the reader click legend items to show and hide series; the first item stays locked. Defaults to `false` | diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/__tests__/comparative-bar-chart.test.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/__tests__/comparative-bar-chart.test.tsx index 3bbeff4882a4..f4ec21bc20a5 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/__tests__/comparative-bar-chart.test.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/__tests__/comparative-bar-chart.test.tsx @@ -186,17 +186,17 @@ describe( 'ComparativeBarChart', () => { setSettings( originalSettings ); } ); - it( 'names the chart with ariaLabel', () => { + it( 'names the chart from chartTitle', () => { render( <ComparativeBarChart series={ SERIES } dataFormat={ DATA_FORMAT } - ariaLabel="Traffic summary" + chartTitle="Traffic summary" /> ); expect( mockBarChartSpy ).toHaveBeenLastCalledWith( - expect.objectContaining( { ariaLabel: 'Traffic summary' } ) + expect.objectContaining( { ariaLabel: 'Traffic summary chart' } ) ); } ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/comparative-bar-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/comparative-bar-chart.tsx index c9d1f1eeb7d0..d6af237b83df 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/comparative-bar-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-bar/comparative-bar-chart.tsx @@ -20,7 +20,12 @@ import { useCallback, useId, useMemo, useState } from 'react'; * Internal dependencies */ import { RESIZE_DEBOUNCE_MS } from '../../constants'; -import { isEmptyChartData, getFixedYAxis, formatBucketTooltipDate } from '../../helpers'; +import { + isEmptyChartData, + formatChartName, + getFixedYAxis, + formatBucketTooltipDate, +} from '../../helpers'; import { resolvePrimarySeriesByGroup } from '../../helpers/resolve-series-names'; import { useLockedPrimaryLegendItems } from '../../hooks/use-locked-primary-legend-items'; import { alignSeriesDates } from '../chart-comparative-line/utils'; @@ -55,8 +60,8 @@ export type ComparativeBarChartProps = { className?: string; - /** Accessible name of the chart; name it after what it shows, such as the widget title. */ - ariaLabel?: string; + /** What the chart shows, such as the widget title; the chart is named "<title> chart". */ + chartTitle?: string; /** Format for chart values: y-axis ticks and tooltips. */ dataFormat: DataFormat; @@ -127,7 +132,7 @@ export function ComparativeBarChart( { series, className, chartId: providedChartId, - ariaLabel, + chartTitle, dataFormat, tickFormat: xTickFormatType, tickResolution, @@ -321,7 +326,7 @@ export function ComparativeBarChart( { <Stack ref={ measureRef } direction="column" className={ clsx( styles.chart, className ) }> <BarChart chartId={ chartId } - ariaLabel={ ariaLabel } + ariaLabel={ formatChartName( chartTitle ) } className={ styles.chartContent } data={ alignedSeries } options={ chartOptions } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/README.md index aad6c428f17a..f16dd011e112 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/README.md @@ -131,7 +131,7 @@ function MyWidget( { series } ) { | `dataFormat` | `DataFormat` | Yes | Format for values (Y-axis ticks and tooltips) | | `tickFormat` | `DateFormatName` | No | Named X-axis date format; uses the chart default when omitted | | `className` | `string` | No | CSS class for the chart container | -| `ariaLabel` | `string` | No | Accessible name of the chart; name it after what it shows, such as the widget title. | +| `chartTitle` | `string` | No | What the chart shows, such as the widget title; the chart is named "<title> chart" | | `chartId` | `string` | No | Identity the charts provider keys visibility on; generated when omitted. Change it whenever `defaultHiddenSeries` should be applied again | | `defaultHiddenSeries` | `readonly string[]` | No | Labels of series hidden until revealed from the legend. Applied once per `chartId`, so only useful with `legendInteractive` | | `legendInteractive` | `boolean` | No | Let the reader click legend items to show and hide series; the first item stays locked. Defaults to `false` | diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/__tests__/comparative-line-chart.test.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/__tests__/comparative-line-chart.test.tsx index 889b030016dc..aba9661fe6f7 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/__tests__/comparative-line-chart.test.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/__tests__/comparative-line-chart.test.tsx @@ -174,17 +174,17 @@ describe( 'ComparativeLineChart', () => { setSettings( originalSettings ); } ); - it( 'names the chart with ariaLabel', () => { + it( 'names the chart from chartTitle', () => { render( <ComparativeLineChart series={ SERIES } dataFormat={ DATA_FORMAT } - ariaLabel="Traffic summary" + chartTitle="Traffic summary" /> ); expect( mockLineChartSpy ).toHaveBeenLastCalledWith( - expect.objectContaining( { ariaLabel: 'Traffic summary' } ) + expect.objectContaining( { ariaLabel: 'Traffic summary chart' } ) ); } ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx index 6e6f55dd69b0..49fb6488d066 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx @@ -19,6 +19,7 @@ import { type ComponentProps } from 'react'; import { RESIZE_DEBOUNCE_MS } from '../../constants'; import { isEmptyChartData, + formatChartName, getFixedYAxis, getPaddedYAxis, getPinnedYTicks, @@ -102,6 +103,9 @@ export type ComparativeLineChartProps = { className?: string; + /** What the chart shows, such as the widget title; the chart is named "<title> chart". */ + chartTitle?: string; + dataFormat: DataFormat; /** Named date format for the X-axis ticks. Uses the chart default when omitted. */ @@ -151,6 +155,7 @@ export type ComparativeLineChartProps = { | 'resizeDebounceTime' | 'withTooltips' | 'renderTooltip' + | 'ariaLabel' >; export function ComparativeLineChart( { @@ -158,7 +163,7 @@ export function ComparativeLineChart( { styles: stylesProp, className, chartId, - ariaLabel, + chartTitle, dataFormat, tickFormat: xTickFormatType, tickResolution, @@ -306,7 +311,7 @@ export function ComparativeLineChart( { <Stack ref={ measureRef } direction="column" className={ clsx( styles.chart, className ) }> <LineChart chartId={ resolvedChartId } - ariaLabel={ ariaLabel } + ariaLabel={ formatChartName( chartTitle ) } className={ styles.chartContent } data={ styledSeries } options={ chartOptions } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/__tests__/metric-tabs-chart.test.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/__tests__/metric-tabs-chart.test.tsx index 874842d02253..69e15230e05c 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/__tests__/metric-tabs-chart.test.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/__tests__/metric-tabs-chart.test.tsx @@ -187,7 +187,7 @@ describe( 'MetricTabsChart', () => { [ 'tabbed', 'bar', mockBarSpy, [ METRIC, VISITORS ] ], [ 'dropdown', 'line', mockLineSpy, [ METRIC, VISITORS ] ], [ 'dropdown', 'bar', mockBarSpy, [ METRIC, VISITORS ] ], - ] as const )( 'names the %s %s chart with ariaLabel', ( layout, type, spy, metrics ) => { + ] as const )( 'passes chartTitle to the %s %s chart', ( layout, type, spy, metrics ) => { if ( layout === 'dropdown' ) { setMockChartWidth( 100 ); } @@ -197,13 +197,13 @@ describe( 'MetricTabsChart', () => { metrics={ [ ...metrics ] } dataFormat={ DATA_FORMAT } chartType={ type } - ariaLabel="Traffic summary" + chartTitle="Traffic summary" /> ); expect( screen.queryByRole( 'tablist' ) !== null ).toBe( layout === 'tabbed' ); expect( spy ).toHaveBeenLastCalledWith( - expect.objectContaining( { ariaLabel: 'Traffic summary' } ) + expect.objectContaining( { chartTitle: 'Traffic summary' } ) ); } ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/metric-tabs-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/metric-tabs-chart.tsx index 4d1931f207a2..47816a7782ce 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/metric-tabs-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-tabs-chart/metric-tabs-chart.tsx @@ -140,8 +140,8 @@ export interface MetricTabsChartProps { controls?: ReactNode; /** Accessible label for the metric tab list. */ groupLabel?: string; - /** Accessible name of the chart; name it after what it shows, such as the widget title. */ - ariaLabel?: string; + /** What the chart shows, such as the widget title; the chart is named "<title> chart". */ + chartTitle?: string; /** * The series' bucket size, declared to the x-axis so tick formats follow the * known granularity rather than being inferred from point spacing. @@ -224,7 +224,7 @@ function MetricChart( { dataFormat, chartType, chartId, - ariaLabel, + chartTitle, tickResolution, onDatumClick, baseline, @@ -238,7 +238,7 @@ function MetricChart( { chartType: MetricTabsChartType; baseline?: ChartBaseline; chartId: string; - ariaLabel?: string; + chartTitle?: string; tickResolution?: TickResolution; onDatumClick?: ( date: Date ) => void; empty?: ReactNode; @@ -368,7 +368,7 @@ function MetricChart( { return chartType === 'bar' ? ( <ComparativeBarChart chartId={ chartId } - ariaLabel={ ariaLabel } + chartTitle={ chartTitle } series={ series } dataFormat={ resolvedDataFormat } defaultHiddenSeries={ defaultHiddenSeries } @@ -381,7 +381,7 @@ function MetricChart( { ) : ( <ComparativeLineChart chartId={ chartId } - ariaLabel={ ariaLabel } + chartTitle={ chartTitle } series={ series } styles={ seriesStyles } dataFormat={ resolvedDataFormat } @@ -468,7 +468,7 @@ export function MetricTabsChart( { onMetricChange, controls, groupLabel = __( 'Select metric', 'jetpack-premium-analytics-pkg' ), - ariaLabel, + chartTitle, tickResolution, onDatumClick, tooltipMetrics = 'active', @@ -568,7 +568,7 @@ export function MetricTabsChart( { dataFormat={ dataFormat } chartType={ chartType } chartId={ chartIdFor( activeMetric ) } - ariaLabel={ ariaLabel } + chartTitle={ chartTitle } tickResolution={ tickResolution } onDatumClick={ onDatumClick } empty={ empty } @@ -642,7 +642,7 @@ export function MetricTabsChart( { dataFormat={ dataFormat } chartType={ chartType } chartId={ chartIdFor( activeMetric ) } - ariaLabel={ ariaLabel } + chartTitle={ chartTitle } tickResolution={ tickResolution } onDatumClick={ onDatumClick } empty={ empty } @@ -692,7 +692,7 @@ export function MetricTabsChart( { dataFormat={ dataFormat } chartType={ chartType } chartId={ chartIdFor( metric ) } - ariaLabel={ ariaLabel } + chartTitle={ chartTitle } tickResolution={ tickResolution } onDatumClick={ onDatumClick } empty={ empty } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-chart-name.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-chart-name.ts new file mode 100644 index 000000000000..b346ec72cd77 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-chart-name.ts @@ -0,0 +1,22 @@ +/** + * External dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Name a chart after what it shows, so a screen reader can tell it from the widget around it. + * + * @param title - What the chart shows, such as the widget title. + * @return The chart's accessible name, or undefined to keep the chart's default. + */ +export function formatChartName( title?: string ): string | undefined { + if ( ! title ) { + return undefined; + } + + return sprintf( + /* translators: %s is what a chart shows, e.g. "Sales by device". */ + __( '%s chart', 'jetpack-premium-analytics-pkg' ), + title + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/index.ts index fb2e1715c5b4..31d74fe95e9f 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/index.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/index.ts @@ -80,5 +80,6 @@ export { export { monthlyHeatmapLifeStart } from './monthly-heatmap-life-start'; export { bucketRange, monthRange, yearRange, type PeriodBounds } from './period-range'; export { siteChartFormatting } from './site-chart-formatting'; +export { formatChartName } from './format-chart-name'; export { formatComparisonSeriesLabel } from './format-comparison-series-label'; export { formatBucketTooltipDate } from './format-bucket-tooltip-date'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx index c1db4c8f5c07..42b9a4655490 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx @@ -21,7 +21,7 @@ type CustomerTypeRevenueWidgetProps = { * @see BOOKINGS_FILTER for booking products (booking, bookable-event, bookable-service) */ filter?: FilterCondition; - ariaLabel: string; + chartTitle: string; }; /** @@ -30,7 +30,7 @@ type CustomerTypeRevenueWidgetProps = { * * Must be used within a WidgetRoot which provides reportParams via context. */ -function CustomerTypeRevenueWidget( { filter, ariaLabel }: CustomerTypeRevenueWidgetProps ) { +function CustomerTypeRevenueWidget( { filter, chartTitle }: CustomerTypeRevenueWidgetProps ) { const { reportParams } = useWidgetRootContext(); const { primary, comparison, isLoading, isFetching, hasData, isError, refetch } = @@ -74,7 +74,7 @@ function CustomerTypeRevenueWidget( { filter, ariaLabel }: CustomerTypeRevenueWi type: 'currency', options: { useMultipliers: true }, } } - ariaLabel={ ariaLabel } + chartTitle={ chartTitle } /> </WidgetState> ); @@ -87,7 +87,7 @@ function CustomerTypeRevenueWidget( { filter, ariaLabel }: CustomerTypeRevenueWi export function RevenueByCustomerTypeWidget() { return ( <CustomerTypeRevenueWidget - ariaLabel={ __( 'Revenue by customer type', 'jetpack-premium-analytics-pkg' ) } + chartTitle={ __( 'Revenue by customer type', 'jetpack-premium-analytics-pkg' ) } /> ); } @@ -99,7 +99,7 @@ export function RevenueByCustomerTypeWidget() { export function BookingsRevenueByCustomerTypeWidget() { return ( <CustomerTypeRevenueWidget - ariaLabel={ __( 'Bookings revenue by customer type', 'jetpack-premium-analytics-pkg' ) } + chartTitle={ __( 'Bookings revenue by customer type', 'jetpack-premium-analytics-pkg' ) } filter={ BOOKINGS_FILTER } /> ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx index c7bea49cc079..afa4151682c4 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx @@ -22,7 +22,7 @@ const TOP_COUPON_SEGMENTS = 3; * * Must be used within a WidgetRoot which provides reportParams via context. */ -export function SalesByCouponWidget( { ariaLabel }: { ariaLabel?: string } ) { +export function SalesByCouponWidget( { chartTitle }: { chartTitle?: string } ) { const { reportParams } = useWidgetRootContext(); const { primary, comparison, isLoading, isFetching, hasData, isError, refetch } = @@ -64,7 +64,7 @@ export function SalesByCouponWidget( { ariaLabel }: { ariaLabel?: string } ) { type: 'currency', options: { useMultipliers: true }, } } - ariaLabel={ ariaLabel } + chartTitle={ chartTitle } /> </WidgetState> ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx index 74abc0901cce..d44fa4dbf200 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx @@ -26,7 +26,7 @@ type SalesByDeviceWidgetProps = { errorText?: string; - ariaLabel?: string; + chartTitle?: string; }; /** @@ -38,7 +38,7 @@ export function SalesByDeviceWidget( { filter, emptyStateText, errorText, - ariaLabel = __( 'Sales by device', 'jetpack-premium-analytics-pkg' ), + chartTitle = __( 'Sales by device', 'jetpack-premium-analytics-pkg' ), }: SalesByDeviceWidgetProps ) { const { reportParams } = useWidgetRootContext(); @@ -92,7 +92,7 @@ export function SalesByDeviceWidget( { type: 'currency', options: { useMultipliers: true }, } } - ariaLabel={ ariaLabel } + chartTitle={ chartTitle } /> </WidgetState> ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx index c642ae9d156d..bc82fe836bf2 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx @@ -60,7 +60,7 @@ export function TotalReturnsWidget() { type: 'currency', options: { useMultipliers: true }, } } - ariaLabel={ __( 'Total returns', 'jetpack-premium-analytics-pkg' ) } + chartTitle={ __( 'Total returns', 'jetpack-premium-analytics-pkg' ) } /> </WidgetState> ); diff --git a/projects/packages/premium-analytics/widgets/author-performance/render.tsx b/projects/packages/premium-analytics/widgets/author-performance/render.tsx index a97d077ac756..f50358aa3514 100644 --- a/projects/packages/premium-analytics/widgets/author-performance/render.tsx +++ b/projects/packages/premium-analytics/widgets/author-performance/render.tsx @@ -135,7 +135,7 @@ function AuthorPerformanceInner( { chartType }: AuthorPerformanceInnerProps ) { metrics={ metricTabs } dataFormat={ DATA_FORMAT } chartType={ chartType } - ariaLabel={ __( 'Author performance', 'jetpack-premium-analytics-pkg' ) } + chartTitle={ __( 'Author performance', 'jetpack-premium-analytics-pkg' ) } empty={ <ChartEmptyState /> } /> </WidgetState> diff --git a/projects/packages/premium-analytics/widgets/bookings-by-device/render.tsx b/projects/packages/premium-analytics/widgets/bookings-by-device/render.tsx index 1e94fd85788a..c774a797bfbb 100644 --- a/projects/packages/premium-analytics/widgets/bookings-by-device/render.tsx +++ b/projects/packages/premium-analytics/widgets/bookings-by-device/render.tsx @@ -33,7 +33,7 @@ function BookingsByDeviceWidget() { "We couldn't load booking data by device. Please try again in a moment.", 'jetpack-premium-analytics-pkg' ) } - ariaLabel={ __( 'Bookings by device', 'jetpack-premium-analytics-pkg' ) } + chartTitle={ __( 'Bookings by device', 'jetpack-premium-analytics-pkg' ) } /> ); } diff --git a/projects/packages/premium-analytics/widgets/email-time-series/render.tsx b/projects/packages/premium-analytics/widgets/email-time-series/render.tsx index 24f68ffea4e1..8237117a55aa 100644 --- a/projects/packages/premium-analytics/widgets/email-time-series/render.tsx +++ b/projects/packages/premium-analytics/widgets/email-time-series/render.tsx @@ -167,7 +167,7 @@ function EmailTimeSeriesReport( { metric, chartType }: EmailTimeSeriesReportProp metrics={ metricTabs } dataFormat={ DATA_FORMAT } chartType={ chartType } - ariaLabel={ __( 'Email performance', 'jetpack-premium-analytics-pkg' ) } + chartTitle={ __( 'Email performance', 'jetpack-premium-analytics-pkg' ) } empty={ <ChartEmptyState /> } /> </WidgetState> diff --git a/projects/packages/premium-analytics/widgets/post-views/render.tsx b/projects/packages/premium-analytics/widgets/post-views/render.tsx index 691fdb71468f..4ea7c397c1fc 100644 --- a/projects/packages/premium-analytics/widgets/post-views/render.tsx +++ b/projects/packages/premium-analytics/widgets/post-views/render.tsx @@ -98,7 +98,7 @@ function PostViewsInner( { chartType }: PostViewsInnerProps ) { metrics={ metricTabs } dataFormat={ DATA_FORMAT } chartType={ chartType } - ariaLabel={ __( 'Post views', 'jetpack-premium-analytics-pkg' ) } + chartTitle={ __( 'Post views', 'jetpack-premium-analytics-pkg' ) } empty={ <ChartEmptyState /> } /> </WidgetState> diff --git a/projects/packages/premium-analytics/widgets/sales-by-coupon-usage/render.tsx b/projects/packages/premium-analytics/widgets/sales-by-coupon-usage/render.tsx index 9e60b8017a4e..39c3c80b95aa 100644 --- a/projects/packages/premium-analytics/widgets/sales-by-coupon-usage/render.tsx +++ b/projects/packages/premium-analytics/widgets/sales-by-coupon-usage/render.tsx @@ -30,7 +30,7 @@ export default function SalesByCouponUsageRender( { return ( <WidgetRoot attributes={ attributes } setError={ setError } options={ { from: '/' } }> <SalesByCouponWidget - ariaLabel={ __( 'Sales by coupon usage', 'jetpack-premium-analytics-pkg' ) } + chartTitle={ __( 'Sales by coupon usage', 'jetpack-premium-analytics-pkg' ) } /> </WidgetRoot> ); diff --git a/projects/packages/premium-analytics/widgets/sales-by-coupon/render.tsx b/projects/packages/premium-analytics/widgets/sales-by-coupon/render.tsx index 9802c7830d66..0b55bb6949a8 100644 --- a/projects/packages/premium-analytics/widgets/sales-by-coupon/render.tsx +++ b/projects/packages/premium-analytics/widgets/sales-by-coupon/render.tsx @@ -29,7 +29,9 @@ export default function SalesByCouponRender( { }: SalesByCouponWidgetProps ) { return ( <WidgetRoot attributes={ attributes } setError={ setError } options={ { from: '/' } }> - <SalesByCouponWidget ariaLabel={ __( 'Sales by coupon', 'jetpack-premium-analytics-pkg' ) } /> + <SalesByCouponWidget + chartTitle={ __( 'Sales by coupon', 'jetpack-premium-analytics-pkg' ) } + /> </WidgetRoot> ); } diff --git a/projects/packages/premium-analytics/widgets/subscribers-chart/render.tsx b/projects/packages/premium-analytics/widgets/subscribers-chart/render.tsx index 2e37fdd300cb..700ef6ec1553 100644 --- a/projects/packages/premium-analytics/widgets/subscribers-chart/render.tsx +++ b/projects/packages/premium-analytics/widgets/subscribers-chart/render.tsx @@ -135,7 +135,7 @@ function SubscribersChartInner( { chartType }: SubscribersChartInnerProps ) { chartType={ chartType } groupLabel={ groupLabel } baseline="padded" - ariaLabel={ __( 'Subscriber summary', 'jetpack-premium-analytics-pkg' ) } + chartTitle={ __( 'Subscriber summary', 'jetpack-premium-analytics-pkg' ) } empty={ <ChartEmptyState /> } /> </WidgetState> diff --git a/projects/packages/premium-analytics/widgets/traffic-chart/render.tsx b/projects/packages/premium-analytics/widgets/traffic-chart/render.tsx index 8f2cb852709f..1c27cd210bed 100644 --- a/projects/packages/premium-analytics/widgets/traffic-chart/render.tsx +++ b/projects/packages/premium-analytics/widgets/traffic-chart/render.tsx @@ -118,7 +118,7 @@ function TrafficChartInner( { chartType, interval }: TrafficChartInnerProps ) { groupLabel={ groupLabel } tickResolution={ period } onDatumClick={ openBucket } - ariaLabel={ __( 'Traffic summary', 'jetpack-premium-analytics-pkg' ) } + chartTitle={ __( 'Traffic summary', 'jetpack-premium-analytics-pkg' ) } empty={ <ChartEmptyState /> } /> </WidgetState> diff --git a/projects/packages/premium-analytics/widgets/video-detail-views-performance/render.tsx b/projects/packages/premium-analytics/widgets/video-detail-views-performance/render.tsx index 5cda8707a2c3..70da4aacd5ff 100644 --- a/projects/packages/premium-analytics/widgets/video-detail-views-performance/render.tsx +++ b/projects/packages/premium-analytics/widgets/video-detail-views-performance/render.tsx @@ -82,7 +82,7 @@ function VideoDetailViewsPerformanceInner( { chartType }: VideoDetailViewsPerfor dataFormat={ COUNT_FORMAT } chartType={ chartType } groupLabel={ groupLabel } - ariaLabel={ __( 'Video performance', 'jetpack-premium-analytics-pkg' ) } + chartTitle={ __( 'Video performance', 'jetpack-premium-analytics-pkg' ) } empty={ <ChartEmptyState /> } /> </WidgetState> diff --git a/projects/packages/woocommerce-stats/widgets/store-performance/render.tsx b/projects/packages/woocommerce-stats/widgets/store-performance/render.tsx index d2352ce9ef08..fe4d58b5f91d 100644 --- a/projects/packages/woocommerce-stats/widgets/store-performance/render.tsx +++ b/projects/packages/woocommerce-stats/widgets/store-performance/render.tsx @@ -112,7 +112,7 @@ function StorePerformance() { metrics={ metrics } dataFormat={ DEFAULT_DATA_FORMAT } groupLabel={ __( 'Store metric', 'jetpack-woocommerce-stats-pkg' ) } - ariaLabel={ __( 'Store performance', 'jetpack-woocommerce-stats-pkg' ) } + chartTitle={ __( 'Store performance', 'jetpack-woocommerce-stats-pkg' ) } empty={ <ChartEmptyState /> } /> </WidgetState> From 534e9ea7f56448340fa755360f88a521ebb86b55 Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:42:21 +1300 Subject: [PATCH 09/14] Changelog: Add the WooCommerce Stats entry for the Store performance chart name The Store performance widget moved into the woocommerce-stats package on trunk, so its chart name now ships from there. --- ...remium-analytics-name-each-widget-chart-for-screen-readers | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/woocommerce-stats/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers diff --git a/projects/packages/woocommerce-stats/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers b/projects/packages/woocommerce-stats/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers new file mode 100644 index 000000000000..5aba173c9555 --- /dev/null +++ b/projects/packages/woocommerce-stats/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Name the Store performance chart after its widget, so screen readers can tell the charts on a dashboard apart. From c601c1e9d278fc6132aa538b7e08ccd6d14b1858 Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:04:45 +1300 Subject: [PATCH 10/14] Premium Analytics: Let the pie chart wrappers take a chart title DonutChart, SemiCircleChart and Donut now name their chart "<title> chart", as the line and bar wrappers do, so the breakdown widgets can stop being announced as "Pie chart" or "Semi-circle chart". --- .../src/components/chart-donut/README.md | 21 +++++------ .../components/chart-donut/donut-chart.tsx | 6 ++++ .../components/chart-semi-circle/README.md | 35 ++++++++++--------- .../__tests__/semi-circle-chart.test.tsx | 33 +++++++++++++++++ .../chart-semi-circle/semi-circle-chart.tsx | 6 ++++ .../components/donut/__tests__/donut.test.tsx | 10 ++++++ .../src/components/donut/donut.tsx | 4 +++ 7 files changed, 88 insertions(+), 27 deletions(-) create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/__tests__/semi-circle-chart.test.tsx diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/README.md index 69dc8f5da7c5..e41e80f02ec9 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/README.md @@ -39,16 +39,17 @@ const styles = [ { color: '#3858E9' }, { color: '#66BDFF' }, { color: '#A77EFF' ## Props -| Prop | Type | Default | Description | -| ----------------- | ---------------- | -------------------- | ------------------------------------------------------------------ | -| `chartData` | `DonutChartData` | required | Array of segments with `label`, `value`, and `percentage` | -| `styles` | `SegmentStyle[]` | - | Explicit colors per segment (takes priority over chartData colors) | -| `value` | `number` | required | Primary metric value displayed in center | -| `comparisonValue` | `number \| null` | - | Previous period value for delta calculation | -| `dataFormat` | `DataFormat` | `{ type: 'number' }` | Format configuration for the metric display | -| `legendData` | `LegendItem[]` | - | Legend items with labels and comparison values | -| `showLegend` | `boolean` | `true` | Whether to show the legend below the chart | -| `thickness` | `number` | `0.3` | Arc thickness as ratio (0-1) | +| Prop | Type | Default | Description | +| ----------------- | ---------------- | -------------------- | ---------------------------------------------------------------------------------- | +| `chartData` | `DonutChartData` | required | Array of segments with `label`, `value`, and `percentage` | +| `styles` | `SegmentStyle[]` | - | Explicit colors per segment (takes priority over chartData colors) | +| `value` | `number` | required | Primary metric value displayed in center | +| `comparisonValue` | `number \| null` | - | Previous period value for delta calculation | +| `dataFormat` | `DataFormat` | `{ type: 'number' }` | Format configuration for the metric display | +| `legendData` | `LegendItem[]` | - | Legend items with labels and comparison values | +| `showLegend` | `boolean` | `true` | Whether to show the legend below the chart | +| `chartTitle` | `string` | - | What the chart shows, such as the widget title; the chart is named "<title> chart" | +| `thickness` | `number` | `0.3` | Arc thickness as ratio (0-1) | ## Data Validation diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.tsx index b930bbfd6bed..5b6aab17768a 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.tsx @@ -13,6 +13,7 @@ import { useMemo } from 'react'; import { resolveSegmentStyles, applyStylesToItems, + formatChartName, isEmptyPieChartData, type SegmentStyle, } from '../../helpers'; @@ -71,6 +72,9 @@ export type DonutChartProps = { showLegend?: boolean; + /** What the chart shows, such as the widget title; the chart is named "<title> chart". */ + chartTitle?: string; + /** * Thickness of the arc (0-1). * @default 0.3 @@ -121,6 +125,7 @@ export function DonutChart( { }, legendData, showLegend = true, + chartTitle, thickness = DEFAULT_THICKNESS, maxSize = MAX_SIZE, emptyStateIcon, @@ -195,6 +200,7 @@ export function DonutChart( { <PieChart data={ styledChartData } className={ styles.pieChart } + ariaLabel={ formatChartName( chartTitle ) } width={ chartSize } height={ chartSize } thickness={ thickness } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/README.md index b044f4399632..cac9b7e24c91 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/README.md @@ -38,23 +38,24 @@ const styles = [ { color: '#3858E9' }, { color: '#66BDFF' }, { color: '#A77EFF' ## Props -| Prop | Type | Default | Description | -| ------------------- | --------------------- | -------------------- | -------------------------------------------------------------------------- | -| `chartData` | `SemiCircleChartData` | required | Array of segments with `label` and `value` (percentage is auto-calculated) | -| `styles` | `SegmentStyle[]` | - | Explicit colors per segment (takes priority over chartData colors) | -| `value` | `number` | required | Primary metric value displayed in center | -| `comparisonValue` | `number \| null` | - | Previous period value for delta calculation | -| `dataFormat` | `DataFormat` | `{ type: 'number' }` | Format configuration for the metric display | -| `legendData` | `LegendItem[]` | - | Legend items with labels and comparison values | -| `showLegend` | `boolean` | `true` | Whether to show the legend below the chart | -| `thickness` | `number` | `0.3` | Arc thickness as ratio (0-1) | -| `maxWidth` | `number` | `Infinity` | Maximum width constraint for the chart | -| `withTooltips` | `boolean` | `false` | Enable tooltips on hover | -| `tooltipOffsetX` | `number` | - | Horizontal offset for tooltip positioning | -| `tooltipOffsetY` | `number` | - | Vertical offset for tooltip positioning | -| `tooltipDataFormat` | `DataFormat` | - | Format for tooltip values (falls back to `dataFormat`) | -| `emptyStateIcon` | `IconProps['icon']` | - | Icon for empty state | -| `emptyStateText` | `string` | - | Text for empty state | +| Prop | Type | Default | Description | +| ------------------- | --------------------- | -------------------- | ---------------------------------------------------------------------------------- | +| `chartData` | `SemiCircleChartData` | required | Array of segments with `label` and `value` (percentage is auto-calculated) | +| `styles` | `SegmentStyle[]` | - | Explicit colors per segment (takes priority over chartData colors) | +| `value` | `number` | required | Primary metric value displayed in center | +| `comparisonValue` | `number \| null` | - | Previous period value for delta calculation | +| `dataFormat` | `DataFormat` | `{ type: 'number' }` | Format configuration for the metric display | +| `legendData` | `LegendItem[]` | - | Legend items with labels and comparison values | +| `showLegend` | `boolean` | `true` | Whether to show the legend below the chart | +| `chartTitle` | `string` | - | What the chart shows, such as the widget title; the chart is named "<title> chart" | +| `thickness` | `number` | `0.3` | Arc thickness as ratio (0-1) | +| `maxWidth` | `number` | `Infinity` | Maximum width constraint for the chart | +| `withTooltips` | `boolean` | `false` | Enable tooltips on hover | +| `tooltipOffsetX` | `number` | - | Horizontal offset for tooltip positioning | +| `tooltipOffsetY` | `number` | - | Vertical offset for tooltip positioning | +| `tooltipDataFormat` | `DataFormat` | - | Format for tooltip values (falls back to `dataFormat`) | +| `emptyStateIcon` | `IconProps['icon']` | - | Icon for empty state | +| `emptyStateText` | `string` | - | Text for empty state | ## Responsive Layout diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/__tests__/semi-circle-chart.test.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/__tests__/semi-circle-chart.test.tsx new file mode 100644 index 000000000000..0b7fd75b0131 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/__tests__/semi-circle-chart.test.tsx @@ -0,0 +1,33 @@ +/** + * External dependencies + */ +import { render } from '@testing-library/react'; +/** + * Internal dependencies + */ +import { SemiCircleChart } from '../semi-circle-chart'; + +const mockPieSemiCircleChartSpy = jest.fn(); + +jest.mock( '@jetpack-premium-analytics/externals', () => ( { + ...jest.requireActual( '@jetpack-premium-analytics/externals' ), + PieSemiCircleChart: ( props: Record< string, unknown > ) => { + mockPieSemiCircleChartSpy( props ); + return null; + }, +} ) ); + +describe( 'SemiCircleChart', () => { + it.each( [ + [ 'Devices', 'Devices chart' ], + [ undefined, undefined ], + ] )( 'names the chart from chartTitle %s', ( chartTitle, ariaLabel ) => { + render( + <SemiCircleChart chartData={ [ { label: 'Mobile', value: 3 } ] } chartTitle={ chartTitle } /> + ); + + expect( mockPieSemiCircleChartSpy ).toHaveBeenLastCalledWith( + expect.objectContaining( { ariaLabel } ) + ); + } ); +} ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.tsx index 6ccf02f144b3..5ff738ca65e5 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.tsx @@ -7,6 +7,7 @@ import { RESIZE_DEBOUNCE_MS } from '../../constants'; import { resolveSegmentStyles, applyStylesToItems, + formatChartName, isEmptyPieChartData, type SegmentStyle, } from '../../helpers'; @@ -77,6 +78,9 @@ export type SemiCircleChartProps = { */ showMetric?: boolean; + /** What the chart shows, such as the widget title; the chart is named "<title> chart". */ + chartTitle?: string; + /** * Thickness of the arc (0-1). * @default 0.3 @@ -136,6 +140,7 @@ export function SemiCircleChart( { legendData, showLegend = true, showMetric = true, + chartTitle, thickness = DEFAULT_THICKNESS, aspectRatio = DEFAULT_ASPECT_RATIO, maxWidth = Infinity, @@ -217,6 +222,7 @@ export function SemiCircleChart( { <PieSemiCircleChart data={ styledChartData } className={ styles.chart } + ariaLabel={ formatChartName( chartTitle ) } thickness={ thickness } clockwise={ false } aspectRatio={ aspectRatio } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/donut/__tests__/donut.test.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/donut/__tests__/donut.test.tsx index 51ea981f4bd0..2a170f87d954 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/donut/__tests__/donut.test.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/donut/__tests__/donut.test.tsx @@ -46,6 +46,16 @@ describe( 'Donut', () => { expect( screen.getByText( /27%/ ) ).toBeInTheDocument(); } ); + it( 'names the chart from chartTitle', () => { + renderDonut( <Donut segments={ SEGMENTS } status={ READY } chartTitle="Payment status" /> ); + + // jsdom measures no size, so the chart stays `visibility: hidden` and has no computed name. + expect( screen.getByRole( 'application', { hidden: true } ) ).toHaveAttribute( + 'aria-label', + 'Payment status chart' + ); + } ); + it( 'renders the loading shape while nothing answers the params', () => { renderDonut( <Donut segments={ [] } status={ { isLoading: true } } /> ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/donut/donut.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/donut/donut.tsx index 3f842d743647..89d06bb84aac 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/donut/donut.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/donut/donut.tsx @@ -42,6 +42,8 @@ export type DonutProps = { * Format of the segment values and the total. Defaults to compact integers. */ format?: DataFormat; + /** What the chart shows, such as the widget title; the chart is named "<title> chart". */ + chartTitle?: string; }; const DEFAULT_FORMAT: DataFormat = { @@ -66,6 +68,7 @@ export function Donut( { error, empty, format = DEFAULT_FORMAT, + chartTitle, }: DonutProps ): JSX.Element { const { isLoading, isFetching, isError, refetch } = status; const hasComparison = !! status.hasComparison; @@ -110,6 +113,7 @@ export function Donut( { comparisonValue={ previousTotal } dataFormat={ format } legendData={ legendData } + chartTitle={ chartTitle } maxSize={ null } withTooltips /> From a42a20b8dc432a13c17420e26e0460a3645c253c Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:04:58 +1300 Subject: [PATCH 11/14] Premium Analytics: Name the semi-circle charts after their widgets Screen reader users can tell the Devices and Sessions by device charts apart from the other charts on the dashboard. --- .../src/widgets/sessions-by-device/sessions-by-device-widget.tsx | 1 + projects/packages/premium-analytics/widgets/devices/render.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx index 23e41c092cdd..6af82340be23 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx @@ -68,6 +68,7 @@ export function SessionsByDeviceWidget() { comparisonValue={ hasComparison ? comparisonTotal : null } legendData={ legendData } showLegend={ true } + chartTitle={ __( 'Sessions by device', 'jetpack-premium-analytics-pkg' ) } dataFormat={ { type: 'number', options: { useMultipliers: true, decimals: 0 }, diff --git a/projects/packages/premium-analytics/widgets/devices/render.tsx b/projects/packages/premium-analytics/widgets/devices/render.tsx index bff490d81747..801b14241133 100644 --- a/projects/packages/premium-analytics/widgets/devices/render.tsx +++ b/projects/packages/premium-analytics/widgets/devices/render.tsx @@ -99,6 +99,7 @@ function DevicesInner() { styles={ segmentStyles } showLegend={ false } showMetric={ false } + chartTitle={ __( 'Devices', 'jetpack-premium-analytics-pkg' ) } withTooltips dataFormat={ PERCENTAGE_DATA_FORMAT } /> From 5164c6cc460ea72e9a173e3e454a74f974cd2fa1 Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:05:43 +1300 Subject: [PATCH 12/14] Premium Analytics SDK: Declare the Donut chart title Widgets built on the SDK can name their donut chart, as the toolkit Donut now accepts. --- ...remium-analytics-name-each-widget-chart-for-screen-readers | 4 ++++ projects/js-packages/premium-analytics-sdk/src/index.d.ts | 2 ++ 2 files changed, 6 insertions(+) create mode 100644 projects/js-packages/premium-analytics-sdk/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers diff --git a/projects/js-packages/premium-analytics-sdk/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers b/projects/js-packages/premium-analytics-sdk/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers new file mode 100644 index 000000000000..b75e40a162d9 --- /dev/null +++ b/projects/js-packages/premium-analytics-sdk/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Declare the optional chart title of a Donut, which names its chart for screen readers. diff --git a/projects/js-packages/premium-analytics-sdk/src/index.d.ts b/projects/js-packages/premium-analytics-sdk/src/index.d.ts index 7855e9b029d5..6ce6ab519421 100644 --- a/projects/js-packages/premium-analytics-sdk/src/index.d.ts +++ b/projects/js-packages/premium-analytics-sdk/src/index.d.ts @@ -86,6 +86,8 @@ export type DonutProps = { empty?: WidgetStateEmpty; // Defaults to compact integers. format?: DataFormat; + // What the chart shows, such as the widget title; the chart is named "<title> chart". + chartTitle?: string; }; // Widget attributes. The chart type is not one: a widget declares it as a `jpa/toggle-group` field. From 5be2dcb1e01b4e6c1592b9de3ab1e689f9d6c688 Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:05:47 +1300 Subject: [PATCH 13/14] WooCommerce Stats: Name the donut charts after their widgets Screen reader users can tell the breakdown charts apart instead of hearing "Pie chart" for each one. --- .../woocommerce-stats/widgets/bookings-by-status/render.tsx | 1 + .../woocommerce-stats/widgets/coupon-usage-over-time/render.tsx | 1 + .../widgets/new-vs-returning-customer/render.tsx | 1 + .../woocommerce-stats/widgets/orders-fulfillment/render.tsx | 1 + .../packages/woocommerce-stats/widgets/payment-status/render.tsx | 1 + 5 files changed, 5 insertions(+) diff --git a/projects/packages/woocommerce-stats/widgets/bookings-by-status/render.tsx b/projects/packages/woocommerce-stats/widgets/bookings-by-status/render.tsx index aa5438600819..233ffd77a74e 100644 --- a/projects/packages/woocommerce-stats/widgets/bookings-by-status/render.tsx +++ b/projects/packages/woocommerce-stats/widgets/bookings-by-status/render.tsx @@ -92,6 +92,7 @@ function BookingsByStatus() { return ( <Donut segments={ segments } + chartTitle={ __( 'Bookings by status', 'jetpack-woocommerce-stats-pkg' ) } status={ { isLoading, isFetching, diff --git a/projects/packages/woocommerce-stats/widgets/coupon-usage-over-time/render.tsx b/projects/packages/woocommerce-stats/widgets/coupon-usage-over-time/render.tsx index 35621fe4cb75..79f44e6a253a 100644 --- a/projects/packages/woocommerce-stats/widgets/coupon-usage-over-time/render.tsx +++ b/projects/packages/woocommerce-stats/widgets/coupon-usage-over-time/render.tsx @@ -68,6 +68,7 @@ function CouponUsageOverTime() { return ( <Donut segments={ segments } + chartTitle={ __( 'Coupon usage over time', 'jetpack-woocommerce-stats-pkg' ) } status={ { isLoading, isFetching, diff --git a/projects/packages/woocommerce-stats/widgets/new-vs-returning-customer/render.tsx b/projects/packages/woocommerce-stats/widgets/new-vs-returning-customer/render.tsx index 4dca199ab347..37898778ac68 100644 --- a/projects/packages/woocommerce-stats/widgets/new-vs-returning-customer/render.tsx +++ b/projects/packages/woocommerce-stats/widgets/new-vs-returning-customer/render.tsx @@ -67,6 +67,7 @@ function NewVsReturningCustomer() { return ( <Donut segments={ segments } + chartTitle={ __( 'New vs returning customer', 'jetpack-woocommerce-stats-pkg' ) } status={ { isLoading, isFetching, diff --git a/projects/packages/woocommerce-stats/widgets/orders-fulfillment/render.tsx b/projects/packages/woocommerce-stats/widgets/orders-fulfillment/render.tsx index 9b7d246839be..b92ed0697da5 100644 --- a/projects/packages/woocommerce-stats/widgets/orders-fulfillment/render.tsx +++ b/projects/packages/woocommerce-stats/widgets/orders-fulfillment/render.tsx @@ -72,6 +72,7 @@ function OrdersFulfillment() { return ( <Donut segments={ segments } + chartTitle={ __( 'Orders fulfillment', 'jetpack-woocommerce-stats-pkg' ) } status={ { isLoading: fulfilled.isLoading || unfulfilled.isLoading, isFetching: fulfilled.isFetching || unfulfilled.isFetching, diff --git a/projects/packages/woocommerce-stats/widgets/payment-status/render.tsx b/projects/packages/woocommerce-stats/widgets/payment-status/render.tsx index a846c8d063f6..9fed301a2bf2 100644 --- a/projects/packages/woocommerce-stats/widgets/payment-status/render.tsx +++ b/projects/packages/woocommerce-stats/widgets/payment-status/render.tsx @@ -68,6 +68,7 @@ function PaymentStatus() { return ( <Donut segments={ segments } + chartTitle={ __( 'Payment status', 'jetpack-woocommerce-stats-pkg' ) } status={ { isLoading, isFetching, From 68618da2b45a286353ba687603dfdf978223f867 Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:06:00 +1300 Subject: [PATCH 14/14] Changelog: Cover the WooCommerce Stats donut chart names --- ...-premium-analytics-name-each-widget-chart-for-screen-readers | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/packages/woocommerce-stats/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers b/projects/packages/woocommerce-stats/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers index 5aba173c9555..3358582b6a2d 100644 --- a/projects/packages/woocommerce-stats/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers +++ b/projects/packages/woocommerce-stats/changelog/stats-521-premium-analytics-name-each-widget-chart-for-screen-readers @@ -1,4 +1,4 @@ Significance: patch Type: fixed -Name the Store performance chart after its widget, so screen readers can tell the charts on a dashboard apart. +Name each widget chart after its widget, so screen readers can tell the charts on a dashboard apart.