From 4b2853ce7f7a894bea83e8d27b760c23961d69ae Mon Sep 17 00:00:00 2001 From: giuliana-gladeye Date: Mon, 25 May 2026 12:01:38 +1200 Subject: [PATCH 1/7] feat: added bar graph component and demo --- .../demo/src/components/demo/bar-graph.tsx | 97 +++++++++++++++ .../demo/src/content/components/bar-graph.mdx | 90 ++++++++++++++ .../components/bar-graph/bar-graph.module.css | 33 +++++ .../ui/src/components/bar-graph/bar-graph.tsx | 117 ++++++++++++++++++ packages/ui/src/components/index.ts | 1 + 5 files changed, 338 insertions(+) create mode 100644 packages/demo/src/components/demo/bar-graph.tsx create mode 100644 packages/demo/src/content/components/bar-graph.mdx create mode 100644 packages/ui/src/components/bar-graph/bar-graph.module.css create mode 100644 packages/ui/src/components/bar-graph/bar-graph.tsx diff --git a/packages/demo/src/components/demo/bar-graph.tsx b/packages/demo/src/components/demo/bar-graph.tsx new file mode 100644 index 0000000..356875d --- /dev/null +++ b/packages/demo/src/components/demo/bar-graph.tsx @@ -0,0 +1,97 @@ +import { BarGraph, BarGraphSegment } from "@eqtylab/equality"; + +export function BarGraphDemo() { + return ( + + + + + + ); +} + +export function BarGraphWithLabelsDemo() { + return ( + + + + + + ); +} + +export function BarGraphRichTooltipDemo() { + return ( + + + Success +

10 controls

+ + } + /> + + Pending +

290 controls

+ + } + /> + + Failure +

3 controls

+ + } + /> +
+ ); +} diff --git a/packages/demo/src/content/components/bar-graph.mdx b/packages/demo/src/content/components/bar-graph.mdx new file mode 100644 index 0000000..e9ec65c --- /dev/null +++ b/packages/demo/src/content/components/bar-graph.mdx @@ -0,0 +1,90 @@ +--- +layout: "@demo/layouts/mdx-layout.astro" +heading: "Bar Graph" +description: "Single line segmented bar graph with segment tooltips" +--- + +import { + BarGraphDemo, + BarGraphWithLabelsDemo, + BarGraphRichTooltipDemo, +} from "@demo/components/demo/bar-graph"; + +## Overview + +`BarGraph` shows a single horizontal bar split into colored segments. Use it to break a total down into its parts, such as controls status. + +Add a `BarGraphSegment` for each part. Widths come from each segment's `value` as a share of the total (`value / sum of all values`), so you pass raw numbers rather than percentages. Children that aren't a `BarGraphSegment` are ignored. + +Segments are focusable and show a [Tooltip](tooltip) on hover and focus, so they work by keyboard alone. The bar itself uses `role="img"` and is described by `aria-label`. Give every segment a `label` and `tooltip`, since color on its own doesn't carry meaning. + +## Usage + +Import the component: + +```tsx +import { BarGraph, BarGraphSegment } from "@eqtylab/equality"; +``` + +Basic usage with required properties: + +```tsx + + + + + +``` + +## Variants + +### Default + +Labels are screen-reader only by default. Hover or focus a segment to reveal its tooltip. + + + +### With visible labels + +Set `showLabels` to render a legend beneath the bar. + + + +### Rich tooltip content + +The `tooltip` prop accepts a string or any `ReactNode`, so segments can show formatted content. + + + +## Props + +### `BarGraph` + +| Name | Description | Type | Default | Required | +| ------------ | --------------------------------------------------------------- | ----------------- | ------- | -------- | +| `showLabels` | Render a visible legend (color swatch + label) beneath the bar. | `boolean` | `false` | ❌ | +| `children` | One or more segments to render. | `BarGraphSegment` | — | ✅ | + +### `BarGraphSegment` + +| Name | Description | Type | Default | Required | +| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------- | ------- | -------- | +| `value` | Sets the segment's width as a share of the total (`value / sum of all values`). Every segment is floored at 2px so small and zero values stay visible. | `number` | — | ✅ | +| `color` | Any valid CSS color (e.g. a token like `var(--color-brand-green)` or `#10b981`). | `string` | — | ✅ | +| `label` | Accessible name; shown visibly in the legend when `showLabels` is set, else SR-only. | `string` | — | ✅ | +| `tooltip` | Tooltip content shown on hover and keyboard focus. | `string \| ReactNode` | — | ✅ | diff --git a/packages/ui/src/components/bar-graph/bar-graph.module.css b/packages/ui/src/components/bar-graph/bar-graph.module.css new file mode 100644 index 0000000..8413fbf --- /dev/null +++ b/packages/ui/src/components/bar-graph/bar-graph.module.css @@ -0,0 +1,33 @@ +@reference '../../theme/theme.module.css'; + +.bar-graph { + @apply flex w-full flex-col gap-2; +} + +.bar { + @apply bg-greyscale-800; + @apply flex h-2 w-full gap-0.5 overflow-hidden rounded-full; +} + +.segment { + @apply h-full; + flex: 1 1 0%; + @apply outline-none; + @apply focus-visible:ring-focus-ring focus-visible:ring-2 focus-visible:ring-inset; +} + +.legend { + @apply m-0 flex list-none flex-wrap gap-x-4 gap-y-1 p-0; +} + +.legend-item { + @apply flex items-center gap-1.5; +} + +.legend-swatch { + @apply size-2.5 shrink-0 rounded-full; +} + +.legend-label { + @apply text-text-primary text-xs; +} diff --git a/packages/ui/src/components/bar-graph/bar-graph.tsx b/packages/ui/src/components/bar-graph/bar-graph.tsx new file mode 100644 index 0000000..09d7431 --- /dev/null +++ b/packages/ui/src/components/bar-graph/bar-graph.tsx @@ -0,0 +1,117 @@ +import * as React from 'react'; + +import styles from '@/components/bar-graph/bar-graph.module.css'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/tooltip/tooltip'; +import { cn } from '@/lib/utils'; + +export interface BarGraphSegmentProps extends Omit, 'color'> { + /** Proportional value used to compute the segment's width (`value / sum of all values`). */ + value: number; + /** Any valid CSS color, applied as the segment's background. */ + color: string; + /** + * Accessible name for the segment. Shown visibly in the legend when `showLabels` + * is set on the parent `BarGraph`, otherwise it is screen-reader only. + */ + label: string; + /** Tooltip content shown on hover and keyboard focus. Required. */ + tooltip: React.ReactNode; +} + +const BarGraphSegment = React.forwardRef( + ({ value, color, label, tooltip, className, style, ...props }, ref) => ( + // Widths are proportional (`value / sum`): each segment grows by its `value` + // within the flex row, so the parent never needs to know the total. + + +
+ + {tooltip} + + ) +); +BarGraphSegment.displayName = 'BarGraphSegment'; + +/** + * A single `BarGraphSegment` element. False values are allowed so conditional + * rendering (`{condition && }`) keeps working. Non-segment + * children are filtered out at runtime — see `BarGraph`. + */ +type BarGraphChild = React.ReactElement | boolean | null | undefined; + +export interface BarGraphProps extends Omit, 'children'> { + /** Render a visible legend (color swatch + label) beneath the bar. Defaults to `false` (labels are screen-reader only). */ + showLabels?: boolean; + /** One or more `BarGraphSegment` elements. */ + children: BarGraphChild | BarGraphChild[]; +} + +const BarGraph = React.forwardRef( + ( + { + className, + children, + showLabels = false, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledby, + ...props + }, + ref + ) => { + const validChildren = React.Children.toArray(children).filter(React.isValidElement); + const segments = validChildren.filter( + (child): child is React.ReactElement => child.type === BarGraphSegment + ); + + return ( + +
+
+ {segments} +
+ {showLabels && ( +
    + {segments.map((child, index) => ( +
  • +
  • + ))} +
+ )} +
+
+ ); + } +); +BarGraph.displayName = 'BarGraph'; + +export { BarGraph, BarGraphSegment }; diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 7b519ab..51a0200 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -2,6 +2,7 @@ export * from './alert/alert'; export * from './alert-dialog/alert-dialog'; export * from './avatar/avatar'; export * from './badge/badge'; +export * from './bar-graph/bar-graph'; export * from './bg-gradient/bg-gradient'; export * from './button/button'; export * from './resource-badge/resource-badge'; From 9a1c9b195f358e3800822e1695c18157a5e46056 Mon Sep 17 00:00:00 2001 From: giuliana-gladeye Date: Wed, 27 May 2026 11:59:24 +1200 Subject: [PATCH 2/7] Added sizes support for Bar Graph --- .../demo/src/components/demo/bar-graph.tsx | 231 ++++++++++++------ .../demo/src/content/components/bar-graph.mdx | 32 ++- .../components/bar-graph/bar-graph.module.css | 16 +- .../ui/src/components/bar-graph/bar-graph.tsx | 22 +- 4 files changed, 213 insertions(+), 88 deletions(-) diff --git a/packages/demo/src/components/demo/bar-graph.tsx b/packages/demo/src/components/demo/bar-graph.tsx index 356875d..d816012 100644 --- a/packages/demo/src/components/demo/bar-graph.tsx +++ b/packages/demo/src/components/demo/bar-graph.tsx @@ -2,96 +2,165 @@ import { BarGraph, BarGraphSegment } from "@eqtylab/equality"; export function BarGraphDemo() { return ( - - - - - +
+ + + + + +
+ ); +} + +const controlsSegments = () => [ + , + , + , +]; + +export function BarGraphSizeSmDemo() { + return ( +
+ + {controlsSegments()} + +
+ ); +} + +export function BarGraphSizeMdDemo() { + return ( +
+ + {controlsSegments()} + +
+ ); +} + +export function BarGraphSizeLgDemo() { + return ( +
+ + {controlsSegments()} + +
); } export function BarGraphWithLabelsDemo() { return ( - - - - - +
+ + + + + +
); } export function BarGraphRichTooltipDemo() { return ( - - - Success -

10 controls

-
- } - /> - - Pending -

290 controls

- - } - /> - - Failure -

3 controls

- - } - /> - +
+ + + Success +

10 controls

+
+ } + /> + + Pending +

290 controls

+ + } + /> + + Failure +

8 controls

+ + } + /> + + ); } diff --git a/packages/demo/src/content/components/bar-graph.mdx b/packages/demo/src/content/components/bar-graph.mdx index e9ec65c..12ce8c9 100644 --- a/packages/demo/src/content/components/bar-graph.mdx +++ b/packages/demo/src/content/components/bar-graph.mdx @@ -6,6 +6,9 @@ description: "Single line segmented bar graph with segment tooltips" import { BarGraphDemo, + BarGraphSizeSmDemo, + BarGraphSizeMdDemo, + BarGraphSizeLgDemo, BarGraphWithLabelsDemo, BarGraphRichTooltipDemo, } from "@demo/components/demo/bar-graph"; @@ -59,6 +62,26 @@ Labels are screen-reader only by default. Hover or focus a segment to reveal its +### Sizes + +Use `size` to control the bar's height. `sm` is a thin pill; the default `md` and `lg` are bolder and use rounded rectangles instead of a full pill. Note the small `Failure` segment, which is floored at 2px so it stays visible and focusable at every size. + +#### Small + + + +#### Medium (default) + + + +#### Large + + + +```tsx +{/* segments */} +``` + ### With visible labels Set `showLabels` to render a legend beneath the bar. @@ -75,10 +98,11 @@ The `tooltip` prop accepts a string or any `ReactNode`, so segments can show for ### `BarGraph` -| Name | Description | Type | Default | Required | -| ------------ | --------------------------------------------------------------- | ----------------- | ------- | -------- | -| `showLabels` | Render a visible legend (color swatch + label) beneath the bar. | `boolean` | `false` | ❌ | -| `children` | One or more segments to render. | `BarGraphSegment` | — | ✅ | +| Name | Description | Type | Default | Required | +| ------------ | ------------------------------------------------------------------------ | ----------------- | ------- | -------- | +| `size` | Bar height. `md` and `lg` use rounded rectangles instead of a full pill. | `sm`, `md`, `lg` | `md` | ❌ | +| `showLabels` | Render a visible legend (color swatch + label) beneath the bar. | `boolean` | `false` | ❌ | +| `children` | One or more segments to render. | `BarGraphSegment` | — | ✅ | ### `BarGraphSegment` diff --git a/packages/ui/src/components/bar-graph/bar-graph.module.css b/packages/ui/src/components/bar-graph/bar-graph.module.css index 8413fbf..db3204d 100644 --- a/packages/ui/src/components/bar-graph/bar-graph.module.css +++ b/packages/ui/src/components/bar-graph/bar-graph.module.css @@ -6,7 +6,21 @@ .bar { @apply bg-greyscale-800; - @apply flex h-2 w-full gap-0.5 overflow-hidden rounded-full; + @apply flex w-full gap-0.5 overflow-hidden; +} + +/* Size Variants */ + +.size--sm { + @apply h-2 rounded-full; +} + +.size--md { + @apply h-5 rounded-sm; +} + +.size--lg { + @apply h-8 rounded-sm; } .segment { diff --git a/packages/ui/src/components/bar-graph/bar-graph.tsx b/packages/ui/src/components/bar-graph/bar-graph.tsx index 09d7431..e90a9ae 100644 --- a/packages/ui/src/components/bar-graph/bar-graph.tsx +++ b/packages/ui/src/components/bar-graph/bar-graph.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; import styles from '@/components/bar-graph/bar-graph.module.css'; import { @@ -51,6 +52,19 @@ const BarGraphSegment = React.forwardRef( ); BarGraphSegment.displayName = 'BarGraphSegment'; +const barVariants = cva(styles['bar'], { + variants: { + size: { + sm: styles['size--sm'], + md: styles['size--md'], + lg: styles['size--lg'], + }, + }, + defaultVariants: { + size: 'md', + }, +}); + /** * A single `BarGraphSegment` element. False values are allowed so conditional * rendering (`{condition && }`) keeps working. Non-segment @@ -58,9 +72,12 @@ BarGraphSegment.displayName = 'BarGraphSegment'; */ type BarGraphChild = React.ReactElement | boolean | null | undefined; -export interface BarGraphProps extends Omit, 'children'> { +export interface BarGraphProps + extends Omit, 'children'>, VariantProps { /** Render a visible legend (color swatch + label) beneath the bar. Defaults to `false` (labels are screen-reader only). */ showLabels?: boolean; + /** Bar height. `md` and `lg` use rounded rectangles instead of a full pill. Defaults to `md`. */ + size?: 'sm' | 'md' | 'lg'; /** One or more `BarGraphSegment` elements. */ children: BarGraphChild | BarGraphChild[]; } @@ -71,6 +88,7 @@ const BarGraph = React.forwardRef( className, children, showLabels = false, + size, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, ...props @@ -89,7 +107,7 @@ const BarGraph = React.forwardRef( role="img" aria-label={ariaLabel} aria-labelledby={ariaLabelledby} - className={styles['bar']} + className={barVariants({ size })} > {segments} From ecc346b59b69f5bee8142e3a77763b3c2e9b702d Mon Sep 17 00:00:00 2001 From: giuliana-gladeye Date: Wed, 27 May 2026 13:22:39 +1200 Subject: [PATCH 3/7] Screen readers updated on bar graph --- .../demo/src/components/demo/bar-graph.tsx | 39 ++------ .../demo/src/content/components/bar-graph.mdx | 10 +- .../components/bar-graph/bar-graph.module.css | 5 + .../ui/src/components/bar-graph/bar-graph.tsx | 93 +++++++++---------- 4 files changed, 64 insertions(+), 83 deletions(-) diff --git a/packages/demo/src/components/demo/bar-graph.tsx b/packages/demo/src/components/demo/bar-graph.tsx index d816012..19a9eb3 100644 --- a/packages/demo/src/components/demo/bar-graph.tsx +++ b/packages/demo/src/components/demo/bar-graph.tsx @@ -3,7 +3,7 @@ import { BarGraph, BarGraphSegment } from "@eqtylab/equality"; export function BarGraphDemo() { return (
- + [ value={12} color="var(--color-brand-green)" label="Success" - tooltip="12 controls" + tooltip="12 controls succeeded" />, , , ]; export function BarGraphSizeSmDemo() { return (
- - {controlsSegments()} - + {controlsSegments()}
); } @@ -67,12 +62,7 @@ export function BarGraphSizeSmDemo() { export function BarGraphSizeMdDemo() { return (
- - {controlsSegments()} - + {controlsSegments()}
); } @@ -80,12 +70,7 @@ export function BarGraphSizeMdDemo() { export function BarGraphSizeLgDemo() { return (
- - {controlsSegments()} - + {controlsSegments()}
); } @@ -93,10 +78,7 @@ export function BarGraphSizeLgDemo() { export function BarGraphWithLabelsDemo() { return (
- + - + + @@ -110,5 +110,5 @@ The `tooltip` prop accepts a string or any `ReactNode`, so segments can show for | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------- | ------- | -------- | | `value` | Sets the segment's width as a share of the total (`value / sum of all values`). Every segment is floored at 2px so small and zero values stay visible. | `number` | — | ✅ | | `color` | Any valid CSS color (e.g. a token like `var(--color-brand-green)` or `#10b981`). | `string` | — | ✅ | -| `label` | Accessible name; shown visibly in the legend when `showLabels` is set, else SR-only. | `string` | — | ✅ | -| `tooltip` | Tooltip content shown on hover and keyboard focus. | `string \| ReactNode` | — | ✅ | +| `label` | Visible legend label, shown when `showLabels` is set. Not announced to screen readers. | `string` | — | ✅ | +| `tooltip` | Tooltip content shown on hover and keyboard focus; also the segment's accessible name, so it must describe the segment on its own. | `string \| ReactNode` | — | ✅ | diff --git a/packages/ui/src/components/bar-graph/bar-graph.module.css b/packages/ui/src/components/bar-graph/bar-graph.module.css index db3204d..f3681a3 100644 --- a/packages/ui/src/components/bar-graph/bar-graph.module.css +++ b/packages/ui/src/components/bar-graph/bar-graph.module.css @@ -30,6 +30,11 @@ @apply focus-visible:ring-focus-ring focus-visible:ring-2 focus-visible:ring-inset; } +/* Screen-reader-only text that backs each segment's accessible name. */ +.visually-hidden { + @apply sr-only; +} + .legend { @apply m-0 flex list-none flex-wrap gap-x-4 gap-y-1 p-0; } diff --git a/packages/ui/src/components/bar-graph/bar-graph.tsx b/packages/ui/src/components/bar-graph/bar-graph.tsx index e90a9ae..b29e4c5 100644 --- a/packages/ui/src/components/bar-graph/bar-graph.tsx +++ b/packages/ui/src/components/bar-graph/bar-graph.tsx @@ -16,39 +16,54 @@ export interface BarGraphSegmentProps extends Omit( - ({ value, color, label, tooltip, className, style, ...props }, ref) => ( - // Widths are proportional (`value / sum`): each segment grows by its `value` - // within the flex row, so the parent never needs to know the total. - - -
- - {tooltip} - - ) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ({ value, color, label, tooltip, className, style, ...props }, ref) => { + // The segment's accessible name is the tooltip content. + // It's announced immediately on focus. + const tooltipId = React.useId(); + + return ( + // Widths are proportional (`value / sum`): each segment grows by its `value` + // within the flex row, so the parent never needs to know the total. + + +
+
+ {tooltip} +
+
+
+ {tooltip} +
+ ); + } ); BarGraphSegment.displayName = 'BarGraphSegment'; @@ -74,7 +89,7 @@ type BarGraphChild = React.ReactElement | boolean | null | export interface BarGraphProps extends Omit, 'children'>, VariantProps { - /** Render a visible legend (color swatch + label) beneath the bar. Defaults to `false` (labels are screen-reader only). */ + /** Render a visible legend (color swatch + label) beneath the bar. Defaults to `false`. */ showLabels?: boolean; /** Bar height. `md` and `lg` use rounded rectangles instead of a full pill. Defaults to `md`. */ size?: 'sm' | 'md' | 'lg'; @@ -83,18 +98,7 @@ export interface BarGraphProps } const BarGraph = React.forwardRef( - ( - { - className, - children, - showLabels = false, - size, - 'aria-label': ariaLabel, - 'aria-labelledby': ariaLabelledby, - ...props - }, - ref - ) => { + ({ className, children, showLabels = false, size, ...props }, ref) => { const validChildren = React.Children.toArray(children).filter(React.isValidElement); const segments = validChildren.filter( (child): child is React.ReactElement => child.type === BarGraphSegment @@ -103,14 +107,7 @@ const BarGraph = React.forwardRef( return (
-
- {segments} -
+
{segments}
{showLabels && (
    {segments.map((child, index) => ( From dcfecd99971ee85239f483fb608d824a7736f17a Mon Sep 17 00:00:00 2001 From: Henry Wilkinson Date: Wed, 27 May 2026 13:34:52 -0400 Subject: [PATCH 4/7] Move aria hidden to hide the legend for screen readers This information should be encapsulated in the tooltip --- packages/ui/src/components/bar-graph/bar-graph.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ui/src/components/bar-graph/bar-graph.tsx b/packages/ui/src/components/bar-graph/bar-graph.tsx index b29e4c5..6a3feea 100644 --- a/packages/ui/src/components/bar-graph/bar-graph.tsx +++ b/packages/ui/src/components/bar-graph/bar-graph.tsx @@ -109,11 +109,10 @@ const BarGraph = React.forwardRef(
    {segments}
    {showLabels && ( -
      +