From ff688d485555145e5fc1e42765c38b936d57b9d2 Mon Sep 17 00:00:00 2001 From: Mael Kerichard Date: Wed, 5 Nov 2025 14:48:56 +0000 Subject: [PATCH 01/15] feat: add status property support for series markers Add support for displaying series markers with different status states, particularly a "warning" status. This enables visual indication of series that require attention or have issues. Changes include: - Extend Highcharts SeriesOptions interface with optional status property - Update ChartSeriesMarker to accept and render status prop - Pass status through chart legend and tooltip components - Add visual examples in marker-permutations page for warning state --- pages/03-core/core-line-chart.page.tsx | 1 + pages/03-core/marker-permutations.page.tsx | 19 ++++++++++- pages/types/highcharts-extension.d.ts | 10 ++++++ src/core/chart-api/chart-extra-legend.tsx | 21 +++++++++--- src/core/components/core-tooltip.tsx | 13 ++++++-- src/core/utils.ts | 9 ++++- .../series-marker/__tests__/index.test.tsx | 33 +++++++++++++++++++ .../components/series-marker/index.tsx | 17 +++++++++- .../components/series-marker/styles.scss | 2 +- types/highcharts-extension.d.ts | 10 ++++++ 10 files changed, 123 insertions(+), 12 deletions(-) create mode 100644 pages/types/highcharts-extension.d.ts create mode 100644 src/internal/components/series-marker/__tests__/index.test.tsx create mode 100644 types/highcharts-extension.d.ts diff --git a/pages/03-core/core-line-chart.page.tsx b/pages/03-core/core-line-chart.page.tsx index 3bcaf61e..3aef6f2a 100644 --- a/pages/03-core/core-line-chart.page.tsx +++ b/pages/03-core/core-line-chart.page.tsx @@ -73,6 +73,7 @@ const series: Highcharts.SeriesOptionsType[] = [ name: "Comprehensive System Resource Utilization Measurements Over Time", type: "line", data: dataC, + status: "warning" as const, }, { name: "X", diff --git a/pages/03-core/marker-permutations.page.tsx b/pages/03-core/marker-permutations.page.tsx index 50f7f75e..090f8e85 100644 --- a/pages/03-core/marker-permutations.page.tsx +++ b/pages/03-core/marker-permutations.page.tsx @@ -21,7 +21,7 @@ import { import { ChartSeriesMarker, ChartSeriesMarkerProps } from "../../lib/components/internal/components/series-marker"; import PermutationsView, { createPermutations } from "../common/permutations"; -import { Page } from "../common/templates"; +import { Page, PageSection } from "../common/templates"; const permutationsForColors = [ colorChartsPaletteCategorical1, @@ -57,6 +57,10 @@ const permutationsForColors = [ ]), ); +const permutationsForWarningColors = permutationsForColors.map((permutations) => + permutations.map((permutation) => ({ ...permutation, status: "warning" as const })), +); + export default function MarkerPermutations() { return ( @@ -70,6 +74,19 @@ export default function MarkerPermutations() { /> ))} + + + + {permutationsForWarningColors.map((permutations, index) => ( + } + direction="horizontal" + /> + ))} + + ); } diff --git a/pages/types/highcharts-extension.d.ts b/pages/types/highcharts-extension.d.ts new file mode 100644 index 00000000..b7201c21 --- /dev/null +++ b/pages/types/highcharts-extension.d.ts @@ -0,0 +1,10 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ChartSeriesMarkerStatus } from "../src/internal/components/series-marker"; + +declare module "highcharts" { + interface SeriesOptions { + status?: ChartSeriesMarkerStatus; + } +} diff --git a/src/core/chart-api/chart-extra-legend.tsx b/src/core/chart-api/chart-extra-legend.tsx index a8d06ffe..d24a1b2d 100644 --- a/src/core/chart-api/chart-extra-legend.tsx +++ b/src/core/chart-api/chart-extra-legend.tsx @@ -3,7 +3,11 @@ import type Highcharts from "highcharts"; -import { ChartSeriesMarker, ChartSeriesMarkerType } from "../../internal/components/series-marker"; +import { + ChartSeriesMarker, + ChartSeriesMarkerStatus, + ChartSeriesMarkerType, +} from "../../internal/components/series-marker"; import { fireNonCancelableEvent } from "../../internal/events"; import AsyncStore from "../../internal/utils/async-store"; import { getChartSeries } from "../../internal/utils/chart-series"; @@ -82,8 +86,8 @@ export class ChartExtraLegend extends AsyncStore { private initLegend = () => { const itemSpecs = getChartLegendItems(this.context.chart()); - const legendItems = itemSpecs.map(({ id, name, color, markerType, visible }) => { - const marker = this.renderMarker(markerType, color, visible); + const legendItems = itemSpecs.map(({ id, name, color, markerType, visible, status }) => { + const marker = this.renderMarker(markerType, color, visible, status); return { id, name, marker, visible, highlighted: false }; }); this.updateLegendItems(legendItems); @@ -107,9 +111,16 @@ export class ChartExtraLegend extends AsyncStore { // The chart markers derive from type and color and are cached to avoid unnecessary renders, // and allow comparing them by reference. private markersCache = new Map(); - public renderMarker(type: ChartSeriesMarkerType, color: string, visible = true): React.ReactNode { + public renderMarker( + type: ChartSeriesMarkerType, + color: string, + visible = true, + status?: ChartSeriesMarkerStatus, + ): React.ReactNode { const key = `${type}:${color}:${visible}`; - const marker = this.markersCache.get(key) ?? ; + const marker = this.markersCache.get(key) ?? ( + + ); this.markersCache.set(key, marker); return marker; } diff --git a/src/core/components/core-tooltip.tsx b/src/core/components/core-tooltip.tsx index f50cb5fa..3432366f 100644 --- a/src/core/components/core-tooltip.tsx +++ b/src/core/components/core-tooltip.tsx @@ -14,7 +14,14 @@ import { getChartSeries } from "../../internal/utils/chart-series"; import { ChartAPI } from "../chart-api"; import { getFormatter } from "../formatters"; import { BaseI18nStrings, CoreChartProps } from "../interfaces"; -import { getPointColor, getSeriesColor, getSeriesId, getSeriesMarkerType, isXThreshold } from "../utils"; +import { + getPointColor, + getSeriesColor, + getSeriesId, + getSeriesMarkerType, + getSeriesStatus, + isXThreshold, +} from "../utils"; import styles from "../styles.css.js"; @@ -151,7 +158,7 @@ function getTooltipContentCartesian( const x = group[0].x; const chart = group[0].series.chart; const getSeriesMarker = (series: Highcharts.Series) => - api.renderMarker(getSeriesMarkerType(series), getSeriesColor(series), true); + api.renderMarker(getSeriesMarkerType(series), getSeriesColor(series), true, getSeriesStatus(series)); const matchedItems = findTooltipSeriesItems(getChartSeries(chart.series), group); const detailItems: ChartSeriesDetailItem[] = matchedItems.map((item) => { const valueFormatter = getFormatter(item.point.series.yAxis); @@ -232,7 +239,7 @@ function getTooltipContentPie( return { header: renderers.header?.(tooltipDetails) ?? (
- {api.renderMarker(getSeriesMarkerType(point.series), getPointColor(point))} + {api.renderMarker(getSeriesMarkerType(point.series), getPointColor(point), true, getSeriesStatus(point.series))} {point.name} diff --git a/src/core/utils.ts b/src/core/utils.ts index 87b2243b..3c2e1cea 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -3,7 +3,8 @@ import type Highcharts from "highcharts"; -import { ChartSeriesMarkerType } from "../internal/components/series-marker"; +// import type { SeriesOptionsType } from "highcharts"; +import { ChartSeriesMarkerStatus, ChartSeriesMarkerType } from "../internal/components/series-marker"; import { getChartSeries } from "../internal/utils/chart-series"; import { getFormatter } from "./formatters"; import { ChartLabels } from "./i18n-utils"; @@ -15,6 +16,7 @@ export interface LegendItemSpec { markerType: ChartSeriesMarkerType; color: string; visible: boolean; + status?: ChartSeriesMarkerStatus; } // The below functions extract unique identifier from series, point, or options. The identifier can be item's ID or name. @@ -123,6 +125,9 @@ export function getSeriesColor(series?: Highcharts.Series): string { export function getPointColor(point?: Highcharts.Point): string { return typeof point?.color === "string" ? point.color : "black"; } +export function getSeriesStatus(series?: Highcharts.Series): ChartSeriesMarkerStatus | undefined { + return series?.userOptions.status; +} // The custom legend implementation does not rely on the Highcharts legend. When Highcharts legend is disabled, // the chart object does not include information on legend items. Instead, we collect series and pie segments @@ -151,6 +156,7 @@ export function getChartLegendItems(chart: Highcharts.Chart): readonly LegendIte markerType: getSeriesMarkerType(series), color: getSeriesColor(series), visible: series.visible, + status: getSeriesStatus(series), }); } }; @@ -162,6 +168,7 @@ export function getChartLegendItems(chart: Highcharts.Chart): readonly LegendIte markerType: getSeriesMarkerType(point.series), color: getPointColor(point), visible: point.visible, + status: getSeriesStatus(point.series), }); } }; diff --git a/src/internal/components/series-marker/__tests__/index.test.tsx b/src/internal/components/series-marker/__tests__/index.test.tsx new file mode 100644 index 00000000..01df7b9c --- /dev/null +++ b/src/internal/components/series-marker/__tests__/index.test.tsx @@ -0,0 +1,33 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ComponentProps } from "react"; +import { render } from "@testing-library/react"; +import { describe, expect, test } from "vitest"; + +import { ChartSeriesMarker } from ".."; + +describe("ChartSeriesMarker", () => { + const defaultProps = { + type: "line", + color: "#0073bb", + } satisfies ComponentProps; + + describe("Warning SVG display", () => { + test("does not render warning SVG when status is undefined", () => { + const { container } = render(); + const svgs = container.querySelectorAll("svg"); + + // Should only have one SVG (the marker itself) + expect(svgs).toHaveLength(1); + }); + + test("renders warning SVG when status is 'warning'", () => { + const { container } = render(); + const svgs = container.querySelectorAll("svg"); + + // Should have two SVGs: marker + warning + expect(svgs).toHaveLength(2); + }); + }); +}); diff --git a/src/internal/components/series-marker/index.tsx b/src/internal/components/series-marker/index.tsx index d9e050fb..1886c4cf 100644 --- a/src/internal/components/series-marker/index.tsx +++ b/src/internal/components/series-marker/index.tsx @@ -17,17 +17,20 @@ export type ChartSeriesMarkerType = | "triangle-down" | "circle"; +export type ChartSeriesMarkerStatus = "warning"; + export interface ChartSeriesMarkerProps extends BaseComponentProps { type: ChartSeriesMarkerType; color: string; visible?: boolean; + status?: ChartSeriesMarkerStatus; } function scale(size: number, value: number) { return `translate(${size * ((1 - value) / 2)}, ${size * ((1 - value) / 2)}) scale(${value})`; } -export function ChartSeriesMarker({ type = "line", color, visible = true }: ChartSeriesMarkerProps) { +export function ChartSeriesMarker({ type = "line", color, visible = true, status }: ChartSeriesMarkerProps) { color = visible ? color : colorTextInteractiveDisabled; return (
@@ -50,6 +53,7 @@ export function ChartSeriesMarker({ type = "line", color, visible = true }: Char {type === "circle" && } + {status === "warning" && }
); } @@ -101,3 +105,14 @@ function SVGCircle({ color }: { color: string }) { const shape = { cx: 8, cy: 8, r: 5 }; return ; } + +function SVGWarning() { + return ( + + + + ); +} diff --git a/src/internal/components/series-marker/styles.scss b/src/internal/components/series-marker/styles.scss index de66b1e8..d1eac16c 100644 --- a/src/internal/components/series-marker/styles.scss +++ b/src/internal/components/series-marker/styles.scss @@ -18,7 +18,7 @@ $marker-margin-right: cs.$space-static-xxs; border-start-end-radius: 2px; border-end-start-radius: 2px; border-end-end-radius: 2px; - inline-size: $marker-size; + display: flex; block-size: $marker-size; flex-shrink: 0; cursor: inherit; diff --git a/types/highcharts-extension.d.ts b/types/highcharts-extension.d.ts new file mode 100644 index 00000000..b7201c21 --- /dev/null +++ b/types/highcharts-extension.d.ts @@ -0,0 +1,10 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ChartSeriesMarkerStatus } from "../src/internal/components/series-marker"; + +declare module "highcharts" { + interface SeriesOptions { + status?: ChartSeriesMarkerStatus; + } +} From a0ba1c451f50d8d108597d53f5770d27d3506f25 Mon Sep 17 00:00:00 2001 From: Mael Kerichard Date: Tue, 11 Nov 2025 15:46:43 +0000 Subject: [PATCH 02/15] chore: make the marker accessible --- pages/03-core/core-legend.page.tsx | 19 +++++++++++++++---- pages/03-core/marker-permutations.page.tsx | 1 + .../__snapshots__/documenter.test.ts.snap | 10 ++++++++++ src/core/chart-api/chart-extra-legend.tsx | 15 +++++++++------ src/core/i18n-utils.tsx | 5 +++-- src/core/interfaces.ts | 5 ++++- src/core/utils.ts | 4 ++-- .../series-marker/__tests__/index.test.tsx | 10 ++++++++++ .../components/series-marker/index.tsx | 19 +++++++++++++++---- .../components/series-marker/interfaces.ts | 6 ++++++ 10 files changed, 75 insertions(+), 19 deletions(-) diff --git a/pages/03-core/core-legend.page.tsx b/pages/03-core/core-legend.page.tsx index fcae7387..b15e9e98 100644 --- a/pages/03-core/core-legend.page.tsx +++ b/pages/03-core/core-legend.page.tsx @@ -19,6 +19,10 @@ import { PageSettingsForm, useChartSettings } from "../common/page-settings"; import { Page } from "../common/templates"; import pseudoRandom from "../utils/pseudo-random"; +const i18nStrings = { + seriesStatusWarningAriaLabel: "warning", +}; + function randomInt(min: number, max: number) { return min + Math.floor(pseudoRandom() * (max - min)); } @@ -105,21 +109,21 @@ const initialLegendItems: readonly LegendItem[] = [ { id: "CPU Utilization", name: "CPU Utilization", - marker: , + marker: , visible: true, highlighted: false, }, { id: "Memory Usage", name: "Memory Usage", - marker: , + marker: , visible: true, highlighted: false, }, { id: "Storage Capacity", name: "Storage Capacity", - marker: , + marker: , visible: true, highlighted: false, }, @@ -180,7 +184,14 @@ export default function () { ...item, visible, highlighted: visible, - marker: , + marker: ( + + ), }; }); }); diff --git a/pages/03-core/marker-permutations.page.tsx b/pages/03-core/marker-permutations.page.tsx index 090f8e85..70a1c779 100644 --- a/pages/03-core/marker-permutations.page.tsx +++ b/pages/03-core/marker-permutations.page.tsx @@ -53,6 +53,7 @@ const permutationsForColors = [ "triangle-down", ], color: [color], + i18nStrings: [{ seriesStatusWarningAriaLabel: "warning" }], }, ]), ); diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index 9a6caa61..03263f00 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -177,6 +177,11 @@ Supported Highcharts versions: 12.", "optional": true, "type": "string", }, + { + "name": "seriesStatusWarningAriaLabel", + "optional": false, + "type": "string", + }, { "name": "xAxisRoleDescription", "optional": true, @@ -830,6 +835,11 @@ Supported Highcharts versions: 12.", "optional": true, "type": "string", }, + { + "name": "seriesStatusWarningAriaLabel", + "optional": false, + "type": "string", + }, ], "type": "object", }, diff --git a/src/core/chart-api/chart-extra-legend.tsx b/src/core/chart-api/chart-extra-legend.tsx index d24a1b2d..6b8ef143 100644 --- a/src/core/chart-api/chart-extra-legend.tsx +++ b/src/core/chart-api/chart-extra-legend.tsx @@ -3,11 +3,8 @@ import type Highcharts from "highcharts"; -import { - ChartSeriesMarker, - ChartSeriesMarkerStatus, - ChartSeriesMarkerType, -} from "../../internal/components/series-marker"; +import { ChartSeriesMarker, ChartSeriesMarkerType } from "../../internal/components/series-marker"; +import { ChartSeriesMarkerStatus } from "../../internal/components/series-marker/interfaces"; import { fireNonCancelableEvent } from "../../internal/events"; import AsyncStore from "../../internal/utils/async-store"; import { getChartSeries } from "../../internal/utils/chart-series"; @@ -119,7 +116,13 @@ export class ChartExtraLegend extends AsyncStore { ): React.ReactNode { const key = `${type}:${color}:${visible}`; const marker = this.markersCache.get(key) ?? ( - + ); this.markersCache.set(key, marker); return marker; diff --git a/src/core/i18n-utils.tsx b/src/core/i18n-utils.tsx index 08f38ce5..99ffd2a1 100644 --- a/src/core/i18n-utils.tsx +++ b/src/core/i18n-utils.tsx @@ -3,9 +3,10 @@ import { useInternalI18n } from "@cloudscape-design/components/internal/do-not-use/i18n"; +import { ChartSeriesMarkerI18n } from "../internal/components/series-marker/interfaces"; import { CoreChartProps } from "./interfaces"; -export interface ChartLabels { +export interface ChartLabels extends ChartSeriesMarkerI18n { chartLabel?: string; chartDescription?: string; chartContainerLabel?: string; @@ -22,7 +23,7 @@ export function useChartI18n({ ariaLabel?: string; ariaDescription?: string; i18nStrings?: CoreChartProps.I18nStrings; -}) { +}): ChartLabels { const i18n = useInternalI18n("[charts]"); const i18nPie = useInternalI18n("pie-chart"); return { diff --git a/src/core/interfaces.ts b/src/core/interfaces.ts index 8ef6a7f2..bda7bc56 100644 --- a/src/core/interfaces.ts +++ b/src/core/interfaces.ts @@ -4,6 +4,7 @@ import type Highcharts from "highcharts"; import type * as InternalComponentTypes from "../internal/components/interfaces"; +import { ChartSeriesMarkerI18n } from "../internal/components/series-marker/interfaces"; import { type NonCancelableEventHandler } from "../internal/events"; // All charts take `highcharts` instance, that can be served statically or dynamically. @@ -113,7 +114,7 @@ export interface BaseTooltipDetail { value: React.ReactNode; } -export interface BaseI18nStrings { +export interface BaseI18nStrings extends ChartSeriesMarkerI18n { loadingText?: string; errorText?: string; recoveryText?: string; @@ -142,6 +143,7 @@ export interface WithCartesianI18nStrings { * * `chartRoleDescription` (optional, string) - Accessible role description of the chart plot area, e.g. "interactive chart". * * `xAxisRoleDescription` (optional, string) - Accessible role description of the x axis, e.g. "x axis". * * `yAxisRoleDescription` (optional, string) - Accessible role description of the y axis, e.g. "y axis". + * * `seriesStatusWarningAriaLabel` (optional, string) - ARIA label for series with status warning, e.g. "warning". */ i18nStrings?: CartesianI18nStrings; } @@ -162,6 +164,7 @@ export interface WithPieI18nStrings { * * `detailPopoverDismissAriaLabel` (optional, string) - ARIA label for the details popover dismiss button. * * `chartRoleDescription` (optional, string) - Accessible role description of the chart plot area, e.g. "interactive chart". * * `segmentRoleDescription` (optional, string) - Accessible role description of the segment. + * * `seriesStatusWarningAriaLabel` (optional, string) - ARIA label for series with status warning, e.g. "warning". */ i18nStrings?: PieI18nStrings; } diff --git a/src/core/utils.ts b/src/core/utils.ts index 3c2e1cea..999c3874 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -3,8 +3,8 @@ import type Highcharts from "highcharts"; -// import type { SeriesOptionsType } from "highcharts"; -import { ChartSeriesMarkerStatus, ChartSeriesMarkerType } from "../internal/components/series-marker"; +import { ChartSeriesMarkerType } from "../internal/components/series-marker"; +import { ChartSeriesMarkerStatus } from "../internal/components/series-marker/interfaces"; import { getChartSeries } from "../internal/utils/chart-series"; import { getFormatter } from "./formatters"; import { ChartLabels } from "./i18n-utils"; diff --git a/src/internal/components/series-marker/__tests__/index.test.tsx b/src/internal/components/series-marker/__tests__/index.test.tsx index 01df7b9c..8644c2db 100644 --- a/src/internal/components/series-marker/__tests__/index.test.tsx +++ b/src/internal/components/series-marker/__tests__/index.test.tsx @@ -11,6 +11,9 @@ describe("ChartSeriesMarker", () => { const defaultProps = { type: "line", color: "#0073bb", + i18nStrings: { + seriesStatusWarningAriaLabel: "warning", + }, } satisfies ComponentProps; describe("Warning SVG display", () => { @@ -29,5 +32,12 @@ describe("ChartSeriesMarker", () => { // Should have two SVGs: marker + warning expect(svgs).toHaveLength(2); }); + + test("renders warning SVG with correct aria-label", () => { + const { container } = render(); + const warningSvg = container.querySelector("svg[aria-label='warning']"); + + expect(warningSvg).toBeTruthy(); + }); }); }); diff --git a/src/internal/components/series-marker/index.tsx b/src/internal/components/series-marker/index.tsx index 1886c4cf..b0220594 100644 --- a/src/internal/components/series-marker/index.tsx +++ b/src/internal/components/series-marker/index.tsx @@ -19,18 +19,29 @@ export type ChartSeriesMarkerType = export type ChartSeriesMarkerStatus = "warning"; +export type ChartSeriesMarkerI18n = Partial<{ + seriesStatusWarningAriaLabel: string; +}>; + export interface ChartSeriesMarkerProps extends BaseComponentProps { type: ChartSeriesMarkerType; color: string; visible?: boolean; status?: ChartSeriesMarkerStatus; + i18nStrings: ChartSeriesMarkerI18n; } function scale(size: number, value: number) { return `translate(${size * ((1 - value) / 2)}, ${size * ((1 - value) / 2)}) scale(${value})`; } -export function ChartSeriesMarker({ type = "line", color, visible = true, status }: ChartSeriesMarkerProps) { +export function ChartSeriesMarker({ + type = "line", + color, + visible = true, + status, + i18nStrings, +}: ChartSeriesMarkerProps) { color = visible ? color : colorTextInteractiveDisabled; return (
@@ -53,7 +64,7 @@ export function ChartSeriesMarker({ type = "line", color, visible = true, status {type === "circle" && } - {status === "warning" && } + {status === "warning" && }
); } @@ -106,9 +117,9 @@ function SVGCircle({ color }: { color: string }) { return ; } -function SVGWarning() { +function SVGWarning({ ariaLabel }: { ariaLabel: string }) { return ( - + ; From 3afac40c078b3c9628bac6f5dd0d869d81007d69 Mon Sep 17 00:00:00 2001 From: Mael Kerichard Date: Mon, 24 Nov 2025 14:31:50 +0000 Subject: [PATCH 03/15] feat: overlap warning with marker --- .../components/series-marker/index.tsx | 25 ++++++++++++++++--- .../components/series-marker/styles.scss | 12 +++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/internal/components/series-marker/index.tsx b/src/internal/components/series-marker/index.tsx index b0220594..74191794 100644 --- a/src/internal/components/series-marker/index.tsx +++ b/src/internal/components/series-marker/index.tsx @@ -64,7 +64,11 @@ export function ChartSeriesMarker({ {type === "circle" && } - {status === "warning" && } + {status === "warning" && ( +
+ +
+ )}
); } @@ -119,11 +123,24 @@ function SVGCircle({ color }: { color: string }) { function SVGWarning({ ariaLabel }: { ariaLabel: string }) { return ( - + + + /> ); } diff --git a/src/internal/components/series-marker/styles.scss b/src/internal/components/series-marker/styles.scss index d1eac16c..1fc136e0 100644 --- a/src/internal/components/series-marker/styles.scss +++ b/src/internal/components/series-marker/styles.scss @@ -22,4 +22,16 @@ $marker-margin-right: cs.$space-static-xxs; block-size: $marker-size; flex-shrink: 0; cursor: inherit; + position: relative; +} + +.marker-status { + position: absolute; + inset-inline-end: -3px; + inset-block-end: -3px; + inline-size: 13px; + block-size: 13px; + display: flex; + align-items: center; + justify-content: center; } From 7b5283eb112d35ebc10abe5f02143d2bb942f2b5 Mon Sep 17 00:00:00 2001 From: Mael Kerichard Date: Tue, 25 Nov 2025 18:04:17 +0000 Subject: [PATCH 04/15] feat: use a mask for the warning icon --- .../components/series-marker/index.tsx | 117 ++++++++++-------- .../components/series-marker/styles.scss | 13 +- 2 files changed, 65 insertions(+), 65 deletions(-) diff --git a/src/internal/components/series-marker/index.tsx b/src/internal/components/series-marker/index.tsx index 74191794..bf4f35cc 100644 --- a/src/internal/components/series-marker/index.tsx +++ b/src/internal/components/series-marker/index.tsx @@ -1,8 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { useId } from "react"; + import { BaseComponentProps } from "@cloudscape-design/components/internal/base-component"; -import { colorTextInteractiveDisabled } from "@cloudscape-design/design-tokens"; +import { colorBorderStatusWarning, colorTextInteractiveDisabled } from "@cloudscape-design/design-tokens"; import styles from "./styles.css.js"; @@ -43,104 +45,113 @@ export function ChartSeriesMarker({ i18nStrings, }: ChartSeriesMarkerProps) { color = visible ? color : colorTextInteractiveDisabled; + + // As React re-renders the components, a new ID should be created for masks. + // Not doing so will not evaluate the mask. + const maskId = useId(); + return (
- - {type === "line" && } + + + + + + {status === "warning" && } + + + + {status === "warning" && } + + {type === "line" && } - {type === "dashed" && } + {type === "dashed" && } - {type === "large-square" && } + {type === "large-square" && } - {type === "hollow-square" && } + {type === "hollow-square" && } - {type === "square" && } + {type === "square" && } - {type === "diamond" && } + {type === "diamond" && } - {type === "triangle" && } + {type === "triangle" && } - {type === "triangle-down" && } + {type === "triangle-down" && } - {type === "circle" && } + {type === "circle" && } - {status === "warning" && ( -
- -
- )}
); } -function SVGLine({ color }: { color: string }) { - return ; +function SVGLine({ color, maskId }: { color: string; maskId: string }) { + return ; } -function SVGLineDashed({ color }: { color: string }) { +function SVGLineDashed({ color, maskId }: { color: string; maskId: string }) { return ( <> - - + + ); } -function SVGLargeSquare({ color }: { color: string }) { +function SVGLargeSquare({ color, maskId }: { color: string; maskId: string }) { const shape = { x: 0, y: 0, width: 16, height: 16, rx: 2, transform: scale(16, 0.85) }; - return ; + return ; } -function SVGHollowSquare({ color }: { color: string }) { +function SVGHollowSquare({ color, maskId }: { color: string; maskId: string }) { const size = { x: 0, y: 0, width: 16, height: 16, rx: 2, transform: scale(16, 0.75) }; - return ; + return ; } -function SVGSquare({ color }: { color: string }) { +function SVGSquare({ color, maskId }: { color: string; maskId: string }) { const size = { x: 3, y: 3, width: 10, height: 10, rx: 2 }; - return ; + return ; } -function SVGDiamond({ color }: { color: string }) { +function SVGDiamond({ color, maskId }: { color: string; maskId: string }) { const shape = { points: "8,0 16,8 8,16 0,8", transform: scale(16, 0.65) }; - return ; + return ; } -function SVGTriangle({ color }: { color: string }) { +function SVGTriangle({ color, maskId }: { color: string; maskId: string }) { const shape = { points: "8,0 16,16 0,16", transform: scale(16, 0.65) }; - return ; + return ; } -function SVGTriangleDown({ color }: { color: string }) { +function SVGTriangleDown({ color, maskId }: { color: string; maskId: string }) { const shape = { points: "8,16 0,0 16,0", transform: scale(16, 0.65) }; - return ; + return ; } -function SVGCircle({ color }: { color: string }) { +function SVGCircle({ color, maskId }: { color: string; maskId: string }) { const shape = { cx: 8, cy: 8, r: 5 }; - return ; + return ; } +const SVGWarningTranslate = `translate(12, 1)`; + function SVGWarning({ ariaLabel }: { ariaLabel: string }) { return ( - - - - + transform={`${SVGWarningTranslate} ${scale(16, 0.8)}`} + d="M9.14157,12.3948v-1.713c0,-0.084,-0.028,-0.154,-0.085,-0.211c-0.056,-0.058,-0.123,-0.086,-0.2,-0.086h-1.713c-0.077,0,-0.144,0.028,-0.2,0.086c-0.057,0.057,-0.085,0.127,-0.085,0.211v1.713c0,0.084,0.028,0.155,0.085,0.212c0.056,0.057,0.123,0.085,0.2,0.085h1.713c0.077,0,0.144,-0.028,0.2,-0.085c0.057,-0.057,0.085,-0.128,0.085,-0.212zm-0.018,-3.371l0.161,-4.138c0,-0.072,-0.03,-0.129,-0.089,-0.171c-0.078,-0.066,-0.149,-0.099,-0.215,-0.099h-1.962c-0.065,0,-0.136,0.033,-0.214,0.099c-0.059,0.042,-0.089,0.105,-0.089,0.189l0.152,4.12c0,0.06,0.03,0.109,0.089,0.148c0.059,0.039,0.131,0.059,0.214,0.059h1.65c0.083,0,0.153,-0.02,0.21,-0.059c0.056,-0.039,0.087,-0.088,0.093,-0.148zm-0.125,-8.419l6.85,12.692c0.208,0.378,0.202,0.757,-0.018,1.136c-0.101,0.174,-0.24,0.312,-0.415,0.414c-0.175,0.102,-0.364,0.154,-0.566,0.154h-13.699c-0.202,0,-0.391,-0.052,-0.566,-0.154c-0.176,-0.102,-0.314,-0.24,-0.415,-0.414c-0.22,-0.379,-0.226,-0.758,-0.018,-1.136l6.849,-12.692c0.101,-0.187,0.241,-0.334,0.42,-0.442c0.178,-0.108,0.371,-0.162,0.579,-0.162c0.208,0,0.402,0.054,0.58,0.162c0.178,0.108,0.318,0.255,0.419,0.442z" + fill={colorBorderStatusWarning} + /> + ); +} + +function SVGWarningMask() { + return ( + ); } diff --git a/src/internal/components/series-marker/styles.scss b/src/internal/components/series-marker/styles.scss index 1fc136e0..c5eae079 100644 --- a/src/internal/components/series-marker/styles.scss +++ b/src/internal/components/series-marker/styles.scss @@ -18,20 +18,9 @@ $marker-margin-right: cs.$space-static-xxs; border-start-end-radius: 2px; border-end-start-radius: 2px; border-end-end-radius: 2px; - display: flex; + inline-size: $marker-size * 2; block-size: $marker-size; flex-shrink: 0; cursor: inherit; position: relative; } - -.marker-status { - position: absolute; - inset-inline-end: -3px; - inset-block-end: -3px; - inline-size: 13px; - block-size: 13px; - display: flex; - align-items: center; - justify-content: center; -} From fab61690fb1acc0539a14c1e64544ca7f8f463c3 Mon Sep 17 00:00:00 2001 From: Mael Kerichard Date: Wed, 26 Nov 2025 08:58:43 +0000 Subject: [PATCH 05/15] fix: test import --- pages/03-core/core-line-chart.page.tsx | 2 +- .../series-marker/__tests__/index.test.tsx | 18 +++++------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/pages/03-core/core-line-chart.page.tsx b/pages/03-core/core-line-chart.page.tsx index 3aef6f2a..9cda51d9 100644 --- a/pages/03-core/core-line-chart.page.tsx +++ b/pages/03-core/core-line-chart.page.tsx @@ -73,7 +73,7 @@ const series: Highcharts.SeriesOptionsType[] = [ name: "Comprehensive System Resource Utilization Measurements Over Time", type: "line", data: dataC, - status: "warning" as const, + status: "warning", }, { name: "X", diff --git a/src/internal/components/series-marker/__tests__/index.test.tsx b/src/internal/components/series-marker/__tests__/index.test.tsx index 8644c2db..557364e3 100644 --- a/src/internal/components/series-marker/__tests__/index.test.tsx +++ b/src/internal/components/series-marker/__tests__/index.test.tsx @@ -2,11 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { ComponentProps } from "react"; +import { ChartSeriesMarker } from "@lib/components/internal/components/series-marker"; import { render } from "@testing-library/react"; import { describe, expect, test } from "vitest"; -import { ChartSeriesMarker } from ".."; - describe("ChartSeriesMarker", () => { const defaultProps = { type: "line", @@ -19,25 +18,18 @@ describe("ChartSeriesMarker", () => { describe("Warning SVG display", () => { test("does not render warning SVG when status is undefined", () => { const { container } = render(); - const svgs = container.querySelectorAll("svg"); + const svgs = container.querySelector("path[aria-label='warning']"); // Should only have one SVG (the marker itself) - expect(svgs).toHaveLength(1); + expect(svgs).toBeFalsy(); }); test("renders warning SVG when status is 'warning'", () => { const { container } = render(); - const svgs = container.querySelectorAll("svg"); + const svgs = container.querySelector("path[aria-label='warning']"); // Should have two SVGs: marker + warning - expect(svgs).toHaveLength(2); - }); - - test("renders warning SVG with correct aria-label", () => { - const { container } = render(); - const warningSvg = container.querySelector("svg[aria-label='warning']"); - - expect(warningSvg).toBeTruthy(); + expect(svgs).toBeTruthy(); }); }); }); From 42308abc4514135504bbbd7f6f8a7d92bd8404c6 Mon Sep 17 00:00:00 2001 From: Mael Kerichard Date: Thu, 27 Nov 2025 16:04:35 +0000 Subject: [PATCH 06/15] chore: remove Core i18n from the public charts --- .../__snapshots__/documenter.test.ts.snap | 15 +++------------ src/core/chart-api/chart-extra-legend.tsx | 2 +- src/core/interfaces.ts | 6 ++---- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index 03263f00..91879356 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -177,11 +177,6 @@ Supported Highcharts versions: 12.", "optional": true, "type": "string", }, - { - "name": "seriesStatusWarningAriaLabel", - "optional": false, - "type": "string", - }, { "name": "xAxisRoleDescription", "optional": true, @@ -835,11 +830,6 @@ Supported Highcharts versions: 12.", "optional": true, "type": "string", }, - { - "name": "seriesStatusWarningAriaLabel", - "optional": false, - "type": "string", - }, ], "type": "object", }, @@ -1548,18 +1538,19 @@ Supported Highcharts versions: 12.", "description": "An object that contains all of the localized strings required by the component.", "i18nTag": true, "inlineType": { - "name": "CartesianI18nStrings & PieI18nStrings", + "name": "CartesianI18nStrings & PieI18nStrings & Partial<{ seriesStatusWarningAriaLabel: string; }>", "type": "union", "valueDescriptions": undefined, "values": [ "CartesianI18nStrings", "PieI18nStrings", + "Partial<{ seriesStatusWarningAriaLabel: string; }>", ], }, "name": "i18nStrings", "optional": true, "systemTags": undefined, - "type": "CartesianI18nStrings & PieI18nStrings", + "type": "CartesianI18nStrings & PieI18nStrings & Partial<{ seriesStatusWarningAriaLabel: string; }>", "visualRefreshTag": undefined, }, { diff --git a/src/core/chart-api/chart-extra-legend.tsx b/src/core/chart-api/chart-extra-legend.tsx index 6b8ef143..4b8ff502 100644 --- a/src/core/chart-api/chart-extra-legend.tsx +++ b/src/core/chart-api/chart-extra-legend.tsx @@ -114,7 +114,7 @@ export class ChartExtraLegend extends AsyncStore { visible = true, status?: ChartSeriesMarkerStatus, ): React.ReactNode { - const key = `${type}:${color}:${visible}`; + const key = `${type}:${color}:${visible}:${status}`; const marker = this.markersCache.get(key) ?? ( Date: Wed, 3 Dec 2025 17:07:03 +0000 Subject: [PATCH 07/15] feat: expose getSeriesStatus --- pages/03-core/core-line-chart.page.tsx | 3 ++- pages/types/highcharts-extension.d.ts | 10 -------- .../__snapshots__/documenter.test.ts.snap | 24 +++++++++++++++++++ src/core/chart-api/chart-extra-context.tsx | 2 ++ src/core/chart-api/chart-extra-legend.tsx | 2 +- src/core/chart-core.tsx | 2 ++ src/core/components/core-tooltip.tsx | 23 ++++++++++-------- src/core/interfaces.ts | 8 ++++++- src/core/utils.ts | 10 ++++---- types/highcharts-extension.d.ts | 10 -------- 10 files changed, 56 insertions(+), 38 deletions(-) delete mode 100644 pages/types/highcharts-extension.d.ts delete mode 100644 types/highcharts-extension.d.ts diff --git a/pages/03-core/core-line-chart.page.tsx b/pages/03-core/core-line-chart.page.tsx index 9cda51d9..cb222c97 100644 --- a/pages/03-core/core-line-chart.page.tsx +++ b/pages/03-core/core-line-chart.page.tsx @@ -70,10 +70,10 @@ const series: Highcharts.SeriesOptionsType[] = [ data: dataB, }, { + id: "A", name: "Comprehensive System Resource Utilization Measurements Over Time", type: "line", data: dataC, - status: "warning", }, { name: "X", @@ -127,6 +127,7 @@ export default function () { }, }, }} + getSeriesStatus={(s) => (s.userOptions.id === "A" ? "warning" : undefined)} chartHeight={400} tooltip={{ placement: "outside" }} getTooltipContent={() => ({ diff --git a/pages/types/highcharts-extension.d.ts b/pages/types/highcharts-extension.d.ts deleted file mode 100644 index b7201c21..00000000 --- a/pages/types/highcharts-extension.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { ChartSeriesMarkerStatus } from "../src/internal/components/series-marker"; - -declare module "highcharts" { - interface SeriesOptions { - status?: ChartSeriesMarkerStatus; - } -} diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index 91879356..8bf31ed3 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -1471,6 +1471,30 @@ If not provided, no tooltip will be displayed.", "type": "GetLegendTooltipContent", "visualRefreshTag": undefined, }, + { + "analyticsTag": undefined, + "defaultValue": undefined, + "deprecatedTag": undefined, + "description": "This property is used to provide a custom status for the series markers. +The callback function is called for each series and should return a status value.", + "i18nTag": undefined, + "inlineType": { + "name": "(series: Highcharts.Series) => "warning"", + "parameters": [ + { + "name": "series", + "type": "Highcharts.Series", + }, + ], + "returnType": ""warning"", + "type": "function", + }, + "name": "getSeriesStatus", + "optional": true, + "systemTags": undefined, + "type": "((series: Highcharts.Series) => "warning" | undefined)", + "visualRefreshTag": undefined, + }, { "analyticsTag": undefined, "defaultValue": undefined, diff --git a/src/core/chart-api/chart-extra-context.tsx b/src/core/chart-api/chart-extra-context.tsx index f1fd257d..d6fbbb55 100644 --- a/src/core/chart-api/chart-extra-context.tsx +++ b/src/core/chart-api/chart-extra-context.tsx @@ -32,6 +32,7 @@ export namespace ChartExtraContext { tooltipEnabled: boolean; keyboardNavigationEnabled: boolean; labels: ChartLabels; + getSeriesStatus: NonNullable; } export interface Handlers { @@ -63,6 +64,7 @@ export function createChartContext(): ChartExtraContext { tooltipEnabled: false, keyboardNavigationEnabled: false, labels: {}, + getSeriesStatus: () => undefined, }, handlers: {}, state: {}, diff --git a/src/core/chart-api/chart-extra-legend.tsx b/src/core/chart-api/chart-extra-legend.tsx index 4b8ff502..74c3a8ea 100644 --- a/src/core/chart-api/chart-extra-legend.tsx +++ b/src/core/chart-api/chart-extra-legend.tsx @@ -82,7 +82,7 @@ export class ChartExtraLegend extends AsyncStore { }; private initLegend = () => { - const itemSpecs = getChartLegendItems(this.context.chart()); + const itemSpecs = getChartLegendItems(this.context.chart(), this.context.settings.getSeriesStatus); const legendItems = itemSpecs.map(({ id, name, color, markerType, visible, status }) => { const marker = this.renderMarker(markerType, color, visible, status); return { id, name, marker, visible, highlighted: false }; diff --git a/src/core/chart-core.tsx b/src/core/chart-core.tsx index ad4116b4..5fd28a14 100644 --- a/src/core/chart-core.tsx +++ b/src/core/chart-core.tsx @@ -63,6 +63,7 @@ export function InternalCoreChart({ onVisibleItemsChange, visibleItems, __internalRootRef, + getSeriesStatus, ...rest }: CoreChartProps & InternalBaseComponentProps) { const highcharts = rest.highcharts as null | typeof Highcharts; @@ -74,6 +75,7 @@ export function InternalCoreChart({ tooltipEnabled: tooltipOptions?.enabled !== false, keyboardNavigationEnabled: keyboardNavigation, labels, + getSeriesStatus: getSeriesStatus ?? (() => undefined), }; const handlers = { onHighlight, onClearHighlight, onVisibleItemsChange }; const state = { visibleItems }; diff --git a/src/core/components/core-tooltip.tsx b/src/core/components/core-tooltip.tsx index 3432366f..a835f931 100644 --- a/src/core/components/core-tooltip.tsx +++ b/src/core/components/core-tooltip.tsx @@ -14,14 +14,7 @@ import { getChartSeries } from "../../internal/utils/chart-series"; import { ChartAPI } from "../chart-api"; import { getFormatter } from "../formatters"; import { BaseI18nStrings, CoreChartProps } from "../interfaces"; -import { - getPointColor, - getSeriesColor, - getSeriesId, - getSeriesMarkerType, - getSeriesStatus, - isXThreshold, -} from "../utils"; +import { getPointColor, getSeriesColor, getSeriesId, getSeriesMarkerType, isXThreshold } from "../utils"; import styles from "../styles.css.js"; @@ -158,7 +151,12 @@ function getTooltipContentCartesian( const x = group[0].x; const chart = group[0].series.chart; const getSeriesMarker = (series: Highcharts.Series) => - api.renderMarker(getSeriesMarkerType(series), getSeriesColor(series), true, getSeriesStatus(series)); + api.renderMarker( + getSeriesMarkerType(series), + getSeriesColor(series), + true, + api.context.settings.getSeriesStatus(series), + ); const matchedItems = findTooltipSeriesItems(getChartSeries(chart.series), group); const detailItems: ChartSeriesDetailItem[] = matchedItems.map((item) => { const valueFormatter = getFormatter(item.point.series.yAxis); @@ -239,7 +237,12 @@ function getTooltipContentPie( return { header: renderers.header?.(tooltipDetails) ?? (
- {api.renderMarker(getSeriesMarkerType(point.series), getPointColor(point), true, getSeriesStatus(point.series))} + {api.renderMarker( + getSeriesMarkerType(point.series), + getPointColor(point), + true, + api.context.settings.getSeriesStatus(point.series), + )} {point.name} diff --git a/src/core/interfaces.ts b/src/core/interfaces.ts index 7d919299..2518baa4 100644 --- a/src/core/interfaces.ts +++ b/src/core/interfaces.ts @@ -4,7 +4,7 @@ import type Highcharts from "highcharts"; import type * as InternalComponentTypes from "../internal/components/interfaces"; -import { ChartSeriesMarkerI18n } from "../internal/components/series-marker/interfaces"; +import { ChartSeriesMarkerI18n, ChartSeriesMarkerStatus } from "../internal/components/series-marker/interfaces"; import { type NonCancelableEventHandler } from "../internal/events"; // All charts take `highcharts` instance, that can be served statically or dynamically. @@ -387,6 +387,12 @@ export interface CoreChartProps * @i18n */ i18nStrings?: CartesianI18nStrings & PieI18nStrings & ChartSeriesMarkerI18n; + + /** + * This property is used to provide a custom status for the series markers. + * The callback function is called for each series and should return a status value. + */ + getSeriesStatus?: (series: Highcharts.Series) => ChartSeriesMarkerStatus | undefined; } export namespace CoreChartProps { diff --git a/src/core/utils.ts b/src/core/utils.ts index 999c3874..9b814e1e 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -8,7 +8,7 @@ import { ChartSeriesMarkerStatus } from "../internal/components/series-marker/in import { getChartSeries } from "../internal/utils/chart-series"; import { getFormatter } from "./formatters"; import { ChartLabels } from "./i18n-utils"; -import { Rect } from "./interfaces"; +import { CoreChartProps, Rect } from "./interfaces"; export interface LegendItemSpec { id: string; @@ -125,9 +125,6 @@ export function getSeriesColor(series?: Highcharts.Series): string { export function getPointColor(point?: Highcharts.Point): string { return typeof point?.color === "string" ? point.color : "black"; } -export function getSeriesStatus(series?: Highcharts.Series): ChartSeriesMarkerStatus | undefined { - return series?.userOptions.status; -} // The custom legend implementation does not rely on the Highcharts legend. When Highcharts legend is disabled, // the chart object does not include information on legend items. Instead, we collect series and pie segments @@ -135,7 +132,10 @@ export function getSeriesStatus(series?: Highcharts.Series): ChartSeriesMarkerSt // There exists a Highcharts APIs to access legend items, but it is unfortunately not available, when // Highcharts legend is disabled. Instead, we use this custom method to collect legend items from the chart. -export function getChartLegendItems(chart: Highcharts.Chart): readonly LegendItemSpec[] { +export function getChartLegendItems( + chart: Highcharts.Chart, + getSeriesStatus: NonNullable, +): readonly LegendItemSpec[] { const legendItems: LegendItemSpec[] = []; const addSeriesItem = (series: Highcharts.Series) => { // The pie series is not shown in the legend. Instead, we show pie segments. diff --git a/types/highcharts-extension.d.ts b/types/highcharts-extension.d.ts deleted file mode 100644 index b7201c21..00000000 --- a/types/highcharts-extension.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { ChartSeriesMarkerStatus } from "../src/internal/components/series-marker"; - -declare module "highcharts" { - interface SeriesOptions { - status?: ChartSeriesMarkerStatus; - } -} From 58ab771e74f9977f800109db6bd3548711bd2701 Mon Sep 17 00:00:00 2001 From: Mael Kerichard Date: Wed, 10 Dec 2025 10:24:29 +0000 Subject: [PATCH 08/15] chore: refactor getStatus to getItemProps --- .../cartesian-tooltip.page.tsx | 2 +- .../__snapshots__/documenter.test.ts.snap | 36 +++++++++--------- src/core/__tests__/chart-core-utils.test.tsx | 35 +++++++++++++++-- src/core/__tests__/utils.test.ts | 38 ------------------- src/core/chart-core.tsx | 2 +- .../components/series-details/index.tsx | 6 +-- 6 files changed, 53 insertions(+), 66 deletions(-) delete mode 100644 src/core/__tests__/utils.test.ts diff --git a/pages/06-visual-tests/cartesian-tooltip.page.tsx b/pages/06-visual-tests/cartesian-tooltip.page.tsx index a0d6b14a..39363bc9 100644 --- a/pages/06-visual-tests/cartesian-tooltip.page.tsx +++ b/pages/06-visual-tests/cartesian-tooltip.page.tsx @@ -31,7 +31,7 @@ const dataA = baseline.map(({ x, y }) => ({ x, y })); const dataB = baseline.map(({ x, y }, index) => ({ x, y: y + index * 10000 })); const series: Highcharts.SeriesOptionsType[] = [ - { id: 'A', name: "A", type: "spline", data: dataA }, + { id: "A", name: "A", type: "spline", data: dataA }, { name: "B", type: "spline", data: dataB }, ]; diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index 77ea1174..e45a3e50 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -1456,48 +1456,47 @@ minimum width, the horizontal scrollbar is automatically added.", "analyticsTag": undefined, "defaultValue": undefined, "deprecatedTag": undefined, - "description": "Called whenever a legend item is hovered to provide content for legend tooltip's header, body, and (optional) footer. -If not provided, no tooltip will be displayed.", + "description": "Specifies the options for each item in the chart.", "i18nTag": undefined, "inlineType": { - "name": "GetLegendTooltipContent", + "name": "(id: string) => ChartItemOptions", "parameters": [ { - "name": "props", - "type": "GetLegendTooltipContentProps", + "name": "id", + "type": "string", }, ], - "returnType": "LegendTooltipContent", + "returnType": "ChartItemOptions", "type": "function", }, - "name": "getLegendTooltipContent", + "name": "getItemProps", "optional": true, "systemTags": undefined, - "type": "GetLegendTooltipContent", + "type": "((id: string) => ChartItemOptions)", "visualRefreshTag": undefined, }, { "analyticsTag": undefined, "defaultValue": undefined, "deprecatedTag": undefined, - "description": "This property is used to provide a custom status for the series markers. -The callback function is called for each series and should return a status value.", + "description": "Called whenever a legend item is hovered to provide content for legend tooltip's header, body, and (optional) footer. +If not provided, no tooltip will be displayed.", "i18nTag": undefined, "inlineType": { - "name": "(series: Highcharts.Series) => "warning"", + "name": "GetLegendTooltipContent", "parameters": [ { - "name": "series", - "type": "Highcharts.Series", + "name": "props", + "type": "GetLegendTooltipContentProps", }, ], - "returnType": ""warning"", + "returnType": "LegendTooltipContent", "type": "function", }, - "name": "getSeriesStatus", + "name": "getLegendTooltipContent", "optional": true, "systemTags": undefined, - "type": "((series: Highcharts.Series) => "warning" | undefined)", + "type": "GetLegendTooltipContent", "visualRefreshTag": undefined, }, { @@ -1567,19 +1566,20 @@ Supported Highcharts versions: 12.", "description": "An object that contains all of the localized strings required by the component.", "i18nTag": true, "inlineType": { - "name": "CartesianI18nStrings & PieI18nStrings & CoreI18nStrings", + "name": "CartesianI18nStrings & PieI18nStrings & CoreI18nStrings & Partial<{ seriesStatusWarningAriaLabel: string; }>", "type": "union", "valueDescriptions": undefined, "values": [ "CartesianI18nStrings", "PieI18nStrings", "CoreI18nStrings", + "Partial<{ seriesStatusWarningAriaLabel: string; }>", ], }, "name": "i18nStrings", "optional": true, "systemTags": undefined, - "type": "CartesianI18nStrings & PieI18nStrings & CoreI18nStrings", + "type": "CartesianI18nStrings & PieI18nStrings & CoreI18nStrings & Partial<{ seriesStatusWarningAriaLabel: string; }>", "visualRefreshTag": undefined, }, { diff --git a/src/core/__tests__/chart-core-utils.test.tsx b/src/core/__tests__/chart-core-utils.test.tsx index 4f90854f..8a2d5c7c 100644 --- a/src/core/__tests__/chart-core-utils.test.tsx +++ b/src/core/__tests__/chart-core-utils.test.tsx @@ -7,6 +7,7 @@ import "highcharts/highcharts-more"; import "highcharts/modules/solid-gauge"; import { CoreChartProps } from "../../../lib/components/core/interfaces"; import { + fillDefaultsForGetItemProps, getChartLegendItems, getLegendsProps, getPointColor, @@ -79,7 +80,7 @@ describe("CoreChart: utils", () => { callback: (api) => (chartApi = api), }); - const items = getChartLegendItems(chartApi!.chart); + const items = getChartLegendItems(chartApi!.chart, fillDefaultsForGetItemProps(undefined)); expect(items[0].isSecondary).toBe(axisOptions.opposite); }, ); @@ -109,7 +110,7 @@ describe("CoreChart: utils", () => { callback: (api) => (chartApi = api), }); - const items = getChartLegendItems(chartApi!.chart); + const items = getChartLegendItems(chartApi!.chart, fillDefaultsForGetItemProps(undefined)); expect(items).toHaveLength(2); expect(items[0].isSecondary).toBe(false); expect(items[1].isSecondary).toBe(true); @@ -137,7 +138,7 @@ describe("CoreChart: utils", () => { callback: (api) => (chartApi = api), }); - const items = getChartLegendItems(chartApi!.chart); + const items = getChartLegendItems(chartApi!.chart, fillDefaultsForGetItemProps(undefined)); if (type === "gauge" || type === "solidgauge") { expect(items).toHaveLength(1); @@ -255,4 +256,32 @@ describe("CoreChart: utils", () => { }); }); }); + + describe("fillDefaultsForGetItemProps", () => { + describe.each([ + { + scenario: "getItemProps is undefined", + getItemProps: undefined, + id: "item1", + expected: { status: "default" }, + }, + { + scenario: "getItemProps returns empty object", + getItemProps: () => ({}), + id: "item1", + expected: { status: "default" }, + }, + { + scenario: "getItemProps returns status", + getItemProps: () => ({ status: "active" as const }), + id: "item1", + expected: { status: "active" }, + }, + ])("$scenario", ({ getItemProps, id, expected }) => { + it("should return correct default values", () => { + const result = fillDefaultsForGetItemProps(getItemProps); + expect(result(id)).toEqual(expected); + }); + }); + }); }); diff --git a/src/core/__tests__/utils.test.ts b/src/core/__tests__/utils.test.ts deleted file mode 100644 index 960afe3f..00000000 --- a/src/core/__tests__/utils.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { fillDefaultsForGetItemProps } from "../utils"; - -describe("fillDefaultsForGetItemProps", () => { - describe.each([ - { - scenario: "getItemProps is undefined", - getItemProps: undefined, - id: "item1", - expected: { status: "default" }, - }, - { - scenario: "getItemProps returns undefined", - getItemProps: () => undefined, - id: "item1", - expected: { status: "default" }, - }, - { - scenario: "getItemProps returns empty object", - getItemProps: () => ({}), - id: "item1", - expected: { status: "default" }, - }, - { - scenario: "getItemProps returns status", - getItemProps: () => ({ status: "active" as const }), - id: "item1", - expected: { status: "active" }, - }, - ])("$scenario", ({ getItemProps, id, expected }) => { - it("should return correct default values", () => { - const result = fillDefaultsForGetItemProps(getItemProps); - expect(result(id)).toEqual(expected); - }); - }); -}); diff --git a/src/core/chart-core.tsx b/src/core/chart-core.tsx index a1d8b132..3c92b595 100644 --- a/src/core/chart-core.tsx +++ b/src/core/chart-core.tsx @@ -30,7 +30,7 @@ import { VerticalAxisTitle } from "./components/core-vertical-axis-title"; import { getFormatter } from "./formatters"; import { useChartI18n } from "./i18n-utils"; import { CoreChartProps } from "./interfaces"; -import { fillDefaultsForGetItemProps, getPointAccessibleDescription, hasVisibleLegendItems } from "./utils"; +import { fillDefaultsForGetItemProps } from "./utils"; import { getLegendsProps, getPointAccessibleDescription } from "./utils"; import styles from "./styles.css.js"; diff --git a/src/internal/components/series-details/index.tsx b/src/internal/components/series-details/index.tsx index 08e35652..e7dd22b6 100644 --- a/src/internal/components/series-details/index.tsx +++ b/src/internal/components/series-details/index.tsx @@ -12,7 +12,6 @@ import { getDataAttributes } from "../../base-component/get-data-attributes"; import styles from "./styles.css.js"; import testClasses from "./test-classes/styles.css.js"; -import { ChartSeriesMarkerStatus } from "../series-marker/interfaces"; interface ChartDetailPair { key: ReactNode; @@ -25,8 +24,6 @@ interface ListItemProps { subItems?: ReadonlyArray; marker?: React.ReactNode; description?: ReactNode; - status: ChartSeriesMarkerStatus; - statusAriaDescription: string; } export interface ChartSeriesDetailItem extends ChartDetailPair { @@ -175,13 +172,12 @@ function ExpandableSeries({ ); } -function NonExpandableSeries({ itemKey, value, subItems, marker, description, status }: ListItemProps) { +function NonExpandableSeries({ itemKey, value, subItems, marker, description }: ListItemProps) { return ( <>
{marker} - warning status {itemKey}
{value} From 9da5a512ddd9d4427fc20fdd32d2d4f3d2540c1f Mon Sep 17 00:00:00 2001 From: Mael Kerichard Date: Mon, 15 Dec 2025 15:28:19 +0000 Subject: [PATCH 09/15] a11y: adds aria description to markers --- package-lock.json | 76 +++---------------- pages/03-core/core-legend.page.tsx | 35 +-------- pages/03-core/core-line-chart.page.tsx | 4 + pages/03-core/events-sync-demo.page.tsx | 2 +- pages/03-core/marker-permutations.page.tsx | 2 +- src/core/__tests__/chart-core-utils.test.tsx | 32 ++++++-- src/core/chart-api/chart-extra-legend.tsx | 13 ++-- src/core/chart-core.tsx | 7 +- src/core/components/core-tooltip.tsx | 11 ++- src/core/i18n-utils.tsx | 3 +- src/core/interfaces.ts | 9 +++ src/core/utils.ts | 35 ++++++++- .../series-marker/__tests__/index.test.tsx | 19 +++-- .../components/series-marker/index.tsx | 17 ++--- .../components/series-marker/styles.scss | 12 +++ 15 files changed, 139 insertions(+), 138 deletions(-) diff --git a/package-lock.json b/package-lock.json index 554d8410..513bfa45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -783,7 +783,6 @@ "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1093,7 +1092,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1117,7 +1115,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1188,7 +1185,6 @@ "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -3968,7 +3964,6 @@ "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4096,7 +4091,6 @@ "integrity": "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/types": "8.33.1", @@ -4561,7 +4555,6 @@ "integrity": "sha512-4bEnqoHr676x4hyq7yOp+V+wVgclisNeOwMyLPEIJOv+cAAxESzIOdFyiQcbAu7gq+HUIuoWMZGlV9UgDnXh1w==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=18.20.0" }, @@ -4576,7 +4569,6 @@ "integrity": "sha512-3IkaissyOsUQwg8IinkVm1svsvRMGJpFyaSiEhQ0oQXD7mnWrNVFSU9kmeFvbKAtoc4j60FRjU6XqtH94xRceg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chalk": "^5.1.2", "loglevel": "^1.6.0", @@ -4761,7 +4753,6 @@ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5471,7 +5462,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -6768,8 +6758,7 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1452169.tgz", "integrity": "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/diff-sequences": { "version": "29.6.3", @@ -7456,7 +7445,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7513,7 +7501,6 @@ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9194,8 +9181,7 @@ "version": "12.2.0", "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-12.2.0.tgz", "integrity": "sha512-UUN+osTP3aeGc4KmoMuWAjzpKif8GYHFozzYI4O8h1ILGof25M/ZGBpXLvgqf1z0LVh7N9eG7i0HnzMfjcR4nA==", - "license": "https://www.highcharts.com/license", - "peer": true + "license": "https://www.highcharts.com/license" }, "node_modules/highcharts-react-official": { "version": "3.2.2", @@ -10505,7 +10491,6 @@ "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "abab": "^2.0.6", "acorn": "^8.8.1", @@ -12644,7 +12629,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12840,7 +12824,6 @@ "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -12872,7 +12855,6 @@ "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -13085,7 +13067,6 @@ "integrity": "sha512-CnzhOgrZj8DvkDqI+Yx+9or33i3Y9uUYbKyYpP4C13jWwXx/keQ38RMTMmxuLCWQlxjZrOH0Foq7P2fGP7adDQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@puppeteer/browsers": "2.10.5", "chromium-bidi": "5.1.0", @@ -13151,7 +13132,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -13165,7 +13145,6 @@ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -14873,7 +14852,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", @@ -15455,7 +15433,6 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15710,7 +15687,6 @@ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15891,7 +15867,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -16012,7 +15987,6 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -16026,7 +16000,6 @@ "integrity": "sha512-VZ40MBnlE1/V5uTgdqY3DmjUgZtIzsYq758JGlyQrv5syIsaYcabkfPkEuWML49Ph0D/SoqpVFd0dyVTr551oA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.1", @@ -16247,7 +16220,6 @@ "integrity": "sha512-910g6ktwXdAKGyhgCPGw9BzIKOEBBYMFN1bLwC3bW/3mFlxGHO/n70c7Sg9hrsu9VWTzv6m+1Clf27B9uz4a/Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "^20.11.30", "@types/sinonjs__fake-timers": "^8.1.5", @@ -17516,7 +17488,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "dev": true, - "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -17714,15 +17685,13 @@ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", "dev": true, - "peer": true, "requires": {} }, "@csstools/css-tokenizer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", - "dev": true, - "peer": true + "dev": true }, "@csstools/media-query-list-parser": { "version": "4.0.3", @@ -17752,7 +17721,6 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "dev": true, - "peer": true, "requires": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -19484,7 +19452,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", "dev": true, - "peer": true, "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -19586,7 +19553,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.1.tgz", "integrity": "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==", "dev": true, - "peer": true, "requires": { "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/types": "8.33.1", @@ -19865,7 +19831,6 @@ "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-9.15.0.tgz", "integrity": "sha512-4bEnqoHr676x4hyq7yOp+V+wVgclisNeOwMyLPEIJOv+cAAxESzIOdFyiQcbAu7gq+HUIuoWMZGlV9UgDnXh1w==", "dev": true, - "peer": true, "requires": { "expect-webdriverio": "^5.1.0", "webdriverio": "9.15.0" @@ -19876,7 +19841,6 @@ "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.15.0.tgz", "integrity": "sha512-3IkaissyOsUQwg8IinkVm1svsvRMGJpFyaSiEhQ0oQXD7mnWrNVFSU9kmeFvbKAtoc4j60FRjU6XqtH94xRceg==", "dev": true, - "peer": true, "requires": { "chalk": "^5.1.2", "loglevel": "^1.6.0", @@ -20016,8 +19980,7 @@ "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, - "peer": true + "dev": true }, "acorn-globals": { "version": "7.0.1", @@ -20472,7 +20435,6 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", "dev": true, - "peer": true, "requires": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -21342,8 +21304,7 @@ "version": "0.0.1452169", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1452169.tgz", "integrity": "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==", - "dev": true, - "peer": true + "dev": true }, "diff-sequences": { "version": "29.6.3", @@ -21827,7 +21788,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "dev": true, - "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -21944,7 +21904,6 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, - "peer": true, "requires": {} }, "eslint-import-resolver-node": { @@ -22970,8 +22929,7 @@ "highcharts": { "version": "12.2.0", "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-12.2.0.tgz", - "integrity": "sha512-UUN+osTP3aeGc4KmoMuWAjzpKif8GYHFozzYI4O8h1ILGof25M/ZGBpXLvgqf1z0LVh7N9eG7i0HnzMfjcR4nA==", - "peer": true + "integrity": "sha512-UUN+osTP3aeGc4KmoMuWAjzpKif8GYHFozzYI4O8h1ILGof25M/ZGBpXLvgqf1z0LVh7N9eG7i0HnzMfjcR4nA==" }, "highcharts-react-official": { "version": "3.2.2", @@ -23824,7 +23782,6 @@ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", "dev": true, - "peer": true, "requires": { "abab": "^2.0.6", "acorn": "^8.8.1", @@ -25278,7 +25235,6 @@ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", "dev": true, - "peer": true, "requires": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -25387,7 +25343,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, - "peer": true, "requires": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -25409,8 +25364,7 @@ "version": "3.5.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", - "dev": true, - "peer": true + "dev": true }, "prettier-linter-helpers": { "version": "1.0.0", @@ -25563,7 +25517,6 @@ "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.10.2.tgz", "integrity": "sha512-CnzhOgrZj8DvkDqI+Yx+9or33i3Y9uUYbKyYpP4C13jWwXx/keQ38RMTMmxuLCWQlxjZrOH0Foq7P2fGP7adDQ==", "dev": true, - "peer": true, "requires": { "@puppeteer/browsers": "2.10.5", "chromium-bidi": "5.1.0", @@ -25601,7 +25554,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "requires": { "loose-envify": "^1.1.0" } @@ -25611,7 +25563,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, - "peer": true, "requires": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -26765,7 +26716,6 @@ "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.20.0.tgz", "integrity": "sha512-B5Myu9WRxrgKuLs3YyUXLP2H0mrbejwNxPmyADlACWwFsrL8Bmor/nTSh4OMae5sHjOz6gkSeccQH34gM4/nAw==", "dev": true, - "peer": true, "requires": { "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", @@ -27173,8 +27123,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "peer": true + "dev": true } } }, @@ -27345,8 +27294,7 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "peer": true + "dev": true }, "unbox-primitive": { "version": "1.1.0", @@ -27464,7 +27412,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, - "peer": true, "requires": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -27486,8 +27433,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "peer": true + "dev": true } } }, @@ -27517,7 +27463,6 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.1.tgz", "integrity": "sha512-VZ40MBnlE1/V5uTgdqY3DmjUgZtIzsYq758JGlyQrv5syIsaYcabkfPkEuWML49Ph0D/SoqpVFd0dyVTr551oA==", "dev": true, - "peer": true, "requires": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.1", @@ -27655,7 +27600,6 @@ "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.15.0.tgz", "integrity": "sha512-910g6ktwXdAKGyhgCPGw9BzIKOEBBYMFN1bLwC3bW/3mFlxGHO/n70c7Sg9hrsu9VWTzv6m+1Clf27B9uz4a/Q==", "dev": true, - "peer": true, "requires": { "@types/node": "^20.11.30", "@types/sinonjs__fake-timers": "^8.1.5", diff --git a/pages/03-core/core-legend.page.tsx b/pages/03-core/core-legend.page.tsx index 05da518c..d5376d1b 100644 --- a/pages/03-core/core-legend.page.tsx +++ b/pages/03-core/core-legend.page.tsx @@ -19,10 +19,6 @@ import { PageSettingsForm, useChartSettings } from "../common/page-settings"; import { Page } from "../common/templates"; import pseudoRandom from "../utils/pseudo-random"; -const i18nStrings = { - seriesStatusWarningAriaLabel: "warning", -}; - function randomInt(min: number, max: number) { return min + Math.floor(pseudoRandom() * (max - min)); } @@ -109,45 +105,21 @@ const initialLegendItems: readonly LegendItem[] = [ { id: "CPU Utilization", name: "CPU Utilization", - marker: ( - - ), + marker: , visible: true, highlighted: false, }, { id: "Memory Usage", name: "Memory Usage", - marker: ( - - ), + marker: , visible: true, highlighted: false, }, { id: "Storage Capacity", name: "Storage Capacity", - marker: ( - - ), + marker: , visible: true, highlighted: false, }, @@ -211,7 +183,6 @@ export default function () { marker: ( ({ id: s.name, name: s.name, - marker: , + marker: , visible: visibleItems.has(s.name), highlighted: highlightedItem === s.name, })); diff --git a/pages/03-core/marker-permutations.page.tsx b/pages/03-core/marker-permutations.page.tsx index a7bebe36..f8fefaf0 100644 --- a/pages/03-core/marker-permutations.page.tsx +++ b/pages/03-core/marker-permutations.page.tsx @@ -53,7 +53,7 @@ const permutationsForColors = [ "triangle-down", ], color: [color], - i18nStrings: [{ seriesStatusWarningAriaLabel: "warning" }], + markerAriaDescription: ["aria"], status: ["default"], }, ]), diff --git a/src/core/__tests__/chart-core-utils.test.tsx b/src/core/__tests__/chart-core-utils.test.tsx index 8a2d5c7c..7f006ee0 100644 --- a/src/core/__tests__/chart-core-utils.test.tsx +++ b/src/core/__tests__/chart-core-utils.test.tsx @@ -263,23 +263,43 @@ describe("CoreChart: utils", () => { scenario: "getItemProps is undefined", getItemProps: undefined, id: "item1", - expected: { status: "default" }, + expected: { status: "default", markerAriaDescription: undefined }, }, { scenario: "getItemProps returns empty object", getItemProps: () => ({}), id: "item1", - expected: { status: "default" }, + expected: { status: "default", markerAriaDescription: undefined }, }, { scenario: "getItemProps returns status", - getItemProps: () => ({ status: "active" as const }), + getItemProps: () => ({ status: "warning" as const }), id: "item1", - expected: { status: "active" }, + expected: { status: "warning", markerAriaDescription: undefined }, }, - ])("$scenario", ({ getItemProps, id, expected }) => { + { + scenario: "getItemProps returns status and contains i18n", + getItemProps: () => ({ status: "warning" as const }), + id: "item1", + expected: { status: "warning", markerAriaDescription: "hello hi" }, + options: { + markerAriaDescriptionTemplate: "hello {status}", + getI18nFromStatus: (): string => "hi", + }, + }, + { + scenario: "getItemProps returns status and contains i18n - getI18nFromStatus returns undefined", + getItemProps: () => ({ status: "warning" as const }), + id: "item1", + expected: { status: "warning", markerAriaDescription: undefined }, + options: { + markerAriaDescriptionTemplate: "hello {status}", + getI18nFromStatus: (): undefined => undefined, + }, + }, + ])("$scenario", ({ getItemProps, id, expected, options }) => { it("should return correct default values", () => { - const result = fillDefaultsForGetItemProps(getItemProps); + const result = fillDefaultsForGetItemProps(getItemProps, options); expect(result(id)).toEqual(expected); }); }); diff --git a/src/core/chart-api/chart-extra-legend.tsx b/src/core/chart-api/chart-extra-legend.tsx index 00c176fd..f6c80678 100644 --- a/src/core/chart-api/chart-extra-legend.tsx +++ b/src/core/chart-api/chart-extra-legend.tsx @@ -85,10 +85,12 @@ export class ChartExtraLegend extends AsyncStore { private initLegend = () => { const prevState = this.get().items.reduce((map, item) => map.set(item.id, item), new Map()); const itemSpecs = getChartLegendItems(this.context.chart(), this.context.settings.getItemProps); - const legendItems = itemSpecs.map(({ id, name, color, markerType, visible, status, isSecondary }) => { - const marker = this.renderMarker(markerType, color, visible, status); - return { id, name, marker, visible, isSecondary, highlighted: prevState.get(id)?.highlighted ?? false }; - }); + const legendItems = itemSpecs.map( + ({ id, name, color, markerType, visible, status, isSecondary, markerAriaDescription }) => { + const marker = this.renderMarker(markerType, color, visible, status, markerAriaDescription); + return { id, name, marker, visible, isSecondary, highlighted: prevState.get(id)?.highlighted ?? false }; + }, + ); this.updateLegendItems(legendItems); }; @@ -115,6 +117,7 @@ export class ChartExtraLegend extends AsyncStore { color: string, visible = true, status: ChartSeriesMarkerStatus, + markerAriaDescription?: string, ): React.ReactNode { const key = `${type}:${color}:${visible}:${status}`; const marker = this.markersCache.get(key) ?? ( @@ -123,7 +126,7 @@ export class ChartExtraLegend extends AsyncStore { color={color} visible={visible} status={status} - i18nStrings={this.context.settings.labels} + markerAriaDescription={markerAriaDescription} /> ); this.markersCache.set(key, marker); diff --git a/src/core/chart-core.tsx b/src/core/chart-core.tsx index 3c92b595..955d4ddf 100644 --- a/src/core/chart-core.tsx +++ b/src/core/chart-core.tsx @@ -30,7 +30,7 @@ import { VerticalAxisTitle } from "./components/core-vertical-axis-title"; import { getFormatter } from "./formatters"; import { useChartI18n } from "./i18n-utils"; import { CoreChartProps } from "./interfaces"; -import { fillDefaultsForGetItemProps } from "./utils"; +import { fillDefaultsForGetItemProps, i18nStatus } from "./utils"; import { getLegendsProps, getPointAccessibleDescription } from "./utils"; import styles from "./styles.css.js"; @@ -80,7 +80,10 @@ export function InternalCoreChart({ tooltipEnabled: tooltipOptions?.enabled !== false, keyboardNavigationEnabled: keyboardNavigation, labels, - getItemProps: fillDefaultsForGetItemProps(getItemProps), + getItemProps: fillDefaultsForGetItemProps(getItemProps, { + markerAriaDescriptionTemplate: i18nStrings?.chartMarkerAriaDescriptionTemplate, + getI18nFromStatus: i18nStatus(i18nStrings), + }), }; const handlers = { onHighlight, onClearHighlight, onVisibleItemsChange }; const state = { visibleItems }; diff --git a/src/core/components/core-tooltip.tsx b/src/core/components/core-tooltip.tsx index 6f9721c7..91c4b923 100644 --- a/src/core/components/core-tooltip.tsx +++ b/src/core/components/core-tooltip.tsx @@ -161,13 +161,16 @@ function getTooltipContentCartesian( // By design, every point of the group has the same x value. const x = group[0].x; const chart = group[0].series.chart; - const getSeriesMarker = (series: Highcharts.Series) => - api.renderMarker( + const getSeriesMarker = (series: Highcharts.Series) => { + const itemProps = api.context.settings.getItemProps(getSeriesId(series)); + return api.renderMarker( getSeriesMarkerType(series), getSeriesColor(series), true, - api.context.settings.getItemProps(getSeriesId(series)).status, + itemProps.status, + itemProps.markerAriaDescription, ); + }; const matchedItems = findTooltipSeriesItems(getChartSeries(chart.series), group, seriesSorting); const detailItems: ChartSeriesDetailItem[] = matchedItems.map((item) => { const valueFormatter = getFormatter(item.point.series.yAxis); @@ -185,6 +188,7 @@ function getTooltipContentCartesian( subItems: customContent?.subItems, expandableId: customContent?.expandable ? item.point.series.name : undefined, highlighted: item.point.x === point?.x && item.point.y === point?.y, + itemAriaLabel: "awd", description: customContent?.description === undefined && item.errorRanges.length ? ( <> @@ -253,6 +257,7 @@ function getTooltipContentPie( getPointColor(point), true, api.context.settings.getItemProps(getPointId(point)).status, + api.context.settings.getItemProps(getPointId(point)).markerAriaDescription, )} {point.name} diff --git a/src/core/i18n-utils.tsx b/src/core/i18n-utils.tsx index 99ffd2a1..70a225fd 100644 --- a/src/core/i18n-utils.tsx +++ b/src/core/i18n-utils.tsx @@ -3,10 +3,9 @@ import { useInternalI18n } from "@cloudscape-design/components/internal/do-not-use/i18n"; -import { ChartSeriesMarkerI18n } from "../internal/components/series-marker/interfaces"; import { CoreChartProps } from "./interfaces"; -export interface ChartLabels extends ChartSeriesMarkerI18n { +export interface ChartLabels { chartLabel?: string; chartDescription?: string; chartContainerLabel?: string; diff --git a/src/core/interfaces.ts b/src/core/interfaces.ts index aeba39e5..55807734 100644 --- a/src/core/interfaces.ts +++ b/src/core/interfaces.ts @@ -136,6 +136,15 @@ export interface CoreI18nStrings extends BaseI18nStrings { * this property to be explicitly provided. */ secondaryLegendAriaLabel?: string; + + /** + * @example "Series with status {status}". + */ + chartMarkerAriaDescriptionTemplate?: string; + /** + * @example "warning". + */ + chartItemStatusWarning?: string; } export interface WithCartesianI18nStrings { diff --git a/src/core/utils.ts b/src/core/utils.ts index cc9ae521..6d4bcbf2 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -19,6 +19,7 @@ export interface LegendItemSpec { visible: boolean; isSecondary: boolean; status: ChartSeriesMarkerStatus; + markerAriaDescription?: string; } // The below functions extract unique identifier from series, point, or options. The identifier can be item's ID or name. @@ -154,6 +155,7 @@ export function getChartLegendItems( // The same is not supported for pie chart segments. if (series.options.showInLegend !== false) { const seriesId = getSeriesId(series); + const itemProps = getItemProps(seriesId); legendItems.push({ id: seriesId, name: series.name, @@ -161,7 +163,8 @@ export function getChartLegendItems( color: getSeriesColor(series), visible: series.visible, isSecondary, - status: getItemProps(seriesId).status, + status: itemProps.status, + markerAriaDescription: itemProps.markerAriaDescription, }); } }; @@ -402,6 +405,11 @@ function getChartRect(rect: Rect, chart: Highcharts.Chart, canBeInverted: boolea }; } +export interface FillDefaultsForGetItemPropsOptions { + markerAriaDescriptionTemplate?: string; + getI18nFromStatus?: (status: ChartSeriesMarkerStatus) => string | undefined; +} + /** * Creates a function that returns chart item properties with default values applied. * @@ -410,11 +418,32 @@ function getChartRect(rect: Rect, chart: Highcharts.Chart, canBeInverted: boolea */ export function fillDefaultsForGetItemProps( getItemProps: CoreChartProps["getItemProps"], -): (id: string) => Required { + options?: FillDefaultsForGetItemPropsOptions, +): (id: string) => Required & { + markerAriaDescription?: string; +} { return (id: string) => { const prevItem = getItemProps?.(id) ?? {}; + const status = prevItem.status ?? "default"; + const statusI18n = options?.getI18nFromStatus?.(status); + return { - status: prevItem.status ?? "default", + status, + markerAriaDescription: + statusI18n !== undefined ? options?.markerAriaDescriptionTemplate?.replace("{status}", statusI18n) : undefined, }; }; } + +export function i18nStatus(i18n: CoreChartProps["i18nStrings"]) { + return (status: ChartSeriesMarkerStatus) => { + switch (status) { + case "warning": + return i18n?.chartItemStatusWarning; + case "default": + return undefined; + default: + throw status satisfies never; + } + }; +} diff --git a/src/internal/components/series-marker/__tests__/index.test.tsx b/src/internal/components/series-marker/__tests__/index.test.tsx index 557364e3..c10f2a77 100644 --- a/src/internal/components/series-marker/__tests__/index.test.tsx +++ b/src/internal/components/series-marker/__tests__/index.test.tsx @@ -1,8 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { ComponentProps } from "react"; import { ChartSeriesMarker } from "@lib/components/internal/components/series-marker"; +import { ChartSeriesMarkerProps } from "@lib/components/internal/components/series-marker"; import { render } from "@testing-library/react"; import { describe, expect, test } from "vitest"; @@ -10,15 +10,13 @@ describe("ChartSeriesMarker", () => { const defaultProps = { type: "line", color: "#0073bb", - i18nStrings: { - seriesStatusWarningAriaLabel: "warning", - }, - } satisfies ComponentProps; + markerAriaDescription: "This is a description", + } satisfies ChartSeriesMarkerProps; describe("Warning SVG display", () => { test("does not render warning SVG when status is undefined", () => { const { container } = render(); - const svgs = container.querySelector("path[aria-label='warning']"); + const svgs = container.querySelector("path[data-testid='warning']"); // Should only have one SVG (the marker itself) expect(svgs).toBeFalsy(); @@ -26,10 +24,17 @@ describe("ChartSeriesMarker", () => { test("renders warning SVG when status is 'warning'", () => { const { container } = render(); - const svgs = container.querySelector("path[aria-label='warning']"); + const svgs = container.querySelector("path[data-testid='warning']"); // Should have two SVGs: marker + warning expect(svgs).toBeTruthy(); }); + + test("renders aria description when provided", () => { + const { container } = render(); + const span = container.querySelector("span"); + + expect(span!.textContent).toBe("This is a description"); + }); }); }); diff --git a/src/internal/components/series-marker/index.tsx b/src/internal/components/series-marker/index.tsx index b8b093b2..b0018776 100644 --- a/src/internal/components/series-marker/index.tsx +++ b/src/internal/components/series-marker/index.tsx @@ -21,16 +21,12 @@ export type ChartSeriesMarkerType = | "triangle-down" | "circle"; -export type ChartSeriesMarkerI18n = Partial<{ - seriesStatusWarningAriaLabel: string; -}>; - export interface ChartSeriesMarkerProps extends BaseComponentProps { type: ChartSeriesMarkerType; color: string; visible?: boolean; status: ChartSeriesMarkerStatus; - i18nStrings: ChartSeriesMarkerI18n; + markerAriaDescription?: string; } function scale(size: number, value: number) { @@ -42,7 +38,7 @@ export function ChartSeriesMarker({ color, visible = true, status, - i18nStrings, + markerAriaDescription, }: ChartSeriesMarkerProps) { color = visible ? color : colorTextInteractiveDisabled; @@ -52,6 +48,7 @@ export function ChartSeriesMarker({ return (
+ {markerAriaDescription && {markerAriaDescription}} @@ -61,7 +58,7 @@ export function ChartSeriesMarker({ - {status === "warning" && } + {status === "warning" && } {type === "line" && } @@ -133,12 +130,12 @@ function SVGCircle({ color, maskId }: { color: string; maskId: string }) { return ; } -const SVGWarningTranslate = `translate(9, 1)`; +const SVGWarningTranslate = `translate(10, 1)`; -function SVGWarning({ ariaLabel }: { ariaLabel: string }) { +function SVGWarning() { return (
{markerAriaDescription && {markerAriaDescription}} - + diff --git a/src/internal/components/series-marker/styles.scss b/src/internal/components/series-marker/styles.scss index d79f0513..e84b7a47 100644 --- a/src/internal/components/series-marker/styles.scss +++ b/src/internal/components/series-marker/styles.scss @@ -13,12 +13,11 @@ $marker-margin-right: cs.$space-static-xxs; .marker { @include styles.styles-reset; margin-block-start: $marker-margin-top; - margin-inline-end: $marker-margin-right; border-start-start-radius: 2px; border-start-end-radius: 2px; border-end-start-radius: 2px; border-end-end-radius: 2px; - inline-size: $marker-size * 1.5; + inline-size: 25px; block-size: $marker-size; flex-shrink: 0; cursor: inherit; From f38f47867dde8585328f6bc7395a061bfe94222f Mon Sep 17 00:00:00 2001 From: Mael Kerichard Date: Fri, 19 Dec 2025 10:00:55 +0000 Subject: [PATCH 11/15] chore: move interfaces around and rename getItemProps --- pages/03-core/core-line-chart.page.tsx | 4 +-- .../cartesian-tooltip.page.tsx | 4 +-- pages/06-visual-tests/column-hover.page.tsx | 4 +-- pages/06-visual-tests/pie-tooltip.page.tsx | 4 +-- .../__snapshots__/documenter.test.ts.snap | 12 +++---- src/core/__tests__/chart-core-legend.test.tsx | 4 +-- src/core/__tests__/chart-core-utils.test.tsx | 34 +++++++++---------- src/core/chart-api/chart-extra-context.tsx | 6 ++-- src/core/chart-api/chart-extra-legend.tsx | 2 +- src/core/chart-core.tsx | 9 ++--- src/core/components/core-tooltip.tsx | 6 ++-- src/core/interfaces.ts | 28 +++++++++------ src/core/utils.ts | 24 ++++++------- 13 files changed, 72 insertions(+), 69 deletions(-) diff --git a/pages/03-core/core-line-chart.page.tsx b/pages/03-core/core-line-chart.page.tsx index 60b05bb2..d8dd0bbd 100644 --- a/pages/03-core/core-line-chart.page.tsx +++ b/pages/03-core/core-line-chart.page.tsx @@ -131,8 +131,8 @@ export default function () { }, }, }} - getItemProps={(id) => ({ - status: id === "A" ? "warning" : "default", + getItemOptions={({ itemId }) => ({ + status: itemId === "A" ? "warning" : "default", })} chartHeight={400} getTooltipContent={() => ({ diff --git a/pages/06-visual-tests/cartesian-tooltip.page.tsx b/pages/06-visual-tests/cartesian-tooltip.page.tsx index 39363bc9..59864318 100644 --- a/pages/06-visual-tests/cartesian-tooltip.page.tsx +++ b/pages/06-visual-tests/cartesian-tooltip.page.tsx @@ -49,8 +49,8 @@ export default function () { }} chartHeight={400} tooltip={{ placement: "outside" }} - getItemProps={(id) => ({ - status: id === "A" ? "warning" : "default", + getItemOptions={({ itemId }) => ({ + status: itemId === "A" ? "warning" : "default", })} getTooltipContent={() => ({ footer() { diff --git a/pages/06-visual-tests/column-hover.page.tsx b/pages/06-visual-tests/column-hover.page.tsx index 63ae1463..3b6807c5 100644 --- a/pages/06-visual-tests/column-hover.page.tsx +++ b/pages/06-visual-tests/column-hover.page.tsx @@ -76,8 +76,8 @@ function Chart({ type }: { type: "single" | "stacked" | "grouped" }) { ], yAxis: [{ title: { text: "Error count" } }], }} - getItemProps={(id) => ({ - status: id === "Severe" ? "warning" : "default", + getItemOptions={({ itemId }) => ({ + status: itemId === "Severe" ? "warning" : "default", })} callback={(api) => { setTimeout(() => { diff --git a/pages/06-visual-tests/pie-tooltip.page.tsx b/pages/06-visual-tests/pie-tooltip.page.tsx index bb355550..219510a7 100644 --- a/pages/06-visual-tests/pie-tooltip.page.tsx +++ b/pages/06-visual-tests/pie-tooltip.page.tsx @@ -31,8 +31,8 @@ export default function () { options={{ series: series, }} - getItemProps={(id) => ({ - status: id === "Failed" ? "warning" : "default", + getItemOptions={({ itemId }) => ({ + status: itemId === "Failed" ? "warning" : "default", })} chartHeight={400} getTooltipContent={() => ({ diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index ef29dc63..5e6678b7 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -1459,20 +1459,20 @@ minimum width, the horizontal scrollbar is automatically added.", "description": "Specifies the options for each item in the chart.", "i18nTag": undefined, "inlineType": { - "name": "(id: string) => ChartItemOptions", + "name": "(props: CoreChartProps.GetItemOptionsProps) => CoreChartProps.ChartItemOptions", "parameters": [ { - "name": "id", - "type": "string", + "name": "props", + "type": "CoreChartProps.GetItemOptionsProps", }, ], - "returnType": "ChartItemOptions", + "returnType": "CoreChartProps.ChartItemOptions", "type": "function", }, - "name": "getItemProps", + "name": "getItemOptions", "optional": true, "systemTags": undefined, - "type": "((id: string) => ChartItemOptions)", + "type": "((props: CoreChartProps.GetItemOptionsProps) => CoreChartProps.ChartItemOptions)", "visualRefreshTag": undefined, }, { diff --git a/src/core/__tests__/chart-core-legend.test.tsx b/src/core/__tests__/chart-core-legend.test.tsx index 0bb744ff..3bf15b14 100644 --- a/src/core/__tests__/chart-core-legend.test.tsx +++ b/src/core/__tests__/chart-core-legend.test.tsx @@ -562,8 +562,8 @@ describe("CoreChart: legend", () => { }, ], }, - getItemProps: (id) => ({ - status: id === "L1" ? "warning" : "default", + getItemOptions: ({ itemId }) => ({ + status: itemId === "L1" ? "warning" : "default", }), }); diff --git a/src/core/__tests__/chart-core-utils.test.tsx b/src/core/__tests__/chart-core-utils.test.tsx index 7f006ee0..f582134f 100644 --- a/src/core/__tests__/chart-core-utils.test.tsx +++ b/src/core/__tests__/chart-core-utils.test.tsx @@ -7,7 +7,7 @@ import "highcharts/highcharts-more"; import "highcharts/modules/solid-gauge"; import { CoreChartProps } from "../../../lib/components/core/interfaces"; import { - fillDefaultsForGetItemProps, + fillDefaultsForgetItemOptions, getChartLegendItems, getLegendsProps, getPointColor, @@ -80,7 +80,7 @@ describe("CoreChart: utils", () => { callback: (api) => (chartApi = api), }); - const items = getChartLegendItems(chartApi!.chart, fillDefaultsForGetItemProps(undefined)); + const items = getChartLegendItems(chartApi!.chart, fillDefaultsForgetItemOptions(undefined)); expect(items[0].isSecondary).toBe(axisOptions.opposite); }, ); @@ -110,7 +110,7 @@ describe("CoreChart: utils", () => { callback: (api) => (chartApi = api), }); - const items = getChartLegendItems(chartApi!.chart, fillDefaultsForGetItemProps(undefined)); + const items = getChartLegendItems(chartApi!.chart, fillDefaultsForgetItemOptions(undefined)); expect(items).toHaveLength(2); expect(items[0].isSecondary).toBe(false); expect(items[1].isSecondary).toBe(true); @@ -138,7 +138,7 @@ describe("CoreChart: utils", () => { callback: (api) => (chartApi = api), }); - const items = getChartLegendItems(chartApi!.chart, fillDefaultsForGetItemProps(undefined)); + const items = getChartLegendItems(chartApi!.chart, fillDefaultsForgetItemOptions(undefined)); if (type === "gauge" || type === "solidgauge") { expect(items).toHaveLength(1); @@ -257,29 +257,29 @@ describe("CoreChart: utils", () => { }); }); - describe("fillDefaultsForGetItemProps", () => { + describe("fillDefaultsForgetItemOptions", () => { describe.each([ { - scenario: "getItemProps is undefined", - getItemProps: undefined, + scenario: "getItemOptions is undefined", + getItemOptions: undefined, id: "item1", expected: { status: "default", markerAriaDescription: undefined }, }, { - scenario: "getItemProps returns empty object", - getItemProps: () => ({}), + scenario: "getItemOptions returns empty object", + getItemOptions: () => ({}), id: "item1", expected: { status: "default", markerAriaDescription: undefined }, }, { - scenario: "getItemProps returns status", - getItemProps: () => ({ status: "warning" as const }), + scenario: "getItemOptions returns status", + getItemOptions: () => ({ status: "warning" as const }), id: "item1", expected: { status: "warning", markerAriaDescription: undefined }, }, { - scenario: "getItemProps returns status and contains i18n", - getItemProps: () => ({ status: "warning" as const }), + scenario: "getItemOptions returns status and contains i18n", + getItemOptions: () => ({ status: "warning" as const }), id: "item1", expected: { status: "warning", markerAriaDescription: "hello hi" }, options: { @@ -288,8 +288,8 @@ describe("CoreChart: utils", () => { }, }, { - scenario: "getItemProps returns status and contains i18n - getI18nFromStatus returns undefined", - getItemProps: () => ({ status: "warning" as const }), + scenario: "getItemOptions returns status and contains i18n - getI18nFromStatus returns undefined", + getItemOptions: () => ({ status: "warning" as const }), id: "item1", expected: { status: "warning", markerAriaDescription: undefined }, options: { @@ -297,9 +297,9 @@ describe("CoreChart: utils", () => { getI18nFromStatus: (): undefined => undefined, }, }, - ])("$scenario", ({ getItemProps, id, expected, options }) => { + ])("$scenario", ({ getItemOptions, id, expected, options }) => { it("should return correct default values", () => { - const result = fillDefaultsForGetItemProps(getItemProps, options); + const result = fillDefaultsForgetItemOptions(getItemOptions, options); expect(result(id)).toEqual(expected); }); }); diff --git a/src/core/chart-api/chart-extra-context.tsx b/src/core/chart-api/chart-extra-context.tsx index b3b24a1b..4c76b61a 100644 --- a/src/core/chart-api/chart-extra-context.tsx +++ b/src/core/chart-api/chart-extra-context.tsx @@ -8,7 +8,7 @@ import { getChartSeries } from "../../internal/utils/chart-series"; import { getSeriesData } from "../../internal/utils/series-data"; import { ChartLabels } from "../i18n-utils"; import { CoreChartProps, Rect } from "../interfaces"; -import { fillDefaultsForGetItemProps, getGroupRect, isSeriesStacked } from "../utils"; +import { fillDefaultsForgetItemOptions, getGroupRect, isSeriesStacked } from "../utils"; // Chart API context is used for dependency injection for chart utilities. // It is initialized on chart render, and includes the chart instance, consumer @@ -32,7 +32,7 @@ export namespace ChartExtraContext { tooltipEnabled: boolean; keyboardNavigationEnabled: boolean; labels: ChartLabels; - getItemProps: ReturnType; + getItemOptions: ReturnType; } export interface Handlers { @@ -64,7 +64,7 @@ export function createChartContext(): ChartExtraContext { tooltipEnabled: false, keyboardNavigationEnabled: false, labels: {}, - getItemProps: fillDefaultsForGetItemProps(undefined), + getItemOptions: fillDefaultsForgetItemOptions(undefined), }, handlers: {}, state: {}, diff --git a/src/core/chart-api/chart-extra-legend.tsx b/src/core/chart-api/chart-extra-legend.tsx index f6c80678..479fd407 100644 --- a/src/core/chart-api/chart-extra-legend.tsx +++ b/src/core/chart-api/chart-extra-legend.tsx @@ -84,7 +84,7 @@ export class ChartExtraLegend extends AsyncStore { private initLegend = () => { const prevState = this.get().items.reduce((map, item) => map.set(item.id, item), new Map()); - const itemSpecs = getChartLegendItems(this.context.chart(), this.context.settings.getItemProps); + const itemSpecs = getChartLegendItems(this.context.chart(), this.context.settings.getItemOptions); const legendItems = itemSpecs.map( ({ id, name, color, markerType, visible, status, isSecondary, markerAriaDescription }) => { const marker = this.renderMarker(markerType, color, visible, status, markerAriaDescription); diff --git a/src/core/chart-core.tsx b/src/core/chart-core.tsx index 955d4ddf..a517fdbf 100644 --- a/src/core/chart-core.tsx +++ b/src/core/chart-core.tsx @@ -1,9 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - import { useRef } from "react"; import clsx from "clsx"; import type Highcharts from "highcharts"; @@ -30,7 +27,7 @@ import { VerticalAxisTitle } from "./components/core-vertical-axis-title"; import { getFormatter } from "./formatters"; import { useChartI18n } from "./i18n-utils"; import { CoreChartProps } from "./interfaces"; -import { fillDefaultsForGetItemProps, i18nStatus } from "./utils"; +import { fillDefaultsForgetItemOptions, i18nStatus } from "./utils"; import { getLegendsProps, getPointAccessibleDescription } from "./utils"; import styles from "./styles.css.js"; @@ -68,7 +65,7 @@ export function InternalCoreChart({ onVisibleItemsChange, visibleItems, __internalRootRef, - getItemProps, + getItemOptions, ...rest }: CoreChartProps & InternalBaseComponentProps) { const highcharts = rest.highcharts as null | typeof Highcharts; @@ -80,7 +77,7 @@ export function InternalCoreChart({ tooltipEnabled: tooltipOptions?.enabled !== false, keyboardNavigationEnabled: keyboardNavigation, labels, - getItemProps: fillDefaultsForGetItemProps(getItemProps, { + getItemOptions: fillDefaultsForgetItemOptions(getItemOptions, { markerAriaDescriptionTemplate: i18nStrings?.chartMarkerAriaDescriptionTemplate, getI18nFromStatus: i18nStatus(i18nStrings), }), diff --git a/src/core/components/core-tooltip.tsx b/src/core/components/core-tooltip.tsx index 91c4b923..869ab5dd 100644 --- a/src/core/components/core-tooltip.tsx +++ b/src/core/components/core-tooltip.tsx @@ -162,7 +162,7 @@ function getTooltipContentCartesian( const x = group[0].x; const chart = group[0].series.chart; const getSeriesMarker = (series: Highcharts.Series) => { - const itemProps = api.context.settings.getItemProps(getSeriesId(series)); + const itemProps = api.context.settings.getItemOptions({ itemId: getSeriesId(series) }); return api.renderMarker( getSeriesMarkerType(series), getSeriesColor(series), @@ -256,8 +256,8 @@ function getTooltipContentPie( getSeriesMarkerType(point.series), getPointColor(point), true, - api.context.settings.getItemProps(getPointId(point)).status, - api.context.settings.getItemProps(getPointId(point)).markerAriaDescription, + api.context.settings.getItemOptions({ itemId: getPointId(point) }).status, + api.context.settings.getItemOptions({ itemId: getPointId(point) }).markerAriaDescription, )} {point.name} diff --git a/src/core/interfaces.ts b/src/core/interfaces.ts index 55807734..33f45a47 100644 --- a/src/core/interfaces.ts +++ b/src/core/interfaces.ts @@ -317,15 +317,6 @@ export interface CoreCartesianOptions { verticalAxisTitlePlacement?: "top" | "side"; } -export interface ChartItemOptions { - /** - * If specified, specifies the status of an item. - * An item can be a point or a series. - * @default "default" - */ - status?: ChartSeriesMarkerStatus; -} - export interface CoreChartProps extends Pick< BaseChartOptions, @@ -419,9 +410,8 @@ export interface CoreChartProps i18nStrings?: CartesianI18nStrings & PieI18nStrings & CoreI18nStrings & ChartSeriesMarkerI18n; /** * Specifies the options for each item in the chart. - * @param id the item id. Can be the id of a series or a point. */ - getItemProps?: (id: string) => ChartItemOptions; + getItemOptions?: (props: CoreChartProps.GetItemOptionsProps) => CoreChartProps.ChartItemOptions; } export namespace CoreChartProps { @@ -446,6 +436,22 @@ export namespace CoreChartProps { export type XAxisOptions = Highcharts.XAxisOptions & { valueFormatter?: (value: null | number) => string }; export type YAxisOptions = Highcharts.YAxisOptions & { valueFormatter?: (value: null | number) => string }; + export interface ChartItemOptions { + /** + * If specified, specifies the status of an item. + * An item can be a point or a series. + * @default "default" + */ + status?: ChartSeriesMarkerStatus; + } + + export interface GetItemOptionsProps { + /** + * The item id. Can be the id of a series or a point. + */ + itemId: string; + } + export interface HeaderOptions { content: React.ReactNode; } diff --git a/src/core/utils.ts b/src/core/utils.ts index 6d4bcbf2..e132c7f2 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -9,7 +9,7 @@ import { getChartSeries } from "../internal/utils/chart-series"; import { castArray } from "../internal/utils/utils"; import { getFormatter } from "./formatters"; import { ChartLabels } from "./i18n-utils"; -import { ChartItemOptions, CoreChartProps, Rect } from "./interfaces"; +import { CoreChartProps, Rect } from "./interfaces"; export interface LegendItemSpec { id: string; @@ -137,7 +137,7 @@ export function getPointColor(point?: Highcharts.Point): string { // Highcharts legend is disabled. Instead, we use this custom method to collect legend items from the chart. export function getChartLegendItems( chart: Highcharts.Chart, - getItemProps: ReturnType, + getItemOptions: ReturnType, ): readonly LegendItemSpec[] { const legendItems: LegendItemSpec[] = []; const isInverted = chart.inverted ?? false; @@ -155,7 +155,7 @@ export function getChartLegendItems( // The same is not supported for pie chart segments. if (series.options.showInLegend !== false) { const seriesId = getSeriesId(series); - const itemProps = getItemProps(seriesId); + const itemProps = getItemOptions({ itemId: seriesId }); legendItems.push({ id: seriesId, name: series.name, @@ -178,7 +178,7 @@ export function getChartLegendItems( markerType: getSeriesMarkerType(point.series), color: getPointColor(point), visible: point.visible, - status: getItemProps(pointId).status, + status: getItemOptions({ itemId: pointId }).status, isSecondary, }); } @@ -405,7 +405,7 @@ function getChartRect(rect: Rect, chart: Highcharts.Chart, canBeInverted: boolea }; } -export interface FillDefaultsForGetItemPropsOptions { +export interface FillDefaultsForgetItemOptionsOptions { markerAriaDescriptionTemplate?: string; getI18nFromStatus?: (status: ChartSeriesMarkerStatus) => string | undefined; } @@ -413,17 +413,17 @@ export interface FillDefaultsForGetItemPropsOptions { /** * Creates a function that returns chart item properties with default values applied. * - * This higher-order function wraps an optional `getItemProps` function and ensures that + * This higher-order function wraps an optional `getItemOptions` function and ensures that * all required properties of `ChartItemOptions` are present, filling in defaults where needed. */ -export function fillDefaultsForGetItemProps( - getItemProps: CoreChartProps["getItemProps"], - options?: FillDefaultsForGetItemPropsOptions, -): (id: string) => Required & { +export function fillDefaultsForgetItemOptions( + getItemOptions: CoreChartProps["getItemOptions"], + options?: FillDefaultsForgetItemOptionsOptions, +): (props: CoreChartProps.GetItemOptionsProps) => Required & { markerAriaDescription?: string; } { - return (id: string) => { - const prevItem = getItemProps?.(id) ?? {}; + return (props: CoreChartProps.GetItemOptionsProps) => { + const prevItem = getItemOptions?.(props) ?? {}; const status = prevItem.status ?? "default"; const statusI18n = options?.getI18nFromStatus?.(status); From 18ce39f6de71bea2fb6f2b26ed5422ffc507e7a1 Mon Sep 17 00:00:00 2001 From: Mael Kerichard Date: Tue, 6 Jan 2026 15:18:51 +0000 Subject: [PATCH 12/15] chore: renamed classname + useUniqueId --- src/internal/components/series-marker/index.tsx | 17 +++++------------ .../components/series-marker/styles.scss | 12 +++--------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/src/internal/components/series-marker/index.tsx b/src/internal/components/series-marker/index.tsx index b1497c3f..8c4e04c2 100644 --- a/src/internal/components/series-marker/index.tsx +++ b/src/internal/components/series-marker/index.tsx @@ -1,8 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { useId } from "react"; - +import { useUniqueId } from "@cloudscape-design/component-toolkit/internal"; import { BaseComponentProps } from "@cloudscape-design/components/internal/base-component"; import { colorBorderStatusWarning, colorTextInteractiveDisabled } from "@cloudscape-design/design-tokens"; @@ -26,29 +25,23 @@ export interface ChartSeriesMarkerProps extends BaseComponentProps { color: string; visible?: boolean; status: ChartSeriesMarkerStatus; - markerAriaDescription?: string; + ariaLabel?: string; } function scale(size: number, value: number) { return `translate(${size * ((1 - value) / 2)}, ${size * ((1 - value) / 2)}) scale(${value})`; } -export function ChartSeriesMarker({ - type = "line", - color, - visible = true, - status, - markerAriaDescription, -}: ChartSeriesMarkerProps) { +export function ChartSeriesMarker({ type = "line", color, visible = true, status, ariaLabel }: ChartSeriesMarkerProps) { color = visible ? color : colorTextInteractiveDisabled; // As React re-renders the components, a new ID should be created for masks. // Not doing so will not evaluate the mask. - const maskId = useId(); + const maskId = useUniqueId("chart-series-marker"); return (
- {markerAriaDescription && {markerAriaDescription}} + {ariaLabel && {ariaLabel}} diff --git a/src/internal/components/series-marker/styles.scss b/src/internal/components/series-marker/styles.scss index e84b7a47..b599ea0d 100644 --- a/src/internal/components/series-marker/styles.scss +++ b/src/internal/components/series-marker/styles.scss @@ -23,14 +23,8 @@ $marker-margin-right: cs.$space-static-xxs; cursor: inherit; } -.visuallyHidden { +.visually-hidden { position: absolute !important; - width: 1px !important; - height: 1px !important; - padding: 0 !important; - margin: -1px !important; - overflow: hidden !important; - clip: rect(0, 0, 0, 0) !important; - white-space: nowrap !important; - border: 0 !important; + inset-block-start: -9999px !important; + inset-inline-start: -9999px !important; } From 3ec4ad0ea36cdca25eee6cb7345a8e6c1d06085e Mon Sep 17 00:00:00 2001 From: Mael Kerichard Date: Tue, 6 Jan 2026 15:55:07 +0000 Subject: [PATCH 13/15] chore: provide i18n via a method instead of strings --- pages/03-core/core-line-chart.page.tsx | 8 ++- pages/03-core/marker-permutations.page.tsx | 2 +- .../__snapshots__/documenter.test.ts.snap | 4 +- src/core/__tests__/chart-core-utils.test.tsx | 55 +----------------- src/core/chart-api/chart-extra-context.tsx | 10 ++-- src/core/chart-api/chart-extra-legend.tsx | 22 ++++--- src/core/chart-core.tsx | 7 +-- src/core/components/core-tooltip.tsx | 19 +++---- src/core/interfaces.ts | 14 ++--- src/core/utils.ts | 57 +++---------------- .../series-marker/__tests__/index.test.tsx | 2 +- .../components/series-marker/index.tsx | 10 +++- 12 files changed, 63 insertions(+), 147 deletions(-) diff --git a/pages/03-core/core-line-chart.page.tsx b/pages/03-core/core-line-chart.page.tsx index d8dd0bbd..9c4f1a8b 100644 --- a/pages/03-core/core-line-chart.page.tsx +++ b/pages/03-core/core-line-chart.page.tsx @@ -107,8 +107,12 @@ export default function () { {...chartProps.core} highcharts={Highcharts} i18nStrings={{ - chartItemStatusWarning: "warning", - chartMarkerAriaDescriptionTemplate: "Series with status {status}", + itemMarkerAriaLabel: (status) => { + if (status === "warning") { + return "Warning status"; + } + return status as never; + }, }} options={{ lang: { diff --git a/pages/03-core/marker-permutations.page.tsx b/pages/03-core/marker-permutations.page.tsx index f8fefaf0..2ef9eca2 100644 --- a/pages/03-core/marker-permutations.page.tsx +++ b/pages/03-core/marker-permutations.page.tsx @@ -53,7 +53,7 @@ const permutationsForColors = [ "triangle-down", ], color: [color], - markerAriaDescription: ["aria"], + ariaLabel: ["aria"], status: ["default"], }, ]), diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index 5e6678b7..d6fc1d09 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -1459,7 +1459,7 @@ minimum width, the horizontal scrollbar is automatically added.", "description": "Specifies the options for each item in the chart.", "i18nTag": undefined, "inlineType": { - "name": "(props: CoreChartProps.GetItemOptionsProps) => CoreChartProps.ChartItemOptions", + "name": "CoreChartProps.GetItemOptions", "parameters": [ { "name": "props", @@ -1472,7 +1472,7 @@ minimum width, the horizontal scrollbar is automatically added.", "name": "getItemOptions", "optional": true, "systemTags": undefined, - "type": "((props: CoreChartProps.GetItemOptionsProps) => CoreChartProps.ChartItemOptions)", + "type": "CoreChartProps.GetItemOptions", "visualRefreshTag": undefined, }, { diff --git a/src/core/__tests__/chart-core-utils.test.tsx b/src/core/__tests__/chart-core-utils.test.tsx index f582134f..e7f16ab4 100644 --- a/src/core/__tests__/chart-core-utils.test.tsx +++ b/src/core/__tests__/chart-core-utils.test.tsx @@ -7,7 +7,6 @@ import "highcharts/highcharts-more"; import "highcharts/modules/solid-gauge"; import { CoreChartProps } from "../../../lib/components/core/interfaces"; import { - fillDefaultsForgetItemOptions, getChartLegendItems, getLegendsProps, getPointColor, @@ -80,7 +79,7 @@ describe("CoreChart: utils", () => { callback: (api) => (chartApi = api), }); - const items = getChartLegendItems(chartApi!.chart, fillDefaultsForgetItemOptions(undefined)); + const items = getChartLegendItems(chartApi!.chart, () => ({}), undefined); expect(items[0].isSecondary).toBe(axisOptions.opposite); }, ); @@ -110,7 +109,7 @@ describe("CoreChart: utils", () => { callback: (api) => (chartApi = api), }); - const items = getChartLegendItems(chartApi!.chart, fillDefaultsForgetItemOptions(undefined)); + const items = getChartLegendItems(chartApi!.chart, () => ({}), undefined); expect(items).toHaveLength(2); expect(items[0].isSecondary).toBe(false); expect(items[1].isSecondary).toBe(true); @@ -138,7 +137,7 @@ describe("CoreChart: utils", () => { callback: (api) => (chartApi = api), }); - const items = getChartLegendItems(chartApi!.chart, fillDefaultsForgetItemOptions(undefined)); + const items = getChartLegendItems(chartApi!.chart, () => ({}), undefined); if (type === "gauge" || type === "solidgauge") { expect(items).toHaveLength(1); @@ -256,52 +255,4 @@ describe("CoreChart: utils", () => { }); }); }); - - describe("fillDefaultsForgetItemOptions", () => { - describe.each([ - { - scenario: "getItemOptions is undefined", - getItemOptions: undefined, - id: "item1", - expected: { status: "default", markerAriaDescription: undefined }, - }, - { - scenario: "getItemOptions returns empty object", - getItemOptions: () => ({}), - id: "item1", - expected: { status: "default", markerAriaDescription: undefined }, - }, - { - scenario: "getItemOptions returns status", - getItemOptions: () => ({ status: "warning" as const }), - id: "item1", - expected: { status: "warning", markerAriaDescription: undefined }, - }, - { - scenario: "getItemOptions returns status and contains i18n", - getItemOptions: () => ({ status: "warning" as const }), - id: "item1", - expected: { status: "warning", markerAriaDescription: "hello hi" }, - options: { - markerAriaDescriptionTemplate: "hello {status}", - getI18nFromStatus: (): string => "hi", - }, - }, - { - scenario: "getItemOptions returns status and contains i18n - getI18nFromStatus returns undefined", - getItemOptions: () => ({ status: "warning" as const }), - id: "item1", - expected: { status: "warning", markerAriaDescription: undefined }, - options: { - markerAriaDescriptionTemplate: "hello {status}", - getI18nFromStatus: (): undefined => undefined, - }, - }, - ])("$scenario", ({ getItemOptions, id, expected, options }) => { - it("should return correct default values", () => { - const result = fillDefaultsForgetItemOptions(getItemOptions, options); - expect(result(id)).toEqual(expected); - }); - }); - }); }); diff --git a/src/core/chart-api/chart-extra-context.tsx b/src/core/chart-api/chart-extra-context.tsx index 4c76b61a..15bf4a0a 100644 --- a/src/core/chart-api/chart-extra-context.tsx +++ b/src/core/chart-api/chart-extra-context.tsx @@ -7,8 +7,8 @@ import { NonCancelableEventHandler } from "../../internal/events"; import { getChartSeries } from "../../internal/utils/chart-series"; import { getSeriesData } from "../../internal/utils/series-data"; import { ChartLabels } from "../i18n-utils"; -import { CoreChartProps, Rect } from "../interfaces"; -import { fillDefaultsForgetItemOptions, getGroupRect, isSeriesStacked } from "../utils"; +import { CoreChartProps, CoreI18nStrings, Rect } from "../interfaces"; +import { getGroupRect, isSeriesStacked } from "../utils"; // Chart API context is used for dependency injection for chart utilities. // It is initialized on chart render, and includes the chart instance, consumer @@ -32,7 +32,8 @@ export namespace ChartExtraContext { tooltipEnabled: boolean; keyboardNavigationEnabled: boolean; labels: ChartLabels; - getItemOptions: ReturnType; + getItemOptions: CoreChartProps.GetItemOptions; + itemMarkerAriaLabel: CoreI18nStrings["itemMarkerAriaLabel"]; } export interface Handlers { @@ -64,7 +65,8 @@ export function createChartContext(): ChartExtraContext { tooltipEnabled: false, keyboardNavigationEnabled: false, labels: {}, - getItemOptions: fillDefaultsForgetItemOptions(undefined), + getItemOptions: () => ({}), + itemMarkerAriaLabel: () => "", }, handlers: {}, state: {}, diff --git a/src/core/chart-api/chart-extra-legend.tsx b/src/core/chart-api/chart-extra-legend.tsx index 479fd407..d8728336 100644 --- a/src/core/chart-api/chart-extra-legend.tsx +++ b/src/core/chart-api/chart-extra-legend.tsx @@ -84,10 +84,14 @@ export class ChartExtraLegend extends AsyncStore { private initLegend = () => { const prevState = this.get().items.reduce((map, item) => map.set(item.id, item), new Map()); - const itemSpecs = getChartLegendItems(this.context.chart(), this.context.settings.getItemOptions); + const itemSpecs = getChartLegendItems( + this.context.chart(), + this.context.settings.getItemOptions, + this.context.settings.itemMarkerAriaLabel, + ); const legendItems = itemSpecs.map( - ({ id, name, color, markerType, visible, status, isSecondary, markerAriaDescription }) => { - const marker = this.renderMarker(markerType, color, visible, status, markerAriaDescription); + ({ id, name, color, markerType, visible, status, isSecondary, markerAriaLabel }) => { + const marker = this.renderMarker(markerType, color, visible, status, markerAriaLabel); return { id, name, marker, visible, isSecondary, highlighted: prevState.get(id)?.highlighted ?? false }; }, ); @@ -116,18 +120,12 @@ export class ChartExtraLegend extends AsyncStore { type: ChartSeriesMarkerType, color: string, visible = true, - status: ChartSeriesMarkerStatus, - markerAriaDescription?: string, + status?: ChartSeriesMarkerStatus, + ariaLabel?: string, ): React.ReactNode { const key = `${type}:${color}:${visible}:${status}`; const marker = this.markersCache.get(key) ?? ( - + ); this.markersCache.set(key, marker); return marker; diff --git a/src/core/chart-core.tsx b/src/core/chart-core.tsx index c6780f89..cae3e002 100644 --- a/src/core/chart-core.tsx +++ b/src/core/chart-core.tsx @@ -27,7 +27,6 @@ import { VerticalAxisTitle } from "./components/core-vertical-axis-title"; import { getFormatter } from "./formatters"; import { useChartI18n } from "./i18n-utils"; import { CoreChartProps } from "./interfaces"; -import { fillDefaultsForgetItemOptions, i18nStatus } from "./utils"; import { getLegendsProps, getPointAccessibleDescription } from "./utils"; import styles from "./styles.css.js"; @@ -77,10 +76,8 @@ export function InternalCoreChart({ tooltipEnabled: tooltipOptions?.enabled !== false, keyboardNavigationEnabled: keyboardNavigation, labels, - getItemOptions: fillDefaultsForgetItemOptions(getItemOptions, { - markerAriaDescriptionTemplate: i18nStrings?.chartMarkerAriaDescriptionTemplate, - getI18nFromStatus: i18nStatus(i18nStrings), - }), + getItemOptions: getItemOptions ?? (() => ({})), + itemMarkerAriaLabel: i18nStrings?.itemMarkerAriaLabel, }; const handlers = { onHighlight, onClearHighlight, onVisibleItemsChange }; const state = { visibleItems }; diff --git a/src/core/components/core-tooltip.tsx b/src/core/components/core-tooltip.tsx index dedeee19..1e0a9e3b 100644 --- a/src/core/components/core-tooltip.tsx +++ b/src/core/components/core-tooltip.tsx @@ -167,13 +167,13 @@ function getTooltipContentCartesian( const x = group[0].x; const chart = group[0].series.chart; const getSeriesMarker = (series: Highcharts.Series) => { - const itemProps = api.context.settings.getItemOptions({ itemId: getSeriesId(series) }); + const { status } = api.context.settings.getItemOptions({ itemId: getSeriesId(series) }); return api.renderMarker( getSeriesMarkerType(series), getSeriesColor(series), true, - itemProps.status, - itemProps.markerAriaDescription, + status, + status !== undefined && status !== "default" ? api.context.settings.itemMarkerAriaLabel?.(status) : undefined, ); }; const matchedItems = findTooltipSeriesItems(getChartSeries(chart.series), group, seriesSorting); @@ -254,16 +254,15 @@ function getTooltipContentPie( items: [{ point, errorRanges: [] }], hideTooltip, }; + + const { status } = api.context.settings.getItemOptions({ itemId: getPointId(point) }); + const markerAriaLabel = + status !== undefined && status !== "default" ? api.context.settings.itemMarkerAriaLabel?.(status) : undefined; + return { header: renderers.header?.(tooltipDetails) ?? (
- {api.renderMarker( - getSeriesMarkerType(point.series), - getPointColor(point), - true, - api.context.settings.getItemOptions({ itemId: getPointId(point) }).status, - api.context.settings.getItemOptions({ itemId: getPointId(point) }).markerAriaDescription, - )} + {api.renderMarker(getSeriesMarkerType(point.series), getPointColor(point), true, status, markerAriaLabel)} {point.name} diff --git a/src/core/interfaces.ts b/src/core/interfaces.ts index 33f45a47..1e5af316 100644 --- a/src/core/interfaces.ts +++ b/src/core/interfaces.ts @@ -138,13 +138,11 @@ export interface CoreI18nStrings extends BaseI18nStrings { secondaryLegendAriaLabel?: string; /** - * @example "Series with status {status}". + * ARIA label for the marker of a series. + * @param status the status of the series. + * @note series with status "default" will not announce a label, thus excluded by this method. */ - chartMarkerAriaDescriptionTemplate?: string; - /** - * @example "warning". - */ - chartItemStatusWarning?: string; + itemMarkerAriaLabel?: (status: Exclude) => string; } export interface WithCartesianI18nStrings { @@ -411,7 +409,7 @@ export interface CoreChartProps /** * Specifies the options for each item in the chart. */ - getItemOptions?: (props: CoreChartProps.GetItemOptionsProps) => CoreChartProps.ChartItemOptions; + getItemOptions?: CoreChartProps.GetItemOptions; } export namespace CoreChartProps { @@ -427,6 +425,8 @@ export namespace CoreChartProps { clearChartHighlight(): void; } + export type GetItemOptions = (props: CoreChartProps.GetItemOptionsProps) => CoreChartProps.ChartItemOptions; + // The extended version of Highcharts.Options. The axes types are extended with Cloudscape value formatter. // We use a custom formatter because we cannot use the built-in Highcharts formatter for our tooltip. export type ChartOptions = Omit & { diff --git a/src/core/utils.ts b/src/core/utils.ts index 65914076..439d3159 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -9,7 +9,7 @@ import { getChartSeries } from "../internal/utils/chart-series"; import { castArray } from "../internal/utils/utils"; import { getFormatter } from "./formatters"; import { ChartLabels } from "./i18n-utils"; -import { CoreChartProps, Rect } from "./interfaces"; +import { CoreChartProps, CoreI18nStrings, Rect } from "./interfaces"; export interface LegendItemSpec { id: string; @@ -18,8 +18,8 @@ export interface LegendItemSpec { color: string; visible: boolean; isSecondary: boolean; - status: ChartSeriesMarkerStatus; - markerAriaDescription?: string; + status?: ChartSeriesMarkerStatus; + markerAriaLabel?: string; } // The below functions extract unique identifier from series, point, or options. The identifier can be item's ID or name. @@ -137,7 +137,8 @@ export function getPointColor(point?: Highcharts.Point): string { // Highcharts legend is disabled. Instead, we use this custom method to collect legend items from the chart. export function getChartLegendItems( chart: Highcharts.Chart, - getItemOptions: ReturnType, + getItemOptions: CoreChartProps.GetItemOptions, + itemMarkerAriaLabel: CoreI18nStrings["itemMarkerAriaLabel"], ): readonly LegendItemSpec[] { const legendItems: LegendItemSpec[] = []; const isInverted = chart.inverted ?? false; @@ -156,6 +157,7 @@ export function getChartLegendItems( if (series.options.showInLegend !== false) { const seriesId = getSeriesId(series); const itemProps = getItemOptions({ itemId: seriesId }); + const status = itemProps.status; legendItems.push({ id: seriesId, name: series.name, @@ -163,8 +165,8 @@ export function getChartLegendItems( color: getSeriesColor(series), visible: series.visible, isSecondary, - status: itemProps.status, - markerAriaDescription: itemProps.markerAriaDescription, + status: status, + markerAriaLabel: status !== undefined && status !== "default" ? itemMarkerAriaLabel?.(status) : undefined, }); } }; @@ -407,46 +409,3 @@ function getChartRect(rect: Rect, chart: Highcharts.Chart, canBeInverted: boolea height: rect.height, }; } - -export interface FillDefaultsForgetItemOptionsOptions { - markerAriaDescriptionTemplate?: string; - getI18nFromStatus?: (status: ChartSeriesMarkerStatus) => string | undefined; -} - -/** - * Creates a function that returns chart item properties with default values applied. - * - * This higher-order function wraps an optional `getItemOptions` function and ensures that - * all required properties of `ChartItemOptions` are present, filling in defaults where needed. - */ -export function fillDefaultsForgetItemOptions( - getItemOptions: CoreChartProps["getItemOptions"], - options?: FillDefaultsForgetItemOptionsOptions, -): (props: CoreChartProps.GetItemOptionsProps) => Required & { - markerAriaDescription?: string; -} { - return (props: CoreChartProps.GetItemOptionsProps) => { - const prevItem = getItemOptions?.(props) ?? {}; - const status = prevItem.status ?? "default"; - const statusI18n = options?.getI18nFromStatus?.(status); - - return { - status, - markerAriaDescription: - statusI18n !== undefined ? options?.markerAriaDescriptionTemplate?.replace("{status}", statusI18n) : undefined, - }; - }; -} - -export function i18nStatus(i18n: CoreChartProps["i18nStrings"]) { - return (status: ChartSeriesMarkerStatus) => { - switch (status) { - case "warning": - return i18n?.chartItemStatusWarning; - case "default": - return undefined; - default: - throw status satisfies never; - } - }; -} diff --git a/src/internal/components/series-marker/__tests__/index.test.tsx b/src/internal/components/series-marker/__tests__/index.test.tsx index c10f2a77..e377cd89 100644 --- a/src/internal/components/series-marker/__tests__/index.test.tsx +++ b/src/internal/components/series-marker/__tests__/index.test.tsx @@ -10,7 +10,7 @@ describe("ChartSeriesMarker", () => { const defaultProps = { type: "line", color: "#0073bb", - markerAriaDescription: "This is a description", + ariaLabel: "This is a description", } satisfies ChartSeriesMarkerProps; describe("Warning SVG display", () => { diff --git a/src/internal/components/series-marker/index.tsx b/src/internal/components/series-marker/index.tsx index 8c4e04c2..a141f158 100644 --- a/src/internal/components/series-marker/index.tsx +++ b/src/internal/components/series-marker/index.tsx @@ -24,7 +24,7 @@ export interface ChartSeriesMarkerProps extends BaseComponentProps { type: ChartSeriesMarkerType; color: string; visible?: boolean; - status: ChartSeriesMarkerStatus; + status?: ChartSeriesMarkerStatus; ariaLabel?: string; } @@ -32,7 +32,13 @@ function scale(size: number, value: number) { return `translate(${size * ((1 - value) / 2)}, ${size * ((1 - value) / 2)}) scale(${value})`; } -export function ChartSeriesMarker({ type = "line", color, visible = true, status, ariaLabel }: ChartSeriesMarkerProps) { +export function ChartSeriesMarker({ + type = "line", + color, + visible = true, + status = "default", + ariaLabel, +}: ChartSeriesMarkerProps) { color = visible ? color : colorTextInteractiveDisabled; // As React re-renders the components, a new ID should be created for masks. From 595be38bf33d051099bd817dfab70d3decbc450c Mon Sep 17 00:00:00 2001 From: Mael Kerichard Date: Tue, 6 Jan 2026 15:55:59 +0000 Subject: [PATCH 14/15] chore: removed unused type --- src/core/interfaces.ts | 4 ++-- src/internal/components/series-marker/interfaces.ts | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/core/interfaces.ts b/src/core/interfaces.ts index 1e5af316..0bb8e77e 100644 --- a/src/core/interfaces.ts +++ b/src/core/interfaces.ts @@ -4,7 +4,7 @@ import type Highcharts from "highcharts"; import type * as InternalComponentTypes from "../internal/components/interfaces"; -import { ChartSeriesMarkerI18n, ChartSeriesMarkerStatus } from "../internal/components/series-marker/interfaces"; +import { ChartSeriesMarkerStatus } from "../internal/components/series-marker/interfaces"; import { type NonCancelableEventHandler } from "../internal/events"; // All charts take `highcharts` instance, that can be served statically or dynamically. @@ -405,7 +405,7 @@ export interface CoreChartProps * An object that contains all of the localized strings required by the component. * @i18n */ - i18nStrings?: CartesianI18nStrings & PieI18nStrings & CoreI18nStrings & ChartSeriesMarkerI18n; + i18nStrings?: CartesianI18nStrings & PieI18nStrings & CoreI18nStrings; /** * Specifies the options for each item in the chart. */ diff --git a/src/internal/components/series-marker/interfaces.ts b/src/internal/components/series-marker/interfaces.ts index 639602c4..12c2a626 100644 --- a/src/internal/components/series-marker/interfaces.ts +++ b/src/internal/components/series-marker/interfaces.ts @@ -13,7 +13,3 @@ export type ChartSeriesMarkerType = | "circle"; export type ChartSeriesMarkerStatus = "warning" | "default"; - -export type ChartSeriesMarkerI18n = Partial<{ - seriesStatusWarningAriaLabel: string; -}>; From ec045f81d3337500277928571534e3525ed8e37f Mon Sep 17 00:00:00 2001 From: Mael Kerichard Date: Tue, 6 Jan 2026 16:43:28 +0000 Subject: [PATCH 15/15] test: update documenter --- src/__tests__/__snapshots__/documenter.test.ts.snap | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index d6fc1d09..c4057ff2 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -1566,20 +1566,19 @@ Supported Highcharts versions: 12.", "description": "An object that contains all of the localized strings required by the component.", "i18nTag": true, "inlineType": { - "name": "CartesianI18nStrings & PieI18nStrings & CoreI18nStrings & Partial<{ seriesStatusWarningAriaLabel: string; }>", + "name": "CartesianI18nStrings & PieI18nStrings & CoreI18nStrings", "type": "union", "valueDescriptions": undefined, "values": [ "CartesianI18nStrings", "PieI18nStrings", "CoreI18nStrings", - "Partial<{ seriesStatusWarningAriaLabel: string; }>", ], }, "name": "i18nStrings", "optional": true, "systemTags": undefined, - "type": "CartesianI18nStrings & PieI18nStrings & CoreI18nStrings & Partial<{ seriesStatusWarningAriaLabel: string; }>", + "type": "CartesianI18nStrings & PieI18nStrings & CoreI18nStrings", "visualRefreshTag": undefined, }, {