@kinetixui/ui 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -474,6 +474,14 @@ type ChartConfig = {
474
474
  declare const ChartContainer: React$1.ForwardRefExoticComponent<Omit<React$1.ClassAttributes<HTMLDivElement> & React$1.HTMLAttributes<HTMLDivElement> & {
475
475
  config: ChartConfig;
476
476
  children: React$1.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
477
+ /**
478
+ * Text alternative for the chart (WCAG 1.1.1). A chart conveys its data by
479
+ * colour + position, which a screen reader can't relay — pass a one-line
480
+ * summary of what it shows and the trend. Falls back to "Chart".
481
+ * Cartesian charts should also get `accessibilityLayer` on the Recharts
482
+ * primitive for keyboard data-point navigation.
483
+ */
484
+ label?: string;
477
485
  }, "ref"> & React$1.RefAttributes<HTMLDivElement>>;
478
486
  declare const ChartStyle: ({ id, config }: {
479
487
  id: string;
@@ -1120,6 +1128,9 @@ declare const Metric: React$1.ForwardRefExoticComponent<MetricProps & React$1.Re
1120
1128
 
1121
1129
  /**
1122
1130
  * NumberInput — a numeric field with increment / decrement controls.
1131
+ * The wrapper shows `--shadow-focus` on `focus-within`; each stepper also
1132
+ * draws its own `--ring` inset outline on `:focus-visible` so keyboard users
1133
+ * can tell which control is focused.
1123
1134
  */
1124
1135
  interface NumberInputProps extends Omit<React$1.InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "defaultValue" | "onChange"> {
1125
1136
  value?: number;
package/dist/index.js CHANGED
@@ -1545,14 +1545,38 @@ function useChart() {
1545
1545
  if (!context) throw new Error("useChart must be used within a <ChartContainer />");
1546
1546
  return context;
1547
1547
  }
1548
- var ChartContainer = React32.forwardRef(({ id, className, children, config, ...props }, ref) => {
1548
+ var ChartContainer = React32.forwardRef(({ id, className, children, config, label, role, "aria-label": ariaLabel, ...props }, ref) => {
1549
1549
  const uniqueId = React32.useId();
1550
1550
  const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
1551
+ const name = ariaLabel ?? label ?? "Chart";
1552
+ const innerRef = React32.useRef(null);
1553
+ const setRef = React32.useCallback(
1554
+ (node) => {
1555
+ innerRef.current = node;
1556
+ if (typeof ref === "function") ref(node);
1557
+ else if (ref) ref.current = node;
1558
+ },
1559
+ [ref]
1560
+ );
1561
+ React32.useEffect(() => {
1562
+ const el = innerRef.current;
1563
+ if (!el) return;
1564
+ const scrub = () => {
1565
+ el.querySelector(".recharts-surface")?.setAttribute("aria-label", name);
1566
+ el.querySelectorAll('[role="img"]:not([data-chart])').forEach((n) => n.removeAttribute("role"));
1567
+ };
1568
+ scrub();
1569
+ const mo = new MutationObserver(scrub);
1570
+ mo.observe(el, { childList: true, subtree: true });
1571
+ return () => mo.disconnect();
1572
+ }, [name]);
1551
1573
  return /* @__PURE__ */ jsx37(ChartContext.Provider, { value: { config }, children: /* @__PURE__ */ jsxs15(
1552
1574
  "div",
1553
1575
  {
1554
1576
  "data-chart": chartId,
1555
- ref,
1577
+ ref: setRef,
1578
+ role: role ?? "img",
1579
+ "aria-label": name,
1556
1580
  className: cn(
1557
1581
  "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
1558
1582
  className
@@ -3530,7 +3554,7 @@ var NumberInput = React60.forwardRef(
3530
3554
  "aria-label": "Decrease",
3531
3555
  disabled: disabled || min != null && current <= min,
3532
3556
  onClick: () => set(current - step),
3533
- className: "flex w-9 shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50",
3557
+ className: "flex w-9 shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
3534
3558
  children: /* @__PURE__ */ jsx66(Minus3, { className: "size-4" })
3535
3559
  }
3536
3560
  ),
@@ -3560,7 +3584,7 @@ var NumberInput = React60.forwardRef(
3560
3584
  "aria-label": "Increase",
3561
3585
  disabled: disabled || max != null && current >= max,
3562
3586
  onClick: () => set(current + step),
3563
- className: "flex w-9 shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50",
3587
+ className: "flex w-9 shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
3564
3588
  children: /* @__PURE__ */ jsx66(Plus, { className: "size-4" })
3565
3589
  }
3566
3590
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kinetixui/ui",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "private": false,
5
5
  "description": "KinetixUI React components — CVA + Radix + Tailwind on the KinetixUI token contract. Also distributed via the kinetixui CLI as a component registry.",
6
6
  "keywords": [
@@ -89,7 +89,7 @@
89
89
  "react-dom": ">=18"
90
90
  },
91
91
  "devDependencies": {
92
- "@kinetixui/tokens": "0.4.1",
92
+ "@kinetixui/tokens": "0.4.2",
93
93
  "@testing-library/jest-dom": "^6.6.3",
94
94
  "@testing-library/react": "^16.1.0",
95
95
  "@testing-library/user-event": "^14.5.2",
@@ -27,16 +27,55 @@ const ChartContainer = React.forwardRef<
27
27
  React.ComponentProps<"div"> & {
28
28
  config: ChartConfig;
29
29
  children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
30
+ /**
31
+ * Text alternative for the chart (WCAG 1.1.1). A chart conveys its data by
32
+ * colour + position, which a screen reader can't relay — pass a one-line
33
+ * summary of what it shows and the trend. Falls back to "Chart".
34
+ * Cartesian charts should also get `accessibilityLayer` on the Recharts
35
+ * primitive for keyboard data-point navigation.
36
+ */
37
+ label?: string;
30
38
  }
31
- >(({ id, className, children, config, ...props }, ref) => {
39
+ >(({ id, className, children, config, label, role, "aria-label": ariaLabel, ...props }, ref) => {
32
40
  const uniqueId = React.useId();
33
41
  const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
42
+ const name = ariaLabel ?? label ?? "Chart";
43
+
44
+ const innerRef = React.useRef<HTMLDivElement | null>(null);
45
+ const setRef = React.useCallback(
46
+ (node: HTMLDivElement | null) => {
47
+ innerRef.current = node;
48
+ if (typeof ref === "function") ref(node);
49
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
50
+ },
51
+ [ref],
52
+ );
53
+
54
+ // The container carries the text alternative (role="img" + aria-label).
55
+ // Recharts additionally (a) tags individual sector / dot <path>s with
56
+ // role="img" and no <title> — redundant noise that fails axe's svg-img-alt —
57
+ // and (b) leaves its accessibilityLayer <svg role="application"> unnamed.
58
+ // Fix both on the rendered output; re-run on Recharts re-layout.
59
+ React.useEffect(() => {
60
+ const el = innerRef.current;
61
+ if (!el) return;
62
+ const scrub = () => {
63
+ el.querySelector(".recharts-surface")?.setAttribute("aria-label", name);
64
+ el.querySelectorAll('[role="img"]:not([data-chart])').forEach((n) => n.removeAttribute("role"));
65
+ };
66
+ scrub();
67
+ const mo = new MutationObserver(scrub);
68
+ mo.observe(el, { childList: true, subtree: true });
69
+ return () => mo.disconnect();
70
+ }, [name]);
34
71
 
35
72
  return (
36
73
  <ChartContext.Provider value={{ config }}>
37
74
  <div
38
75
  data-chart={chartId}
39
- ref={ref}
76
+ ref={setRef}
77
+ role={role ?? "img"}
78
+ aria-label={name}
40
79
  className={cn(
41
80
  "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
42
81
  className,
@@ -6,6 +6,9 @@ import { cn } from "../lib/utils";
6
6
 
7
7
  /**
8
8
  * NumberInput — a numeric field with increment / decrement controls.
9
+ * The wrapper shows `--shadow-focus` on `focus-within`; each stepper also
10
+ * draws its own `--ring` inset outline on `:focus-visible` so keyboard users
11
+ * can tell which control is focused.
9
12
  */
10
13
  export interface NumberInputProps
11
14
  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "defaultValue" | "onChange"> {
@@ -48,7 +51,7 @@ const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
48
51
  aria-label="Decrease"
49
52
  disabled={disabled || (min != null && current <= min)}
50
53
  onClick={() => set(current - step)}
51
- className="flex w-9 shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
54
+ className="flex w-9 shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
52
55
  >
53
56
  <Minus className="size-4" />
54
57
  </button>
@@ -73,7 +76,7 @@ const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
73
76
  aria-label="Increase"
74
77
  disabled={disabled || (max != null && current >= max)}
75
78
  onClick={() => set(current + step)}
76
- className="flex w-9 shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
79
+ className="flex w-9 shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
77
80
  >
78
81
  <Plus className="size-4" />
79
82
  </button>