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/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..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,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 | +| `chartTitle` | `string` | No | What the chart shows, such as the widget title; the chart is named " 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 new file mode 100644 index 000000000000..f3d96a072489 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/__tests__/bar-chart.test.tsx @@ -0,0 +1,39 @@ +/** + * 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.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' } } + chartTitle={ chartTitle } + /> + ); + + 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 d0b18bc150ff..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,6 +48,9 @@ export type BarChartProps = { className?: string; + /** What the chart shows, such as the widget title; the chart is named "<title> chart". */ + chartTitle?: string; + emptyStateIcon?: React.ComponentProps< typeof Icon >[ 'icon' ]; emptyStateText?: string; @@ -116,6 +119,7 @@ export function BarChart( { dataFormat, styles: stylesProp, className, + chartTitle, emptyStateIcon, emptyStateText, showZeroValues = true, @@ -192,6 +196,7 @@ export function BarChart( { return ( <BarChartBase chartId={ chartId } + 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 3445bf267051..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,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 | +| `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 635b8fdf07f9..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,6 +186,20 @@ describe( 'ComparativeBarChart', () => { setSettings( originalSettings ); } ); + it( 'names the chart from chartTitle', () => { + render( + <ComparativeBarChart + series={ SERIES } + dataFormat={ DATA_FORMAT } + chartTitle="Traffic summary" + /> + ); + + expect( mockBarChartSpy ).toHaveBeenLastCalledWith( + expect.objectContaining( { ariaLabel: 'Traffic summary chart' } ) + ); + } ); + it( 'passes no x tickFormat when no tick format is requested', () => { render( <ComparativeBarChart series={ SERIES } dataFormat={ DATA_FORMAT } /> ); 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 ad06e34d2ef4..9ced27aea1f4 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,6 +60,9 @@ export type ComparativeBarChartProps = { className?: 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; @@ -124,6 +132,7 @@ export function ComparativeBarChart( { series, className, chartId: providedChartId, + chartTitle, dataFormat, tickFormat: xTickFormatType, tickResolution, @@ -314,6 +323,7 @@ export function ComparativeBarChart( { <Stack ref={ measureRef } direction="column" className={ clsx( styles.chart, className ) }> <BarChart chartId={ chartId } + 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 06de5b311ad2..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,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 | +| `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 407c85fa9b6a..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,6 +174,20 @@ describe( 'ComparativeLineChart', () => { setSettings( originalSettings ); } ); + it( 'names the chart from chartTitle', () => { + render( + <ComparativeLineChart + series={ SERIES } + dataFormat={ DATA_FORMAT } + chartTitle="Traffic summary" + /> + ); + + expect( mockLineChartSpy ).toHaveBeenLastCalledWith( + expect.objectContaining( { ariaLabel: 'Traffic summary chart' } ) + ); + } ); + // `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 c0501797254b..28588f1967fb 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,6 +163,7 @@ export function ComparativeLineChart( { styles: stylesProp, className, chartId, + chartTitle, dataFormat, tickFormat: xTickFormatType, tickResolution, @@ -304,6 +310,7 @@ export function ComparativeLineChart( { <Stack ref={ measureRef } direction="column" className={ clsx( styles.chart, className ) }> <LineChart chartId={ resolvedChartId } + 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 29d909d901d5..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 @@ -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'; @@ -180,6 +180,33 @@ describe( 'MetricTabsChart', () => { ); } ); + it.each( [ + [ 'single-metric', 'line', mockLineSpy, [ METRIC ] ], + [ 'single-metric', 'bar', mockBarSpy, [ METRIC ] ], + [ 'tabbed', 'line', mockLineSpy, [ METRIC, VISITORS ] ], + [ 'tabbed', 'bar', mockBarSpy, [ METRIC, VISITORS ] ], + [ 'dropdown', 'line', mockLineSpy, [ METRIC, VISITORS ] ], + [ 'dropdown', 'bar', mockBarSpy, [ METRIC, VISITORS ] ], + ] as const )( 'passes chartTitle to the %s %s chart', ( layout, type, spy, metrics ) => { + if ( layout === 'dropdown' ) { + setMockChartWidth( 100 ); + } + + render( + <MetricTabsChart + metrics={ [ ...metrics ] } + dataFormat={ DATA_FORMAT } + chartType={ type } + chartTitle="Traffic summary" + /> + ); + + expect( screen.queryByRole( 'tablist' ) !== null ).toBe( layout === 'tabbed' ); + expect( spy ).toHaveBeenLastCalledWith( + expect.objectContaining( { chartTitle: 'Traffic summary' } ) + ); + } ); + it( 'keeps the previous period as a same-group comparison series in both chart types', () => { render( <MetricTabsChart metrics={ [ METRIC ] } dataFormat={ DATA_FORMAT } /> ); render( <MetricTabsChart metrics={ [ METRIC ] } dataFormat={ DATA_FORMAT } chartType="bar" /> ); 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..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,6 +140,8 @@ export interface MetricTabsChartProps { controls?: ReactNode; /** Accessible label for the metric tab list. */ groupLabel?: 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. @@ -222,6 +224,7 @@ function MetricChart( { dataFormat, chartType, chartId, + chartTitle, tickResolution, onDatumClick, baseline, @@ -235,6 +238,7 @@ function MetricChart( { chartType: MetricTabsChartType; baseline?: ChartBaseline; chartId: string; + chartTitle?: string; tickResolution?: TickResolution; onDatumClick?: ( date: Date ) => void; empty?: ReactNode; @@ -364,6 +368,7 @@ function MetricChart( { return chartType === 'bar' ? ( <ComparativeBarChart chartId={ chartId } + chartTitle={ chartTitle } series={ series } dataFormat={ resolvedDataFormat } defaultHiddenSeries={ defaultHiddenSeries } @@ -376,6 +381,7 @@ function MetricChart( { ) : ( <ComparativeLineChart chartId={ chartId } + chartTitle={ chartTitle } series={ series } styles={ seriesStyles } dataFormat={ resolvedDataFormat } @@ -462,6 +468,7 @@ export function MetricTabsChart( { onMetricChange, controls, groupLabel = __( 'Select metric', 'jetpack-premium-analytics-pkg' ), + chartTitle, tickResolution, onDatumClick, tooltipMetrics = 'active', @@ -561,6 +568,7 @@ export function MetricTabsChart( { dataFormat={ dataFormat } chartType={ chartType } chartId={ chartIdFor( activeMetric ) } + chartTitle={ chartTitle } tickResolution={ tickResolution } onDatumClick={ onDatumClick } empty={ empty } @@ -634,6 +642,7 @@ export function MetricTabsChart( { dataFormat={ dataFormat } chartType={ chartType } chartId={ chartIdFor( activeMetric ) } + chartTitle={ chartTitle } tickResolution={ tickResolution } onDatumClick={ onDatumClick } empty={ empty } @@ -683,6 +692,7 @@ export function MetricTabsChart( { dataFormat={ dataFormat } chartType={ chartType } chartId={ chartIdFor( metric ) } + 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 2e69c6d59524..80fa1ccaf76a 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 @@ -103,5 +103,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 78838fdc4556..b68cab7aee9e 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 @@ -22,6 +22,7 @@ type CustomerTypeRevenueWidgetProps = { * @see BOOKINGS_FILTER for booking products (booking, bookable-event, bookable-service) */ filter?: FilterCondition; + chartTitle: string; }; /** @@ -30,7 +31,7 @@ type CustomerTypeRevenueWidgetProps = { * * Must be used within a WidgetRoot which provides reportParams via context. */ -function CustomerTypeRevenueWidget( { filter }: CustomerTypeRevenueWidgetProps ) { +function CustomerTypeRevenueWidget( { filter, chartTitle }: CustomerTypeRevenueWidgetProps ) { const { reportParams } = useWidgetRootContext(); const { primary, comparison, isLoading, isFetching, hasData, isError, refetch } = @@ -74,6 +75,7 @@ function CustomerTypeRevenueWidget( { filter }: CustomerTypeRevenueWidgetProps ) type: 'currency', options: { useMultipliers: true }, } } + chartTitle={ chartTitle } /> </WidgetState> ); @@ -84,7 +86,11 @@ function CustomerTypeRevenueWidget( { filter }: CustomerTypeRevenueWidgetProps ) * No product type filtering applied. */ export function RevenueByCustomerTypeWidget() { - return <CustomerTypeRevenueWidget />; + return ( + <CustomerTypeRevenueWidget + chartTitle={ __( 'Revenue by customer type', 'jetpack-premium-analytics-pkg' ) } + /> + ); } /** @@ -92,5 +98,10 @@ export function RevenueByCustomerTypeWidget() { * Filters to: booking, bookable-event, and bookable-service product types. */ export function BookingsRevenueByCustomerTypeWidget() { - return <CustomerTypeRevenueWidget filter={ BOOKINGS_FILTER } />; + return ( + <CustomerTypeRevenueWidget + 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 47b8dbf2fe12..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() { +export function SalesByCouponWidget( { chartTitle }: { chartTitle?: string } ) { const { reportParams } = useWidgetRootContext(); const { primary, comparison, isLoading, isFetching, hasData, isError, refetch } = @@ -64,6 +64,7 @@ export function SalesByCouponWidget() { type: 'currency', options: { useMultipliers: true }, } } + 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 48289f51ddec..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 @@ -25,6 +25,8 @@ type SalesByDeviceWidgetProps = { emptyStateText?: string; errorText?: string; + + chartTitle?: string; }; /** @@ -36,6 +38,7 @@ export function SalesByDeviceWidget( { filter, emptyStateText, errorText, + chartTitle = __( 'Sales by device', 'jetpack-premium-analytics-pkg' ), }: SalesByDeviceWidgetProps ) { const { reportParams } = useWidgetRootContext(); @@ -89,6 +92,7 @@ export function SalesByDeviceWidget( { type: 'currency', options: { useMultipliers: true }, } } + 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 cf002e81f523..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,6 +60,7 @@ export function TotalReturnsWidget() { type: 'currency', options: { useMultipliers: true }, } } + chartTitle={ __( 'Total returns', 'jetpack-premium-analytics-pkg' ) } /> </WidgetState> ); diff --git a/projects/packages/premium-analytics/tests/groups/widgets-toolkit-chart-mock.test.tsx b/projects/packages/premium-analytics/tests/groups/widgets-toolkit-chart-mock.test.tsx index 0f282bb448ff..3ed07897f8f5 100644 --- a/projects/packages/premium-analytics/tests/groups/widgets-toolkit-chart-mock.test.tsx +++ b/projects/packages/premium-analytics/tests/groups/widgets-toolkit-chart-mock.test.tsx @@ -1,5 +1,6 @@ // See README.md before adding a suite to this group. +import '../../packages/widgets-toolkit/src/components/chart-bar/__tests__/bar-chart.test'; import '../../packages/widgets-toolkit/src/components/chart-comparative-bar/__tests__/comparative-bar-chart.test'; import '../../packages/widgets-toolkit/src/components/chart-comparative-line/__tests__/comparative-line-chart.test'; import '../../packages/widgets-toolkit/src/components/metric-tabs-chart/__tests__/metric-tabs-chart.test'; 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 } } ] ); } }, }; diff --git a/projects/packages/premium-analytics/widgets/author-performance/render.tsx b/projects/packages/premium-analytics/widgets/author-performance/render.tsx index e8b6692c5d00..8dadff237eab 100644 --- a/projects/packages/premium-analytics/widgets/author-performance/render.tsx +++ b/projects/packages/premium-analytics/widgets/author-performance/render.tsx @@ -98,6 +98,7 @@ function AuthorPerformanceInner( { chartType }: AuthorPerformanceInnerProps ) { metrics={ metricTabs } dataFormat={ DATA_FORMAT } chartType={ chartType } + 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 9e1150880174..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,6 +33,7 @@ function BookingsByDeviceWidget() { "We couldn't load booking data by device. Please try again in a moment.", '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 4ea715ee4b18..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,6 +167,7 @@ function EmailTimeSeriesReport( { metric, chartType }: EmailTimeSeriesReportProp metrics={ metricTabs } dataFormat={ DATA_FORMAT } chartType={ chartType } + 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 1ea46713463e..4ea7c397c1fc 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 } + 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 5e13ffb00f29..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 @@ -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 ( <WidgetRoot attributes={ attributes } setError={ setError } options={ { from: '/' } }> - <SalesByCouponWidget /> + <SalesByCouponWidget + 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 ce7d666c7a08..0b55bb6949a8 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,9 @@ export default function SalesByCouponRender( { }: SalesByCouponWidgetProps ) { return ( <WidgetRoot attributes={ attributes } setError={ setError } options={ { from: '/' } }> - <SalesByCouponWidget /> + <SalesByCouponWidget + chartTitle={ __( 'Sales by coupon', 'jetpack-premium-analytics-pkg' ) } + /> </WidgetRoot> ); } diff --git a/projects/packages/premium-analytics/widgets/store-performance/render.tsx b/projects/packages/premium-analytics/widgets/store-performance/render.tsx index 4b58b9b5ecf9..878e08c93493 100644 --- a/projects/packages/premium-analytics/widgets/store-performance/render.tsx +++ b/projects/packages/premium-analytics/widgets/store-performance/render.tsx @@ -362,6 +362,7 @@ function StorePerformanceContent() { metrics={ metricTabs } dataFormat={ DEFAULT_DATA_FORMAT } groupLabel={ __( 'Store metric', 'jetpack-premium-analytics-pkg' ) } + chartTitle={ __( 'Store performance', 'jetpack-premium-analytics-pkg' ) } empty={ <ChartEmptyState /> } /> </WidgetState> diff --git a/projects/packages/premium-analytics/widgets/subscribers-chart/render.tsx b/projects/packages/premium-analytics/widgets/subscribers-chart/render.tsx index 827e3df8998c..c41b65c2246f 100644 --- a/projects/packages/premium-analytics/widgets/subscribers-chart/render.tsx +++ b/projects/packages/premium-analytics/widgets/subscribers-chart/render.tsx @@ -137,6 +137,7 @@ function SubscribersChartInner( { chartType }: SubscribersChartInnerProps ) { chartType={ chartType } groupLabel={ groupLabel } baseline="padded" + 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 4eb4a3c2234a..87b85bfa2630 100644 --- a/projects/packages/premium-analytics/widgets/traffic-chart/render.tsx +++ b/projects/packages/premium-analytics/widgets/traffic-chart/render.tsx @@ -120,6 +120,7 @@ function TrafficChartInner( { chartType, interval }: TrafficChartInnerProps ) { groupLabel={ groupLabel } tickResolution={ period } onDatumClick={ openBucket } + 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 d1382d389fc1..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,6 +82,7 @@ function VideoDetailViewsPerformanceInner( { chartType }: VideoDetailViewsPerfor dataFormat={ COUNT_FORMAT } chartType={ chartType } groupLabel={ groupLabel } + chartTitle={ __( 'Video performance', 'jetpack-premium-analytics-pkg' ) } empty={ <ChartEmptyState /> } /> </WidgetState> 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. 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.