@kennycorrea/ui 0.6.0 → 0.6.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.1 — 2026-10-08
4
+
5
+ ### Fixed
6
+ - **`Slider` thumbs always have an accessible name.** Without `fillFrom`, `label` landed on the
7
+ Themes root, which has no role, and the `role="slider"` thumb had none (axe:
8
+ aria-input-field-name). The thumb is now named `label`; a range's thumbs are
9
+ `"<label>, minimum"` / `"<label>, maximum"` (`"<label>, n of m"` beyond two) instead of
10
+ Radix's bare "Minimum"/"Maximum". Consumers using `fillFrom={min}` only to get a name can
11
+ drop it.
12
+
3
13
  ## 0.6.0 — 2026-10-07
4
14
 
5
15
  ### Added
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as SliderPrimitive from '@radix-ui/react-slider';
3
3
  import { Slider as ThemedSlider } from '@radix-ui/themes';
4
- import { forwardRef, useState } from 'react';
4
+ import { forwardRef, useCallback, useLayoutEffect, useRef, useState, } from 'react';
5
5
  import { cn } from '../../utils/cn.js';
6
6
  import { fillEdges, fillStyle, percentOf, thumbCentre } from './geometry.js';
7
7
  const MARGIN_KEYS = ['m', 'mx', 'my', 'mt', 'mr', 'mb', 'ml'];
@@ -30,14 +30,54 @@ export const Slider = forwardRef(function Slider(props, ref) {
30
30
  setInner(next);
31
31
  onValueChange?.(next);
32
32
  };
33
+ // Themes spreads every prop onto the Root, which has no role, and gives its Thumbs no way in —
34
+ // so the role="slider" elements are named here, after Radix has rendered them.
35
+ const themedRoot = useRef(null);
36
+ const setThemedRoot = useCallback((node) => {
37
+ themedRoot.current = node;
38
+ assignRef(ref, node);
39
+ }, [ref]);
40
+ // Radix labels a range's thumbs "Minimum"/"Maximum" in a later render of its own, so a
41
+ // one-off write is overwritten; the observer puts ours back whenever anything touches them.
42
+ useLayoutEffect(() => {
43
+ const root = themedRoot.current;
44
+ if (!root)
45
+ return;
46
+ const name = () => {
47
+ const thumbs = root.querySelectorAll('[role="slider"]');
48
+ thumbs.forEach((thumb, i) => {
49
+ const wanted = thumbLabel(label, i, thumbs.length);
50
+ if (thumb.getAttribute('aria-label') !== wanted)
51
+ thumb.setAttribute('aria-label', wanted);
52
+ });
53
+ };
54
+ name();
55
+ const observer = new MutationObserver(name);
56
+ observer.observe(root, { subtree: true, childList: true, attributeFilter: ['aria-label'] });
57
+ return () => observer.disconnect();
58
+ }, [label]);
33
59
  const unsupported = fillFrom === undefined ? null : fillFromUnsupported(props, values.length);
34
60
  if (unsupported && isDev)
35
61
  console.warn(`Slider: fillFrom is ignored — ${unsupported}.`);
36
- const slider = fillFrom !== undefined && !unsupported ? (_jsx(FillFromSlider, { ref: ref, ...rest, label: label, className: className, values: values, fillFrom: fillFrom, min: min, max: max, onValueChange: handleChange })) : (_jsx(ThemedSlider, { ref: ref, size: "2", "aria-label": label, className: cn(className), ...rest, onValueChange: handleChange }));
62
+ const slider = fillFrom !== undefined && !unsupported ? (_jsx(FillFromSlider, { ref: ref, ...rest, label: label, className: className, values: values, fillFrom: fillFrom, min: min, max: max, onValueChange: handleChange })) : (_jsx(ThemedSlider, { ref: setThemedRoot, size: "2", "aria-label": label, className: cn(className), ...rest, onValueChange: handleChange }));
37
63
  if (!marks?.length)
38
64
  return slider;
39
65
  return (_jsxs("div", { className: "ui-slider-field", children: [_jsx(SliderMarks, { marks: marks, min: min, max: max }), slider] }));
40
66
  });
67
+ /** One thumb takes the label; a range says which end each thumb is. */
68
+ function thumbLabel(label, index, count) {
69
+ if (count === 1)
70
+ return label;
71
+ if (count === 2)
72
+ return `${label}, ${index === 0 ? 'minimum' : 'maximum'}`;
73
+ return `${label}, ${index + 1} of ${count}`;
74
+ }
75
+ function assignRef(ref, value) {
76
+ if (typeof ref === 'function')
77
+ ref(value);
78
+ else if (ref)
79
+ ref.current = value;
80
+ }
41
81
  /**
42
82
  * Radix Themes draws its range itself and offers no way to place it, so this path is built on the
43
83
  * primitives and styled in `tokens.css` (`.ui-slider*`) from Themes' public tokens — never by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kennycorrea/ui",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Upfront's house component layer: atoms over Radix Themes, the token bridge, and the accent logic.",
5
5
  "private": false,
6
6
  "type": "module",