@synerise/ds-radio 1.1.19 → 2.0.0

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
@@ -3,6 +3,16 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [2.0.0](https://github.com/synerise/synerise-design/compare/@synerise/ds-radio@1.1.20...@synerise/ds-radio@2.0.0) (2026-08-26)
7
+
8
+ **Note:** Version bump only for package @synerise/ds-radio
9
+
10
+ ## [1.1.20](https://github.com/synerise/synerise-design/compare/@synerise/ds-radio@1.1.19...@synerise/ds-radio@1.1.20) (2026-08-18)
11
+
12
+ ### Bug Fixes
13
+
14
+ - **radio:** restore antd parity for onChange order, event target and Radio.Group props ([faf5d6a](https://github.com/synerise/synerise-design/commit/faf5d6a969c06cabc80a4e778caeef0da9c5f4a7))
15
+
6
16
  ## [1.1.19](https://github.com/synerise/synerise-design/compare/@synerise/ds-radio@1.1.18...@synerise/ds-radio@1.1.19) (2026-08-11)
7
17
 
8
18
  **Note:** Version bump only for package @synerise/ds-radio
package/CLAUDE.md CHANGED
@@ -40,13 +40,15 @@ Compound component: `Radio` + `Radio.Group` + `Radio.Button`.
40
40
 
41
41
  ### `Radio.Group`
42
42
 
43
- Wraps `AntdRadio.Group` with two additional layout props:
43
+ DS-native single-select context provider:
44
44
 
45
45
  | Prop | Type | Default | Description |
46
46
  |------|------|---------|-------------|
47
47
  | `fullWidth` | `boolean` | `undefined` | Makes the group `display: flex; width: 100%` — each radio button gets `flex: 1` |
48
48
  | `big` | `boolean` | `undefined` | When combined with `fullWidth`, sets button height to `48px` instead of `32px` |
49
49
  | `size` | `'small' \| 'middle' \| 'large'` | `'middle'` | Segmented `Radio.Button` height: `small` 24px · `middle` 32px · `large` 40px |
50
+ | `id`, `role`, `onFocus`, `onBlur`, `onMouseEnter`, `onMouseLeave` | — | — | Forwarded to the group wrapper. `onBlur` matters to form libraries — it is what marks the field touched |
51
+ | `data-*` / `aria-*` | — | — | Forwarded to the wrapper. Other unknown props are **not** — spreading a form field bag passes `type`/`checked`, which would warn on a `div` |
50
52
  | + DS `RadioGroupProps` | — | — | `value`, `defaultValue`, `onChange`, `options`, `optionType`, `buttonStyle`, `disabled`, `name` (DS-native, no antd inheritance) |
51
53
 
52
54
  ### `Radio.Button`
@@ -104,6 +106,7 @@ All styling is in `Radio.styles.tsx` (no LESS).
104
106
 
105
107
  - **`label` vs `children`**: `label` takes precedence — if `label` is set, `children` is ignored entirely. `children` is marked `@deprecated` in the type file.
106
108
  - **`Radio.Button` is DS-native**: reads `RadioContext` for checked state; the segment visual (connected borders, checked, focus, `buttonStyle` solid/outline) is styled by `RadioGroupWrapper`.
107
- - **`RadioChangeEvent`** is synthesised from the native input change (`{ target: { value, checked, name }, stopPropagation, preventDefault, nativeEvent }`); `target.value` is `RadioValueType` (consumers needing a narrower type cast it — antd typed it `any`).
109
+ - **`onChange` order (antd parity)**: a child's own `onChange` fires *before* the group's, for both `Radio` and `Radio.Button`. Consumers let both handlers write the same state and rely on the group's write landing last — reversing the order silently flips last-writer-wins. Covered by tests in `__specs__`.
110
+ - **`RadioChangeEvent`** is synthesised from the native input change. The child builds `target` from its own props (`value`, `checked`, `name`, `disabled`, `type: 'radio'`, `id`/`autoFocus`/`tabIndex` on `Radio`, plus any `data-*`/`aria-*` it was given) and hands it to the group, as antd's rc-checkbox did — so consumers can read more than the value. `target.value` is `RadioValueType` (consumers needing a narrower type cast it — antd typed it `any`).
108
111
  - **`AdditionalData` styled-component** is marked `@deprecated` in its source comment but is still the live wrapper div for `Description` — the comment is misleading.
109
112
  - **Uses Vitest** for testing.
package/dist/Radio.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * context) and `Radio.Button` (segmented control) attached as static members.
4
4
  */
5
5
  declare const Radio: (({ value, checked, defaultChecked, disabled, name, id, autoFocus, tabIndex, onChange, onClick, label, description, children, className, style, ...rest }: import('./Radio.types').RadioProps) => import("react").JSX.Element) & {
6
- Group: ({ value, defaultValue, onChange, options, optionType, buttonStyle, size, disabled, name, fullWidth, big, children, className, style, }: import('./Radio.types').RadioGroupProps) => import("react").JSX.Element;
6
+ Group: ({ value, defaultValue, onChange, options, optionType, buttonStyle, size, disabled, name, fullWidth, big, children, className, style, id, onFocus, onBlur, onMouseEnter, onMouseLeave, ...rest }: import('./Radio.types').RadioGroupProps) => import("react").JSX.Element;
7
7
  Button: ({ value, disabled, children, className, checked, onChange, onClick, style, ...rest }: import('./Radio.types').RadioButtonProps) => import("react").JSX.Element;
8
8
  };
9
9
  export default Radio;
@@ -1,11 +1,14 @@
1
- import { CSSProperties, MouseEventHandler, ReactNode } from 'react';
2
- import { DataAttributes } from '@synerise/ds-utils';
1
+ import { CSSProperties, FocusEventHandler, MouseEventHandler, ReactNode } from 'react';
2
+ import { DataAttributes, PassthroughAttributes } from '@synerise/ds-utils';
3
3
  export type RadioValueType = string | number | boolean;
4
4
  /** antd-compatible radio change event. */
5
5
  export type RadioChangeEventTarget = {
6
6
  value?: RadioValueType;
7
7
  checked: boolean;
8
8
  name?: string;
9
+ id?: string;
10
+ disabled?: boolean;
11
+ type?: string;
9
12
  [key: string]: unknown;
10
13
  };
11
14
  export type RadioChangeEvent = {
@@ -59,7 +62,13 @@ export type RadioGroupProps = {
59
62
  children?: ReactNode;
60
63
  className?: string;
61
64
  style?: CSSProperties;
62
- } & DataAttributes;
65
+ id?: string;
66
+ role?: string;
67
+ onFocus?: FocusEventHandler<HTMLDivElement>;
68
+ onBlur?: FocusEventHandler<HTMLDivElement>;
69
+ onMouseEnter?: MouseEventHandler<HTMLDivElement>;
70
+ onMouseLeave?: MouseEventHandler<HTMLDivElement>;
71
+ } & PassthroughAttributes;
63
72
  export type RadioButtonProps = {
64
73
  value?: RadioValueType;
65
74
  disabled?: boolean;
@@ -1,9 +1,12 @@
1
- import { RadioValueType } from './Radio.types';
1
+ import { RadioChangeEventTarget, RadioValueType } from './Radio.types';
2
2
  export type RadioGroupContextValue = {
3
3
  /** Currently selected value (single). */
4
4
  value?: RadioValueType;
5
- /** Select a value; the group builds the change event + calls the consumer onChange. */
6
- onChange: (value: RadioValueType, nativeEvent: Event) => void;
5
+ /**
6
+ * Select a value; the group wraps `target` in the change event + calls the consumer onChange.
7
+ * `target` is built by the child from its own props, as antd's rc-checkbox did.
8
+ */
9
+ onChange: (value: RadioValueType, nativeEvent: Event, target: RadioChangeEventTarget) => void;
7
10
  disabled?: boolean;
8
11
  name?: string;
9
12
  /** `'button'` makes the group render segmented `Radio.Button`s. */
@@ -5,5 +5,5 @@ import { RadioGroupProps } from './Radio.types';
5
5
  * read, and emits `onChange(RadioChangeEvent)` (`target.value` = the selected value). Controlled via
6
6
  * `value`, uncontrolled via `defaultValue`. `optionType="button"` renders segmented `Radio.Button`s.
7
7
  */
8
- declare const RadioGroup: ({ value, defaultValue, onChange, options, optionType, buttonStyle, size, disabled, name, fullWidth, big, children, className, style, }: RadioGroupProps) => React.JSX.Element;
8
+ declare const RadioGroup: ({ value, defaultValue, onChange, options, optionType, buttonStyle, size, disabled, name, fullWidth, big, children, className, style, id, onFocus, onBlur, onMouseEnter, onMouseLeave, ...rest }: RadioGroupProps) => React.JSX.Element;
9
9
  export default RadioGroup;
@@ -5,6 +5,7 @@ import { RadioGroupContext } from "./RadioContext.js";
5
5
  import { RadioBase } from "./components/RadioBase.js";
6
6
  import { RadioButton } from "./components/RadioButton.js";
7
7
  const cx = (...classes) => classes.filter(Boolean).join(" ");
8
+ const passthroughAttributes = (props) => Object.fromEntries(Object.entries(props).filter(([key]) => key.startsWith("data-") || key.startsWith("aria-") || key === "role"));
8
9
  const RadioGroup = ({
9
10
  value,
10
11
  defaultValue,
@@ -19,27 +20,37 @@ const RadioGroup = ({
19
20
  big,
20
21
  children,
21
22
  className,
22
- style
23
+ style,
24
+ id,
25
+ onFocus,
26
+ onBlur,
27
+ onMouseEnter,
28
+ onMouseLeave,
29
+ ...rest
23
30
  }) => {
24
31
  const isControlled = value !== void 0;
25
32
  const [internalValue, setInternalValue] = useState(defaultValue);
26
33
  const resolvedValue = isControlled ? value : internalValue;
27
- const handleChange = useCallback((newValue, nativeEvent) => {
34
+ const handleChange = useCallback((newValue, nativeEvent, target) => {
35
+ const lastValue = resolvedValue;
28
36
  if (!isControlled) {
29
37
  setInternalValue(newValue);
30
38
  }
39
+ if (newValue === lastValue) {
40
+ return;
41
+ }
31
42
  const event = {
43
+ // the child builds the target from its own props (antd parity); the group only owns `name`
32
44
  target: {
33
- value: newValue,
34
- checked: true,
35
- name
45
+ ...target,
46
+ name: target.name ?? name
36
47
  },
37
48
  stopPropagation: () => nativeEvent.stopPropagation?.(),
38
49
  preventDefault: () => nativeEvent.preventDefault?.(),
39
50
  nativeEvent
40
51
  };
41
52
  onChange?.(event);
42
- }, [isControlled, name, onChange]);
53
+ }, [isControlled, name, onChange, resolvedValue]);
43
54
  const contextValue = useMemo(() => ({
44
55
  value: resolvedValue,
45
56
  onChange: handleChange,
@@ -54,7 +65,7 @@ const RadioGroup = ({
54
65
  value: option
55
66
  });
56
67
  const renderOptions = () => normalizedOptions?.map((option) => optionType === "button" ? /* @__PURE__ */ jsx(RadioButton, { value: option.value, disabled: option.disabled, children: option.label }, String(option.value)) : /* @__PURE__ */ jsx(RadioBase, { value: option.value, disabled: option.disabled, label: option.label }, String(option.value)));
57
- return /* @__PURE__ */ jsx(RadioGroupContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(RadioGroupWrapper, { className: cx("ant-radio-group", `ant-radio-group-${buttonStyle}`, "ds-radio-group", className), style, fullWidth, big, children: normalizedOptions ? renderOptions() : children }) });
68
+ return /* @__PURE__ */ jsx(RadioGroupContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(RadioGroupWrapper, { ...passthroughAttributes(rest), className: cx("ant-radio-group", `ant-radio-group-${buttonStyle}`, "ds-radio-group", className), style, id, onFocus, onBlur, onMouseEnter, onMouseLeave, fullWidth, big, children: normalizedOptions ? renderOptions() : children }) });
58
69
  };
59
70
  export {
60
71
  RadioGroup as default
@@ -31,24 +31,28 @@ const RadioBase = ({
31
31
  if (isDisabled) {
32
32
  return;
33
33
  }
34
- if (group) {
35
- group.onChange(value, event.nativeEvent);
36
- return;
37
- }
38
- if (!isControlled) {
34
+ if (!group && !isControlled) {
39
35
  setInternalChecked(true);
40
36
  }
37
+ const target = {
38
+ ...rest,
39
+ value,
40
+ checked: event.target.checked,
41
+ name: name ?? group?.name,
42
+ id,
43
+ disabled: isDisabled,
44
+ type: "radio",
45
+ autoFocus,
46
+ tabIndex
47
+ };
41
48
  const changeEvent = {
42
- target: {
43
- value,
44
- checked: true,
45
- name
46
- },
49
+ target,
47
50
  stopPropagation: () => event.stopPropagation(),
48
51
  preventDefault: () => event.preventDefault(),
49
52
  nativeEvent: event.nativeEvent
50
53
  };
51
54
  onChange?.(changeEvent);
55
+ group?.onChange(value, event.nativeEvent, target);
52
56
  };
53
57
  return /* @__PURE__ */ jsxs(RadioWrapper, { className: cx("ds-radio-field", className), style, children: [
54
58
  /* @__PURE__ */ jsxs(RadioLabel, { className: cx("ant-radio-wrapper", "ds-radio", isChecked && "ant-radio-wrapper-checked", isDisabled && "ant-radio-wrapper-disabled"), $checked: isChecked, $disabled: isDisabled, onClick, children: [
@@ -22,22 +22,24 @@ const RadioButton = ({
22
22
  if (isDisabled) {
23
23
  return;
24
24
  }
25
- if (group) {
26
- group.onChange(value, event.nativeEvent);
27
- }
25
+ const target = {
26
+ ...rest,
27
+ value,
28
+ checked: event.target.checked,
29
+ name: group?.name,
30
+ disabled: isDisabled,
31
+ type: "radio"
32
+ };
28
33
  if (onChange) {
29
34
  const changeEvent = {
30
- target: {
31
- value,
32
- checked: true,
33
- name: group?.name
34
- },
35
+ target,
35
36
  stopPropagation: () => event.stopPropagation(),
36
37
  preventDefault: () => event.preventDefault(),
37
38
  nativeEvent: event.nativeEvent
38
39
  };
39
40
  onChange(changeEvent);
40
41
  }
42
+ group?.onChange(value, event.nativeEvent, target);
41
43
  };
42
44
  return /* @__PURE__ */ jsxs(RadioButtonLabel, { className: cx("ant-radio-button-wrapper", "ds-radio-button", isChecked && "ant-radio-button-wrapper-checked", isDisabled && "ant-radio-button-wrapper-disabled", className), style, onClick, $checked: isChecked, $disabled: isDisabled, $solid: group?.buttonStyle === "solid", $size: group?.size, children: [
43
45
  /* @__PURE__ */ jsx("span", { className: cx("ant-radio-button", isChecked && "ant-radio-button-checked", isDisabled && "ant-radio-button-disabled"), children: /* @__PURE__ */ jsx(RadioButtonInput, { type: "radio", className: "ant-radio-button-input ds-radio-button-input", checked: isChecked, disabled: isDisabled, name: group?.name, value: value === void 0 ? void 0 : String(value), onChange: handleChange, ...rest }) }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synerise/ds-radio",
3
- "version": "1.1.19",
3
+ "version": "2.0.0",
4
4
  "description": "Radio UI Component for the Synerise Design System",
5
5
  "license": "ISC",
6
6
  "repository": "synerise/synerise-design",
@@ -42,9 +42,9 @@
42
42
  ],
43
43
  "types": "dist/index.d.ts",
44
44
  "dependencies": {
45
- "@synerise/ds-form-field": "^1.3.25",
46
- "@synerise/ds-typography": "^1.1.28",
47
- "@synerise/ds-utils": "^1.10.2"
45
+ "@synerise/ds-form-field": "^2.0.0",
46
+ "@synerise/ds-typography": "^2.0.0",
47
+ "@synerise/ds-utils": "^2.0.0"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@synerise/ds-core": "*",
@@ -52,5 +52,5 @@
52
52
  "styled-components": "^5.3.3",
53
53
  "vitest": "4"
54
54
  },
55
- "gitHead": "6df24ed12cd5f276d8eccde730591807982e3385"
55
+ "gitHead": "033854a18f038079e9a16b0ff250f8401a2287c2"
56
56
  }