@recursica/mantine-adapter 0.10.0 → 0.12.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +6 -28
  3. package/dist/mantine-adapter.cjs +1 -1
  4. package/dist/mantine-adapter.cjs.map +1 -1
  5. package/dist/mantine-adapter.css +1 -1
  6. package/dist/mantine-adapter.js +1650 -1344
  7. package/dist/mantine-adapter.js.map +1 -1
  8. package/dist/src/components/Checkbox/Checkbox.d.ts +3 -1
  9. package/dist/src/components/FormControlLayout/FormControlLayout.d.ts +21 -0
  10. package/dist/src/components/FormControlWrapper/FormControlWrapper.d.ts +2 -0
  11. package/dist/src/components/Label/Label.d.ts +1 -3
  12. package/dist/src/components/Menu/Menu.d.ts +85 -3
  13. package/dist/src/components/Radio/Radio.d.ts +3 -1
  14. package/dist/src/components/ReadOnlyField/ReadOnlyBooleanField.d.ts +5 -0
  15. package/dist/src/components/ReadOnlyField/ReadOnlyField.d.ts +6 -6
  16. package/dist/src/components/ReadOnlyField/ReadOnlySwitchField.d.ts +5 -0
  17. package/dist/src/components/ReadOnlyField/WithReadOnlyWrapper.d.ts +7 -1
  18. package/dist/src/components/ReadOnlyField/index.d.ts +2 -0
  19. package/dist/src/components/Switch/Switch.d.ts +3 -1
  20. package/dist/src/components/index.d.ts +1 -0
  21. package/dist/src/utils/RequireAccessibleLabel.d.ts +18 -0
  22. package/package.json +1 -1
  23. package/src/components/AssistiveElement/AssistiveElement.stories.tsx +6 -4
  24. package/src/components/Avatar/Avatar.stories.tsx +10 -5
  25. package/src/components/Badge/Badge.stories.tsx +2 -1
  26. package/src/components/Button/Button.stories.tsx +2 -4
  27. package/src/components/Checkbox/Checkbox.module.css +1 -1
  28. package/src/components/Checkbox/Checkbox.stories.tsx +24 -3
  29. package/src/components/Checkbox/Checkbox.tsx +72 -15
  30. package/src/components/Checkbox/CheckboxGroup.stories.tsx +26 -25
  31. package/src/components/Checkbox/CheckboxGroup.tsx +38 -58
  32. package/src/components/Container/Container.stories.tsx +6 -3
  33. package/src/components/DatePicker/DatePicker.stories.tsx +1 -5
  34. package/src/components/Dropdown/Dropdown.stories.tsx +0 -3
  35. package/src/components/Dropdown/Dropdown.tsx +6 -5
  36. package/src/components/FileInput/FileInput.stories.tsx +1 -5
  37. package/src/components/FileUpload/FileUpload.stories.tsx +1 -5
  38. package/src/components/Flex/Flex.stories.tsx +6 -3
  39. package/src/components/FormControlLayout/FormControlLayout.module.css +80 -0
  40. package/src/components/FormControlLayout/FormControlLayout.stories.tsx +82 -0
  41. package/src/components/FormControlLayout/FormControlLayout.tsx +78 -0
  42. package/src/components/FormControlWrapper/FormControlWrapper.module.css +3 -41
  43. package/src/components/FormControlWrapper/FormControlWrapper.stories.tsx +18 -5
  44. package/src/components/FormControlWrapper/FormControlWrapper.tsx +28 -41
  45. package/src/components/Group/Group.stories.tsx +6 -3
  46. package/src/components/Label/Label.module.css +0 -37
  47. package/src/components/Label/Label.stories.tsx +8 -9
  48. package/src/components/Label/Label.tsx +2 -7
  49. package/src/components/Loader/Loader.stories.tsx +10 -5
  50. package/src/components/Menu/MENU_IMPLEMENTATION_NOTES.md +78 -0
  51. package/src/components/Menu/Menu.module.css +244 -0
  52. package/src/components/Menu/Menu.stories.tsx +309 -5
  53. package/src/components/Menu/Menu.tsx +317 -4
  54. package/src/components/NumberInput/NumberInput.stories.tsx +1 -5
  55. package/src/components/Radio/Radio.stories.tsx +22 -8
  56. package/src/components/Radio/Radio.tsx +134 -75
  57. package/src/components/Radio/RadioGroup.stories.tsx +26 -7
  58. package/src/components/Radio/RadioGroup.tsx +37 -57
  59. package/src/components/ReadOnlyField/ReadOnlyBooleanField.tsx +21 -0
  60. package/src/components/ReadOnlyField/ReadOnlyField.stories.tsx +43 -0
  61. package/src/components/ReadOnlyField/ReadOnlyField.tsx +30 -3
  62. package/src/components/ReadOnlyField/ReadOnlySwitchField.tsx +21 -0
  63. package/src/components/ReadOnlyField/ReadOnlyTextField.tsx +3 -1
  64. package/src/components/ReadOnlyField/WithReadOnlyWrapper.tsx +10 -2
  65. package/src/components/ReadOnlyField/index.ts +2 -0
  66. package/src/components/SegmentedControl/SegmentedControl.stories.tsx +1 -5
  67. package/src/components/Slider/Slider.stories.tsx +1 -5
  68. package/src/components/Stack/Stack.stories.tsx +6 -3
  69. package/src/components/Switch/Switch.module.css +2 -10
  70. package/src/components/Switch/Switch.stories.tsx +42 -27
  71. package/src/components/Switch/Switch.tsx +138 -89
  72. package/src/components/Switch/SwitchGroup.stories.tsx +28 -8
  73. package/src/components/Switch/SwitchGroup.tsx +37 -57
  74. package/src/components/TextArea/TextArea.stories.tsx +0 -3
  75. package/src/components/TextArea/TextArea.tsx +6 -5
  76. package/src/components/TextField/TextField.tsx +6 -5
  77. package/src/components/TimePicker/TimePicker.stories.tsx +1 -5
  78. package/src/components/index.ts +1 -0
  79. package/src/utils/RequireAccessibleLabel.ts +12 -0
@@ -1,7 +1,320 @@
1
- import React from "react";
1
+ import { forwardRef } from "react";
2
+ import {
3
+ Menu as MantineMenu,
4
+ type MenuProps as MantineMenuProps,
5
+ type MenuTargetProps as MantineMenuTargetProps,
6
+ type MenuDropdownProps as MantineMenuDropdownProps,
7
+ type MenuItemProps as MantineMenuItemProps,
8
+ type MenuDividerProps as MantineMenuDividerProps,
9
+ type MenuLabelProps as MantineMenuLabelProps,
10
+ type MenuSubProps as MantineMenuSubProps,
11
+ type MenuSubTargetProps as MantineMenuSubTargetProps,
12
+ type MenuSubDropdownProps as MantineMenuSubDropdownProps,
13
+ } from "@mantine/core";
14
+ import {
15
+ filterStylingProps,
16
+ type RecursicaOverStyled,
17
+ } from "../../utils/filterStylingProps";
18
+ import styles from "./Menu.module.css";
2
19
 
3
- export type MenuProps = React.HTMLAttributes<HTMLDivElement>;
20
+ // ============================================================
21
+ // MENU ROOT
22
+ // ============================================================
4
23
 
5
- export const Menu: React.FC<MenuProps> = (props) => {
6
- return <div {...props}>Menu</div>;
24
+ /**
25
+ * Recursica Menu component wrapping Mantine's composable Menu.
26
+ *
27
+ * Usage follows the composable dot-notation pattern:
28
+ * ```tsx
29
+ * <Menu>
30
+ * <Menu.Target><Button>Open</Button></Menu.Target>
31
+ * <Menu.Dropdown>
32
+ * <Menu.Label>Section</Menu.Label>
33
+ * <Menu.Item>Action</Menu.Item>
34
+ * <Menu.Divider />
35
+ * <Menu.Item>Another action</Menu.Item>
36
+ * </Menu.Dropdown>
37
+ * </Menu>
38
+ * ```
39
+ */
40
+ export type MenuProps = RecursicaOverStyled<MantineMenuProps>;
41
+
42
+ const MenuBase = function Menu({ overStyled = false, ...rest }: MenuProps) {
43
+ const sanitizedProps = filterStylingProps(rest, overStyled);
44
+
45
+ // Bind CSS module classes to Mantine's internal classNames API
46
+ const mergedClassNames: Partial<Record<string, string>> = {
47
+ dropdown: styles.dropdown,
48
+ item: styles.item,
49
+ itemLabel: styles.itemLabel,
50
+ itemSection: styles.itemSection,
51
+ divider: styles.divider,
52
+ label: styles.label,
53
+ chevron: styles.chevron,
54
+ };
55
+
56
+ const classNamesProp = (sanitizedProps as Record<string, unknown>).classNames;
57
+ if (
58
+ classNamesProp &&
59
+ typeof classNamesProp === "object" &&
60
+ !Array.isArray(classNamesProp)
61
+ ) {
62
+ const o = classNamesProp as Record<string, string>;
63
+ Object.keys(o).forEach((key) => {
64
+ if (mergedClassNames[key]) {
65
+ mergedClassNames[key] = `${mergedClassNames[key]} ${o[key]}`;
66
+ } else {
67
+ mergedClassNames[key] = o[key];
68
+ }
69
+ });
70
+ }
71
+
72
+ return (
73
+ <MantineMenu
74
+ classNames={mergedClassNames}
75
+ {...(sanitizedProps as unknown as MantineMenuProps)}
76
+ />
77
+ );
7
78
  };
79
+ MenuBase.displayName = "Menu";
80
+
81
+ // ============================================================
82
+ // MENU TARGET
83
+ // ============================================================
84
+
85
+ /**
86
+ * Wrapper for the element that triggers the menu.
87
+ * Requires a single child element that supports ref forwarding.
88
+ */
89
+ export type MenuTargetProps = MantineMenuTargetProps;
90
+
91
+ const MenuTarget = function MenuTarget(props: MenuTargetProps) {
92
+ return <MantineMenu.Target {...props} />;
93
+ };
94
+ MenuTarget.displayName = "MenuTarget";
95
+
96
+ // ============================================================
97
+ // MENU DROPDOWN
98
+ // ============================================================
99
+
100
+ /** The dropdown panel containing menu items, dividers, and labels. */
101
+ export type MenuDropdownProps = RecursicaOverStyled<MantineMenuDropdownProps>;
102
+
103
+ const MenuDropdown = forwardRef<HTMLDivElement, MenuDropdownProps>(
104
+ function MenuDropdown({ overStyled = false, ...rest }, ref) {
105
+ const sanitizedProps = filterStylingProps(rest, overStyled);
106
+ const classNameProp = (sanitizedProps as Record<string, unknown>)
107
+ .className as string | undefined;
108
+
109
+ return (
110
+ <MantineMenu.Dropdown
111
+ ref={ref}
112
+ className={classNameProp}
113
+ {...(sanitizedProps as unknown as MantineMenuDropdownProps)}
114
+ />
115
+ );
116
+ },
117
+ );
118
+ MenuDropdown.displayName = "MenuDropdown";
119
+
120
+ // ============================================================
121
+ // MENU ITEM
122
+ // ============================================================
123
+
124
+ /**
125
+ * An individual actionable item within the menu dropdown.
126
+ *
127
+ * **Note:** Mantine's `color` prop is stripped in strict mode to enforce
128
+ * design token adherence. Use `overStyled={true}` if you need to bypass.
129
+ */
130
+ export type MenuItemProps = RecursicaOverStyled<
131
+ Omit<MantineMenuItemProps, "color">
132
+ >;
133
+
134
+ const MenuItem = forwardRef<HTMLButtonElement, MenuItemProps>(function MenuItem(
135
+ { overStyled = false, ...rest },
136
+ ref,
137
+ ) {
138
+ const sanitizedProps = filterStylingProps(rest, overStyled);
139
+ const restRecord = sanitizedProps as Record<string, unknown>;
140
+
141
+ // Strip Mantine's `color` prop to enforce token-driven styling
142
+ if (!overStyled) {
143
+ delete restRecord["color"];
144
+ }
145
+
146
+ const classNameProp = restRecord.className as string | undefined;
147
+
148
+ return (
149
+ <MantineMenu.Item
150
+ ref={ref}
151
+ className={classNameProp}
152
+ {...(sanitizedProps as unknown as MantineMenuItemProps)}
153
+ />
154
+ );
155
+ });
156
+ MenuItem.displayName = "MenuItem";
157
+
158
+ // ============================================================
159
+ // MENU DIVIDER
160
+ // ============================================================
161
+
162
+ /** A visual separator between groups of menu items. */
163
+ export type MenuDividerProps = RecursicaOverStyled<MantineMenuDividerProps>;
164
+
165
+ const MenuDivider = forwardRef<HTMLHRElement, MenuDividerProps>(
166
+ function MenuDivider({ overStyled = false, ...rest }, ref) {
167
+ const sanitizedProps = filterStylingProps(rest, overStyled);
168
+ const classNameProp = (sanitizedProps as Record<string, unknown>)
169
+ .className as string | undefined;
170
+
171
+ return (
172
+ <MantineMenu.Divider
173
+ ref={ref}
174
+ className={classNameProp}
175
+ {...(sanitizedProps as unknown as MantineMenuDividerProps)}
176
+ />
177
+ );
178
+ },
179
+ );
180
+ MenuDivider.displayName = "MenuDivider";
181
+
182
+ // ============================================================
183
+ // MENU LABEL
184
+ // ============================================================
185
+
186
+ /** A non-interactive section label used to categorize groups of menu items. */
187
+ export type MenuLabelProps = RecursicaOverStyled<MantineMenuLabelProps>;
188
+
189
+ const MenuLabel = forwardRef<HTMLDivElement, MenuLabelProps>(function MenuLabel(
190
+ { overStyled = false, ...rest },
191
+ ref,
192
+ ) {
193
+ const sanitizedProps = filterStylingProps(rest, overStyled);
194
+ const classNameProp = (sanitizedProps as Record<string, unknown>)
195
+ .className as string | undefined;
196
+
197
+ return (
198
+ <MantineMenu.Label
199
+ ref={ref}
200
+ className={classNameProp}
201
+ {...(sanitizedProps as unknown as MantineMenuLabelProps)}
202
+ />
203
+ );
204
+ });
205
+ MenuLabel.displayName = "MenuLabel";
206
+
207
+ // ============================================================
208
+ // MENU SUB (SUBMENU CONTAINER)
209
+ // ============================================================
210
+
211
+ /** Container for a submenu, wrapping Menu.Sub.Target and Menu.Sub.Dropdown. */
212
+ export type MenuSubProps = MantineMenuSubProps;
213
+
214
+ const MenuSubBase = function MenuSub(props: MenuSubProps) {
215
+ return <MantineMenu.Sub {...props} />;
216
+ };
217
+ MenuSubBase.displayName = "MenuSub";
218
+
219
+ // ============================================================
220
+ // MENU SUB TARGET
221
+ // ============================================================
222
+
223
+ /** Wrapper for the element that triggers the submenu. */
224
+ export type MenuSubTargetProps = MantineMenuSubTargetProps;
225
+
226
+ const MenuSubTarget = function MenuSubTarget(props: MenuSubTargetProps) {
227
+ return <MantineMenu.Sub.Target {...props} />;
228
+ };
229
+ MenuSubTarget.displayName = "MenuSubTarget";
230
+
231
+ // ============================================================
232
+ // MENU SUB ITEM (triggers submenu)
233
+ // ============================================================
234
+
235
+ /**
236
+ * A menu item that acts as a submenu trigger.
237
+ * Renders within Menu.Sub.Target and displays a chevron indicator.
238
+ */
239
+ export type MenuSubItemProps = RecursicaOverStyled<
240
+ Omit<MantineMenuItemProps, "color">
241
+ >;
242
+
243
+ const MenuSubItem = forwardRef<HTMLButtonElement, MenuSubItemProps>(
244
+ function MenuSubItem({ overStyled = false, ...rest }, ref) {
245
+ const sanitizedProps = filterStylingProps(rest, overStyled);
246
+ const restRecord = sanitizedProps as Record<string, unknown>;
247
+
248
+ if (!overStyled) {
249
+ delete restRecord["color"];
250
+ }
251
+
252
+ const classNameProp = restRecord.className as string | undefined;
253
+
254
+ return (
255
+ <MantineMenu.Sub.Item
256
+ ref={ref}
257
+ className={classNameProp}
258
+ {...(sanitizedProps as unknown as MantineMenuItemProps)}
259
+ />
260
+ );
261
+ },
262
+ );
263
+ MenuSubItem.displayName = "MenuSubItem";
264
+
265
+ // ============================================================
266
+ // MENU SUB DROPDOWN
267
+ // ============================================================
268
+
269
+ /** The dropdown panel for a submenu. */
270
+ export type MenuSubDropdownProps =
271
+ RecursicaOverStyled<MantineMenuSubDropdownProps>;
272
+
273
+ const MenuSubDropdown = forwardRef<HTMLDivElement, MenuSubDropdownProps>(
274
+ function MenuSubDropdown({ overStyled = false, ...rest }, ref) {
275
+ const sanitizedProps = filterStylingProps(rest, overStyled);
276
+ const classNameProp = (sanitizedProps as Record<string, unknown>)
277
+ .className as string | undefined;
278
+
279
+ return (
280
+ <MantineMenu.Sub.Dropdown
281
+ ref={ref}
282
+ className={classNameProp}
283
+ {...(sanitizedProps as unknown as MantineMenuSubDropdownProps)}
284
+ />
285
+ );
286
+ },
287
+ );
288
+ MenuSubDropdown.displayName = "MenuSubDropdown";
289
+
290
+ // ============================================================
291
+ // DOT NOTATION EXPORT
292
+ // ============================================================
293
+
294
+ type MenuSubComponent = typeof MenuSubBase & {
295
+ Target: typeof MenuSubTarget;
296
+ Item: typeof MenuSubItem;
297
+ Dropdown: typeof MenuSubDropdown;
298
+ };
299
+
300
+ const MenuSub = MenuSubBase as MenuSubComponent;
301
+ MenuSub.Target = MenuSubTarget;
302
+ MenuSub.Item = MenuSubItem;
303
+ MenuSub.Dropdown = MenuSubDropdown;
304
+
305
+ type MenuComponent = typeof MenuBase & {
306
+ Target: typeof MenuTarget;
307
+ Dropdown: typeof MenuDropdown;
308
+ Item: typeof MenuItem;
309
+ Divider: typeof MenuDivider;
310
+ Label: typeof MenuLabel;
311
+ Sub: MenuSubComponent;
312
+ };
313
+
314
+ export const Menu = MenuBase as MenuComponent;
315
+ Menu.Target = MenuTarget;
316
+ Menu.Dropdown = MenuDropdown;
317
+ Menu.Item = MenuItem;
318
+ Menu.Divider = MenuDivider;
319
+ Menu.Label = MenuLabel;
320
+ Menu.Sub = MenuSub;
@@ -6,11 +6,7 @@ const meta: Meta<typeof NumberInput> = {
6
6
  title: "UI-Kit/🚧 NumberInput",
7
7
  component: NumberInput,
8
8
  tags: ["autodocs"],
9
- argTypes: {
10
- disabled: {
11
- control: "boolean",
12
- },
13
- },
9
+ argTypes: {},
14
10
  };
15
11
 
16
12
  export default meta;
@@ -18,9 +18,6 @@ The \`Radio\` component represents a fundamental standalone boolean selection no
18
18
  },
19
19
  },
20
20
  argTypes: {
21
- disabled: {
22
- control: "boolean",
23
- },
24
21
  checked: {
25
22
  control: "boolean",
26
23
  },
@@ -33,6 +30,8 @@ The \`Radio\` component represents a fundamental standalone boolean selection no
33
30
  defaultChecked: {
34
31
  control: "boolean",
35
32
  },
33
+ controlMaxWidth: { table: { disable: true } },
34
+ controlMinWidth: { table: { disable: true } },
36
35
  },
37
36
  };
38
37
 
@@ -46,6 +45,13 @@ export const Default: Story = {
46
45
  },
47
46
  };
48
47
 
48
+ export const SideBySideLayout: Story = {
49
+ args: {
50
+ label: "Opt-in form alignment",
51
+ formLayout: "side-by-side",
52
+ },
53
+ };
54
+
49
55
  export const CheckedState: Story = {
50
56
  args: {
51
57
  ...Default.args,
@@ -68,9 +74,17 @@ export const DisabledChecked: Story = {
68
74
  },
69
75
  };
70
76
 
71
- export const ReadOnlyStandalone: Story = {
72
- args: {
73
- readOnly: true,
74
- readOnlyComponent: <span>Locked Native Value</span>,
75
- },
77
+ export const ReadOnly: Story = {
78
+ args: {},
79
+ render: () => (
80
+ <div
81
+ style={{
82
+ display: "flex",
83
+ flexDirection: "column",
84
+ gap: "24px",
85
+ }}
86
+ >
87
+ <Radio label="Account Type" defaultChecked readOnly />
88
+ </div>
89
+ ),
76
90
  };
@@ -9,6 +9,12 @@ import {
9
9
  filterStylingProps,
10
10
  type RecursicaOverStyled,
11
11
  } from "../../utils/filterStylingProps";
12
+ import { type RequireAccessibleLabel } from "../../utils/RequireAccessibleLabel";
13
+ import {
14
+ FormControlLayout,
15
+ type FormControlLayoutProps,
16
+ } from "../FormControlLayout/FormControlLayout";
17
+
12
18
  import styles from "./Radio.module.css";
13
19
 
14
20
  const RadioIcon: React.FC<{
@@ -26,11 +32,17 @@ const RadioIcon: React.FC<{
26
32
  </svg>
27
33
  );
28
34
 
29
- export type RecursicaRadioProps = Omit<
30
- MantineRadioProps,
31
- "size" | "color" | "radius" | "iconColor" | "variant"
32
- > &
33
- ReadOnlyControlProps;
35
+ export type RecursicaRadioProps = RequireAccessibleLabel<
36
+ Omit<
37
+ MantineRadioProps,
38
+ "size" | "color" | "radius" | "iconColor" | "variant"
39
+ > &
40
+ ReadOnlyControlProps &
41
+ Pick<
42
+ FormControlLayoutProps,
43
+ "formLayout" | "labelSize" | "controlMaxWidth" | "controlMinWidth"
44
+ >
45
+ >;
34
46
 
35
47
  export type RadioProps = RecursicaOverStyled<RecursicaRadioProps>;
36
48
 
@@ -40,76 +52,123 @@ type RadioComponent = React.ForwardRefExoticComponent<
40
52
  Group: typeof RadioGroup;
41
53
  };
42
54
 
43
- export const Radio = forwardRef<HTMLInputElement, RadioProps>(function Radio(
44
- { overStyled = false, readOnly, readOnlyComponent, disabled, ...rest },
45
- ref,
46
- ) {
47
- const sanitizedProps = filterStylingProps(rest, overStyled);
48
- const restRecord = sanitizedProps as Record<string, unknown>;
49
-
50
- // Actively delete dimension bindings that bypass the abstraction
51
- delete restRecord["size"];
52
- delete restRecord["color"];
53
- delete restRecord["radius"];
54
- delete restRecord["variant"];
55
- delete restRecord["iconColor"];
56
-
57
- if (readOnly && readOnlyComponent) {
58
- // Safely fallback explicitly returning mapped elements natively without macro wrappers
59
- return <>{readOnlyComponent}</>;
60
- }
61
-
62
- const mergedClassNames: Partial<Record<string, string>> = {
63
- root: styles.root,
64
- body: styles.body,
65
- inner: styles.inner,
66
- radio: styles.radio,
67
- icon: styles.icon,
68
- labelWrapper: styles.labelWrapper,
69
- label: styles.label,
70
- };
71
-
72
- const classNamesProp = restRecord.classNames;
73
- if (
74
- classNamesProp &&
75
- typeof classNamesProp === "object" &&
76
- !Array.isArray(classNamesProp)
77
- ) {
78
- const o = classNamesProp as Partial<Record<string, string>>;
79
- mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
80
- mergedClassNames.body = o.body ? `${styles.body} ${o.body}` : styles.body;
81
- mergedClassNames.inner = o.inner
82
- ? `${styles.inner} ${o.inner}`
83
- : styles.inner;
84
- mergedClassNames.radio = o.radio
85
- ? `${styles.radio} ${o.radio}`
86
- : styles.radio;
87
- mergedClassNames.icon = o.icon ? `${styles.icon} ${o.icon}` : styles.icon;
88
- mergedClassNames.labelWrapper = o.labelWrapper
89
- ? `${styles.labelWrapper} ${o.labelWrapper}`
90
- : styles.labelWrapper;
91
- mergedClassNames.label = o.label
92
- ? `${styles.label} ${o.label}`
93
- : styles.label;
94
- }
95
-
96
- const classNameProp = restRecord.className as string | undefined;
97
- const finalClass = classNameProp
98
- ? `${styles.root} ${classNameProp}`
99
- : styles.root;
100
-
101
- // We omit Mantine's sizing/coloring so we rely strictly on variables from Radio.module.css
102
- return (
103
- <MantineRadio
104
- ref={ref}
105
- icon={RadioIcon}
106
- className={finalClass}
107
- classNames={mergedClassNames}
108
- disabled={readOnly || disabled}
109
- {...(sanitizedProps as unknown as MantineRadioProps)}
110
- />
111
- );
112
- }) as RadioComponent;
55
+ export const Radio = forwardRef<HTMLInputElement, RadioProps>(
56
+ function Radio(props, ref) {
57
+ const {
58
+ overStyled = false,
59
+ readOnly,
60
+ readOnlyComponent,
61
+ disabled,
62
+ formLayout,
63
+ labelSize,
64
+ controlMaxWidth,
65
+ controlMinWidth,
66
+ ...rest
67
+ } = props;
68
+ const sanitizedProps = filterStylingProps(rest, overStyled);
69
+ const restRecord = sanitizedProps as Record<string, unknown>;
70
+
71
+ // Actively delete dimension bindings that bypass the abstraction
72
+ delete restRecord["size"];
73
+ delete restRecord["color"];
74
+ delete restRecord["radius"];
75
+ delete restRecord["variant"];
76
+ delete restRecord["iconColor"];
77
+
78
+ const mergedClassNames: Partial<Record<string, string>> = {
79
+ root: styles.root,
80
+ body: styles.body,
81
+ inner: styles.inner,
82
+ radio: styles.radio,
83
+ icon: styles.icon,
84
+ labelWrapper: styles.labelWrapper,
85
+ label: styles.label,
86
+ };
87
+
88
+ const classNamesProp = restRecord.classNames;
89
+ if (
90
+ classNamesProp &&
91
+ typeof classNamesProp === "object" &&
92
+ !Array.isArray(classNamesProp)
93
+ ) {
94
+ const o = classNamesProp as Partial<Record<string, string>>;
95
+ mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
96
+ mergedClassNames.body = o.body ? `${styles.body} ${o.body}` : styles.body;
97
+ mergedClassNames.inner = o.inner
98
+ ? `${styles.inner} ${o.inner}`
99
+ : styles.inner;
100
+ mergedClassNames.radio = o.radio
101
+ ? `${styles.radio} ${o.radio}`
102
+ : styles.radio;
103
+ mergedClassNames.icon = o.icon ? `${styles.icon} ${o.icon}` : styles.icon;
104
+ mergedClassNames.labelWrapper = o.labelWrapper
105
+ ? `${styles.labelWrapper} ${o.labelWrapper}`
106
+ : styles.labelWrapper;
107
+ mergedClassNames.label = o.label
108
+ ? `${styles.label} ${o.label}`
109
+ : styles.label;
110
+ }
111
+
112
+ const classNameProp = restRecord.className as string | undefined;
113
+ const finalClass = classNameProp
114
+ ? `${styles.root} ${classNameProp}`
115
+ : styles.root;
116
+
117
+ if (readOnly && !!readOnlyComponent) {
118
+ const isChecked = !!(restRecord.checked ?? restRecord.defaultChecked);
119
+ const ReadOnlyComp = readOnlyComponent;
120
+ const roNode = (
121
+ <ReadOnlyComp
122
+ {...props}
123
+ checked={isChecked}
124
+ label={restRecord.label as React.ReactNode}
125
+ />
126
+ );
127
+
128
+ if (formLayout) {
129
+ return (
130
+ <FormControlLayout
131
+ formLayout={formLayout}
132
+ labelSize={labelSize}
133
+ controlMaxWidth={controlMaxWidth}
134
+ controlMinWidth={controlMinWidth}
135
+ >
136
+ {roNode}
137
+ </FormControlLayout>
138
+ );
139
+ }
140
+
141
+ return <>{roNode}</>;
142
+ }
143
+
144
+ // We omit Mantine's sizing/coloring so we rely strictly on variables from Radio.module.css
145
+ const radioNode = (
146
+ <MantineRadio
147
+ ref={ref}
148
+ icon={RadioIcon}
149
+ className={finalClass}
150
+ classNames={mergedClassNames}
151
+ disabled={readOnly || disabled}
152
+ {...(sanitizedProps as unknown as MantineRadioProps)}
153
+ />
154
+ );
155
+
156
+ if (formLayout) {
157
+ return (
158
+ <FormControlLayout
159
+ formLayout={formLayout}
160
+ labelSize={labelSize}
161
+ controlMaxWidth={controlMaxWidth}
162
+ controlMinWidth={controlMinWidth}
163
+ >
164
+ {radioNode}
165
+ </FormControlLayout>
166
+ );
167
+ }
168
+
169
+ return radioNode;
170
+ },
171
+ ) as RadioComponent;
113
172
 
114
173
  Radio.displayName = "Radio";
115
174
  Radio.Group = RadioGroup;