@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 +10 -0
- package/CLAUDE.md +5 -2
- package/dist/Radio.d.ts +1 -1
- package/dist/Radio.types.d.ts +12 -3
- package/dist/RadioContext.d.ts +6 -3
- package/dist/RadioGroup.d.ts +1 -1
- package/dist/RadioGroup.js +18 -7
- package/dist/components/RadioBase.js +14 -10
- package/dist/components/RadioButton.js +10 -8
- package/package.json +5 -5
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
|
-
|
|
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
|
-
- **`
|
|
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;
|
package/dist/Radio.types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/RadioContext.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
6
|
-
|
|
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. */
|
package/dist/RadioGroup.d.ts
CHANGED
|
@@ -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;
|
package/dist/RadioGroup.js
CHANGED
|
@@ -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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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": "
|
|
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": "^
|
|
46
|
-
"@synerise/ds-typography": "^
|
|
47
|
-
"@synerise/ds-utils": "^
|
|
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": "
|
|
55
|
+
"gitHead": "033854a18f038079e9a16b0ff250f8401a2287c2"
|
|
56
56
|
}
|