@choice-ui/radio 0.0.4 → 0.0.6

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
@@ -1,6 +1,13 @@
1
1
  import * as react from 'react';
2
2
  import { HTMLProps, ReactNode } from 'react';
3
3
 
4
+ interface RadioIconProps extends Omit<HTMLProps<HTMLDivElement>, "children"> {
5
+ children?: ReactNode | ((props: {
6
+ value?: boolean;
7
+ }) => ReactNode);
8
+ }
9
+ declare const RadioIcon: react.MemoExoticComponent<react.ForwardRefExoticComponent<Omit<RadioIconProps, "ref"> & react.RefAttributes<HTMLDivElement>>>;
10
+
4
11
  interface RadioLabelProps extends Omit<HTMLProps<HTMLLabelElement>, "htmlFor" | "id" | "disabled"> {
5
12
  children: ReactNode;
6
13
  }
@@ -19,6 +26,7 @@ interface RadioType {
19
26
  (props: RadioProps & {
20
27
  ref?: React.Ref<HTMLInputElement>;
21
28
  }): JSX.Element;
29
+ Icon: typeof RadioIcon;
22
30
  Label: typeof RadioLabel;
23
31
  displayName?: string;
24
32
  }
@@ -50,20 +58,4 @@ interface RadioGroupType {
50
58
  }
51
59
  declare const RadioGroup: RadioGroupType;
52
60
 
53
- interface RadioContextType {
54
- descriptionId: string;
55
- disabled?: boolean;
56
- id: string;
57
- }
58
- declare function useRadioContext(): RadioContextType;
59
- interface RadioGroupContextType {
60
- disabled?: boolean;
61
- name: string;
62
- onChange: (value: string) => void;
63
- readOnly?: boolean;
64
- value: string;
65
- variant?: "default" | "accent" | "outline";
66
- }
67
- declare function useRadioGroupContext(): RadioGroupContextType;
68
-
69
- export { Radio, type RadioContextType, RadioGroup, type RadioGroupContextType, type RadioGroupProps, RadioLabel, type RadioLabelProps, type RadioProps, useRadioContext, useRadioGroupContext };
61
+ export { Radio, RadioGroup, type RadioGroupProps, type RadioIconProps, type RadioLabelProps, type RadioProps };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { tcv, tcx } from '@choice-ui/shared';
2
2
  import { Dot } from '@choiceform/icons-react';
3
- import { createContext, memo, forwardRef, useId, useMemo, useContext } from 'react';
3
+ import { createContext, memo, forwardRef, useId, Children, isValidElement, useMemo, useContext } from 'react';
4
4
  import { useEventCallback } from 'usehooks-ts';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
6
6
 
@@ -147,18 +147,50 @@ var radioTv = tcv({
147
147
  focused: false
148
148
  }
149
149
  });
150
+ var RadioIcon = memo(
151
+ forwardRef(function RadioIcon2(props, ref) {
152
+ const { className, children, ...rest } = props;
153
+ const { value, disabled, variant } = useRadioContext();
154
+ const tv = radioTv({
155
+ type: "radio",
156
+ variant,
157
+ disabled,
158
+ checked: value
159
+ });
160
+ const renderIcon = () => {
161
+ if (typeof children === "function") {
162
+ return children({ value });
163
+ }
164
+ if (children !== void 0) {
165
+ return children;
166
+ }
167
+ return value ? /* @__PURE__ */ jsx(Dot, {}) : null;
168
+ };
169
+ return /* @__PURE__ */ jsx(
170
+ "div",
171
+ {
172
+ ref,
173
+ className: tcx(tv.box(), className),
174
+ "data-active": value,
175
+ ...rest,
176
+ children: renderIcon()
177
+ }
178
+ );
179
+ })
180
+ );
181
+ RadioIcon.displayName = "Radio.Icon";
150
182
  var RadioLabel = memo(
151
183
  forwardRef(function RadioLabel2(props, ref) {
152
184
  const { children, className, ...rest } = props;
153
185
  const { id, descriptionId, disabled } = useRadioContext();
154
- const styles = radioTv({ disabled });
186
+ const tv = radioTv({ disabled });
155
187
  return /* @__PURE__ */ jsx(
156
188
  "label",
157
189
  {
158
190
  ref,
159
191
  id: descriptionId,
160
192
  htmlFor: id,
161
- className: tcx(styles.label(), className),
193
+ className: tcx(tv.label(), className),
162
194
  ...rest,
163
195
  children
164
196
  }
@@ -184,7 +216,7 @@ var RadioBase = forwardRef(function Radio(props, ref) {
184
216
  } = props;
185
217
  const id = useId();
186
218
  const descriptionId = useId();
187
- const styles = radioTv({
219
+ const tv = radioTv({
188
220
  type: "radio",
189
221
  variant,
190
222
  disabled,
@@ -203,19 +235,27 @@ var RadioBase = forwardRef(function Radio(props, ref) {
203
235
  }
204
236
  onKeyDown?.(e);
205
237
  });
238
+ const isIconElement = (child) => isValidElement(child) && (child.type === RadioIcon || child.type?.displayName === "Radio.Icon");
239
+ const childArray = Children.toArray(children);
240
+ const iconChild = childArray.find(isIconElement);
241
+ const otherChildren = childArray.filter((child) => !isIconElement(child));
206
242
  const renderChildren = () => {
207
- if (typeof children === "string" || typeof children === "number") {
208
- return /* @__PURE__ */ jsx(RadioLabel, { children });
243
+ if (otherChildren.length === 1) {
244
+ const child = otherChildren[0];
245
+ if (typeof child === "string" || typeof child === "number") {
246
+ return /* @__PURE__ */ jsx(RadioLabel, { children: child });
247
+ }
209
248
  }
210
- return children;
249
+ return otherChildren;
211
250
  };
212
- return /* @__PURE__ */ jsx(RadioContext.Provider, { value: { id, descriptionId, disabled }, children: /* @__PURE__ */ jsxs("div", { className: tcx(styles.root(), className), children: [
251
+ const renderDefaultIcon = () => /* @__PURE__ */ jsx("div", { className: tv.box(), children: value && /* @__PURE__ */ jsx(Dot, {}) });
252
+ return /* @__PURE__ */ jsx(RadioContext.Provider, { value: { id, descriptionId, disabled, value, variant }, children: /* @__PURE__ */ jsxs("div", { className: tcx(tv.root(), className), children: [
213
253
  /* @__PURE__ */ jsxs("div", { className: "pointer-events-none relative", children: [
214
254
  /* @__PURE__ */ jsx(
215
255
  "input",
216
256
  {
217
257
  ref,
218
- className: styles.input(),
258
+ className: tv.input(),
219
259
  type: "radio",
220
260
  id,
221
261
  name,
@@ -231,13 +271,14 @@ var RadioBase = forwardRef(function Radio(props, ref) {
231
271
  ...rest
232
272
  }
233
273
  ),
234
- /* @__PURE__ */ jsx("div", { className: styles.box(), children: value && /* @__PURE__ */ jsx(Dot, {}) })
274
+ iconChild ?? renderDefaultIcon()
235
275
  ] }),
236
276
  renderChildren()
237
277
  ] }) });
238
278
  });
239
279
  var MemoizedRadio = memo(RadioBase);
240
280
  var Radio2 = MemoizedRadio;
281
+ Radio2.Icon = RadioIcon;
241
282
  Radio2.Label = RadioLabel;
242
283
  Radio2.displayName = "Radio";
243
284
  var RadioGroupItem = memo(
@@ -332,4 +373,4 @@ var RadioGroup2 = MemoizedRadioGroup;
332
373
  RadioGroup2.Item = RadioGroupItem;
333
374
  RadioGroup2.displayName = "RadioGroup";
334
375
 
335
- export { Radio2 as Radio, RadioGroup2 as RadioGroup, RadioLabel, useRadioContext, useRadioGroupContext };
376
+ export { Radio2 as Radio, RadioGroup2 as RadioGroup };
package/package.json CHANGED
@@ -1,21 +1,18 @@
1
1
  {
2
2
  "name": "@choice-ui/radio",
3
- "version": "0.0.4",
4
- "description": "Radio component for Choiceform Design System",
3
+ "version": "0.0.6",
4
+ "description": "A radio button component for single selection from a group of mutually exclusive options",
5
5
  "sideEffects": false,
6
6
  "type": "module",
7
- "main": "./dist/index.cjs",
8
- "module": "./dist/index.js",
9
- "types": "./src/index.ts",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
10
9
  "files": [
11
- "dist",
12
- "src"
10
+ "dist"
13
11
  ],
14
12
  "exports": {
15
13
  ".": {
16
- "types": "./src/index.ts",
17
- "import": "./dist/index.js",
18
- "require": "./dist/index.cjs"
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
19
16
  }
20
17
  },
21
18
  "publishConfig": {
@@ -23,8 +20,8 @@
23
20
  },
24
21
  "dependencies": {
25
22
  "usehooks-ts": "^3.1.0",
26
- "@choiceform/icons-react": "^1.3.8",
27
- "@choice-ui/shared": "0.0.1"
23
+ "@choice-ui/shared": "^0.0.1",
24
+ "@choiceform/icons-react": "^1.3.8"
28
25
  },
29
26
  "devDependencies": {
30
27
  "@types/react": "^18.3.12",
@@ -39,6 +36,7 @@
39
36
  "react": ">=18.0.0",
40
37
  "react-dom": ">=18.0.0"
41
38
  },
39
+ "source": "./src/index.ts",
42
40
  "scripts": {
43
41
  "build": "tsup",
44
42
  "build:watch": "tsup --watch",
package/dist/index.cjs DELETED
@@ -1,341 +0,0 @@
1
- 'use strict';
2
-
3
- var shared = require('@choice-ui/shared');
4
- var iconsReact = require('@choiceform/icons-react');
5
- var react = require('react');
6
- var usehooksTs = require('usehooks-ts');
7
- var jsxRuntime = require('react/jsx-runtime');
8
-
9
- // src/radio.tsx
10
- var RadioContext = react.createContext(null);
11
- function useRadioContext() {
12
- const context = react.useContext(RadioContext);
13
- if (!context) {
14
- throw new Error("Radio.Label must be used within a Radio component");
15
- }
16
- return context;
17
- }
18
- var RadioGroupContext = react.createContext(null);
19
- function useRadioGroupContext() {
20
- const context = react.useContext(RadioGroupContext);
21
- if (!context) {
22
- throw new Error("RadioGroupItem must be used within a RadioGroup");
23
- }
24
- return context;
25
- }
26
- var radioTv = shared.tcv({
27
- slots: {
28
- root: "flex items-center select-none",
29
- box: ["relative flex size-4 items-center justify-center", "border border-solid"],
30
- input: "peer pointer-events-auto absolute inset-0 appearance-none opacity-0",
31
- label: "pl-2"
32
- },
33
- variants: {
34
- type: {
35
- checkbox: {
36
- box: "rounded-md"
37
- },
38
- radio: {
39
- box: "rounded-full"
40
- }
41
- },
42
- variant: {
43
- default: {},
44
- accent: {},
45
- outline: {}
46
- },
47
- checked: {
48
- true: {},
49
- false: {}
50
- },
51
- disabled: {
52
- true: {},
53
- false: {}
54
- },
55
- focused: {
56
- true: {},
57
- false: {}
58
- }
59
- },
60
- compoundVariants: [
61
- // 未选中状态
62
- {
63
- variant: ["default", "accent"],
64
- checked: false,
65
- disabled: false,
66
- focused: false,
67
- class: {
68
- box: "bg-secondary-background border-default-boundary"
69
- }
70
- },
71
- {
72
- variant: "outline",
73
- checked: false,
74
- disabled: false,
75
- focused: false,
76
- class: {
77
- box: ["border-default-foreground", "peer-focus-visible:border-selected-boundary"]
78
- }
79
- },
80
- // 选中状态 - default
81
- {
82
- variant: "default",
83
- checked: true,
84
- disabled: false,
85
- focused: false,
86
- class: {
87
- box: [
88
- "bg-secondary-background border-default-boundary",
89
- "peer-focus-visible:border-selected-strong-boundary"
90
- ]
91
- }
92
- },
93
- // 选中状态 - accent & outline
94
- {
95
- variant: ["accent", "outline"],
96
- checked: true,
97
- disabled: false,
98
- focused: false,
99
- class: {
100
- box: [
101
- "bg-accent-background border-selected-strong-boundary text-on-accent-foreground",
102
- "peer-focus-visible:border-selected-strong-boundary",
103
- "peer-focus-visible:text-on-accent-foreground",
104
- "peer-focus-visible:shadow-checked-focused"
105
- ]
106
- }
107
- },
108
- {
109
- variant: ["default", "accent", "outline"],
110
- checked: false,
111
- disabled: false,
112
- focused: true,
113
- class: {
114
- box: "border-selected-boundary"
115
- }
116
- },
117
- {
118
- variant: "default",
119
- checked: true,
120
- disabled: false,
121
- focused: true,
122
- class: {
123
- box: "border-selected-strong-boundary"
124
- }
125
- },
126
- {
127
- variant: ["accent", "outline"],
128
- checked: true,
129
- disabled: false,
130
- focused: true,
131
- class: {
132
- box: "text-on-accent-foreground border-selected-strong-boundary shadow-checked-focused"
133
- }
134
- },
135
- {
136
- variant: ["accent", "outline", "default"],
137
- disabled: true,
138
- class: {
139
- root: "text-default-background",
140
- box: "border-disabled-background bg-disabled-background",
141
- label: "text-disabled-foreground"
142
- }
143
- }
144
- ],
145
- defaultVariants: {
146
- variant: "default",
147
- checked: false,
148
- disabled: false,
149
- focused: false
150
- }
151
- });
152
- var RadioLabel = react.memo(
153
- react.forwardRef(function RadioLabel2(props, ref) {
154
- const { children, className, ...rest } = props;
155
- const { id, descriptionId, disabled } = useRadioContext();
156
- const styles = radioTv({ disabled });
157
- return /* @__PURE__ */ jsxRuntime.jsx(
158
- "label",
159
- {
160
- ref,
161
- id: descriptionId,
162
- htmlFor: id,
163
- className: shared.tcx(styles.label(), className),
164
- ...rest,
165
- children
166
- }
167
- );
168
- })
169
- );
170
- RadioLabel.displayName = "Radio.Label";
171
- var RadioBase = react.forwardRef(function Radio(props, ref) {
172
- const {
173
- value,
174
- onChange,
175
- disabled,
176
- readOnly = false,
177
- name,
178
- variant = "default",
179
- className,
180
- focused,
181
- children,
182
- "aria-label": ariaLabel,
183
- "aria-describedby": ariaDescribedby,
184
- onKeyDown,
185
- ...rest
186
- } = props;
187
- const id = react.useId();
188
- const descriptionId = react.useId();
189
- const styles = radioTv({
190
- type: "radio",
191
- variant,
192
- disabled,
193
- checked: value,
194
- focused
195
- });
196
- const handleChange = usehooksTs.useEventCallback((e) => {
197
- if (readOnly) return;
198
- onChange(e.target.checked);
199
- });
200
- const handleKeyDown = usehooksTs.useEventCallback((e) => {
201
- if (readOnly) return;
202
- if (e.key === " " || e.key === "Enter") {
203
- e.preventDefault();
204
- onChange(!value);
205
- }
206
- onKeyDown?.(e);
207
- });
208
- const renderChildren = () => {
209
- if (typeof children === "string" || typeof children === "number") {
210
- return /* @__PURE__ */ jsxRuntime.jsx(RadioLabel, { children });
211
- }
212
- return children;
213
- };
214
- return /* @__PURE__ */ jsxRuntime.jsx(RadioContext.Provider, { value: { id, descriptionId, disabled }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: shared.tcx(styles.root(), className), children: [
215
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pointer-events-none relative", children: [
216
- /* @__PURE__ */ jsxRuntime.jsx(
217
- "input",
218
- {
219
- ref,
220
- className: styles.input(),
221
- type: "radio",
222
- id,
223
- name,
224
- checked: value,
225
- disabled: disabled || readOnly,
226
- onChange: handleChange,
227
- "aria-label": ariaLabel,
228
- "aria-describedby": ariaDescribedby || descriptionId,
229
- "aria-checked": value,
230
- "aria-disabled": disabled || readOnly,
231
- role: "radio",
232
- onKeyDown: handleKeyDown,
233
- ...rest
234
- }
235
- ),
236
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.box(), children: value && /* @__PURE__ */ jsxRuntime.jsx(iconsReact.Dot, {}) })
237
- ] }),
238
- renderChildren()
239
- ] }) });
240
- });
241
- var MemoizedRadio = react.memo(RadioBase);
242
- var Radio2 = MemoizedRadio;
243
- Radio2.Label = RadioLabel;
244
- Radio2.displayName = "Radio";
245
- var RadioGroupItem = react.memo(
246
- react.forwardRef(function RadioGroupItem2(props, ref) {
247
- const { value, children, className, disabled, ...rest } = props;
248
- const {
249
- name,
250
- value: selectedValue,
251
- onChange,
252
- variant,
253
- readOnly: contextReadonly
254
- } = useRadioGroupContext();
255
- const isChecked = selectedValue === value;
256
- const handleChange = usehooksTs.useEventCallback(() => {
257
- if (contextReadonly) return;
258
- onChange(value);
259
- });
260
- return /* @__PURE__ */ jsxRuntime.jsx(
261
- Radio2,
262
- {
263
- name,
264
- value: isChecked,
265
- disabled,
266
- readOnly: contextReadonly,
267
- variant,
268
- onChange: handleChange,
269
- className,
270
- ...rest,
271
- ref,
272
- children: /* @__PURE__ */ jsxRuntime.jsx(Radio2.Label, { children })
273
- }
274
- );
275
- })
276
- );
277
- RadioGroupItem.displayName = "RadioGroup.Item";
278
- var RadioGroupBase = react.forwardRef(function RadioGroup(props, ref) {
279
- const {
280
- className,
281
- options,
282
- value,
283
- onChange,
284
- disabled,
285
- readOnly = false,
286
- variant = "default",
287
- children,
288
- ...rest
289
- } = props;
290
- const id = react.useId();
291
- const handleChange = usehooksTs.useEventCallback((newValue) => {
292
- if (readOnly) return;
293
- onChange(newValue);
294
- });
295
- const contextValue = react.useMemo(
296
- () => ({
297
- name: id,
298
- value,
299
- onChange: handleChange,
300
- disabled,
301
- readOnly,
302
- variant
303
- }),
304
- [id, value, handleChange, disabled, readOnly, variant]
305
- );
306
- const renderOptionsRadios = react.useMemo(() => {
307
- if (!options) return null;
308
- return options.map((option) => /* @__PURE__ */ jsxRuntime.jsx(
309
- RadioGroupItem,
310
- {
311
- value: option.value,
312
- disabled: option.disabled || disabled,
313
- variant,
314
- children: option.label
315
- },
316
- option.value
317
- ));
318
- }, [disabled, options, variant]);
319
- return /* @__PURE__ */ jsxRuntime.jsx(RadioGroupContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxRuntime.jsx(
320
- "div",
321
- {
322
- className: shared.tcx("flex flex-col gap-2", className),
323
- ref,
324
- role: "radiogroup",
325
- "aria-labelledby": props["aria-labelledby"],
326
- "aria-label": props["aria-label"],
327
- ...rest,
328
- children: options ? renderOptionsRadios : children
329
- }
330
- ) });
331
- });
332
- var MemoizedRadioGroup = react.memo(RadioGroupBase);
333
- var RadioGroup2 = MemoizedRadioGroup;
334
- RadioGroup2.Item = RadioGroupItem;
335
- RadioGroup2.displayName = "RadioGroup";
336
-
337
- exports.Radio = Radio2;
338
- exports.RadioGroup = RadioGroup2;
339
- exports.RadioLabel = RadioLabel;
340
- exports.useRadioContext = useRadioContext;
341
- exports.useRadioGroupContext = useRadioGroupContext;
package/dist/index.d.cts DELETED
@@ -1,69 +0,0 @@
1
- import * as react from 'react';
2
- import { HTMLProps, ReactNode } from 'react';
3
-
4
- interface RadioLabelProps extends Omit<HTMLProps<HTMLLabelElement>, "htmlFor" | "id" | "disabled"> {
5
- children: ReactNode;
6
- }
7
- declare const RadioLabel: react.MemoExoticComponent<react.ForwardRefExoticComponent<Omit<RadioLabelProps, "ref"> & react.RefAttributes<HTMLLabelElement>>>;
8
-
9
- interface RadioProps extends Omit<HTMLProps<HTMLInputElement>, "value" | "onChange"> {
10
- children?: ReactNode;
11
- className?: string;
12
- focused?: boolean;
13
- onChange: (value: boolean) => void;
14
- readOnly?: boolean;
15
- value: boolean;
16
- variant?: "default" | "accent" | "outline";
17
- }
18
- interface RadioType {
19
- (props: RadioProps & {
20
- ref?: React.Ref<HTMLInputElement>;
21
- }): JSX.Element;
22
- Label: typeof RadioLabel;
23
- displayName?: string;
24
- }
25
- declare const Radio: RadioType;
26
-
27
- interface RadioGroupProps extends Omit<HTMLProps<HTMLDivElement>, "value" | "onChange"> {
28
- children?: ReactNode;
29
- onChange: (value: string) => void;
30
- options?: {
31
- disabled?: boolean;
32
- label: string;
33
- value: string;
34
- }[];
35
- readOnly?: boolean;
36
- value: string;
37
- variant?: "default" | "accent" | "outline";
38
- }
39
- type RadioGroupItemProps = Omit<RadioProps, "value" | "onChange"> & {
40
- children: ReactNode;
41
- value: string;
42
- };
43
- declare const RadioGroupItem: react.MemoExoticComponent<react.ForwardRefExoticComponent<Omit<RadioGroupItemProps, "ref"> & react.RefAttributes<HTMLInputElement>>>;
44
- interface RadioGroupType {
45
- (props: RadioGroupProps & {
46
- ref?: React.Ref<HTMLDivElement>;
47
- }): JSX.Element;
48
- Item: typeof RadioGroupItem;
49
- displayName?: string;
50
- }
51
- declare const RadioGroup: RadioGroupType;
52
-
53
- interface RadioContextType {
54
- descriptionId: string;
55
- disabled?: boolean;
56
- id: string;
57
- }
58
- declare function useRadioContext(): RadioContextType;
59
- interface RadioGroupContextType {
60
- disabled?: boolean;
61
- name: string;
62
- onChange: (value: string) => void;
63
- readOnly?: boolean;
64
- value: string;
65
- variant?: "default" | "accent" | "outline";
66
- }
67
- declare function useRadioGroupContext(): RadioGroupContextType;
68
-
69
- export { Radio, type RadioContextType, RadioGroup, type RadioGroupContextType, type RadioGroupProps, RadioLabel, type RadioLabelProps, type RadioProps, useRadioContext, useRadioGroupContext };
package/src/context.ts DELETED
@@ -1,38 +0,0 @@
1
- import { createContext, useContext } from "react"
2
-
3
- // Radio 组件的 Context
4
- export interface RadioContextType {
5
- descriptionId: string
6
- disabled?: boolean
7
- id: string
8
- }
9
-
10
- export const RadioContext = createContext<RadioContextType | null>(null)
11
-
12
- export function useRadioContext() {
13
- const context = useContext(RadioContext)
14
- if (!context) {
15
- throw new Error("Radio.Label must be used within a Radio component")
16
- }
17
- return context
18
- }
19
-
20
- // RadioGroup 组件的 Context
21
- export interface RadioGroupContextType {
22
- disabled?: boolean
23
- name: string
24
- onChange: (value: string) => void
25
- readOnly?: boolean
26
- value: string
27
- variant?: "default" | "accent" | "outline"
28
- }
29
-
30
- export const RadioGroupContext = createContext<RadioGroupContextType | null>(null)
31
-
32
- export function useRadioGroupContext() {
33
- const context = useContext(RadioGroupContext)
34
- if (!context) {
35
- throw new Error("RadioGroupItem must be used within a RadioGroup")
36
- }
37
- return context
38
- }
package/src/index.ts DELETED
@@ -1,8 +0,0 @@
1
- export { Radio } from "./radio"
2
- export { RadioGroup } from "./radio-group"
3
- export { RadioLabel } from "./radio-label"
4
- export { useRadioContext, useRadioGroupContext } from "./context"
5
- export type { RadioProps } from "./radio"
6
- export type { RadioGroupProps } from "./radio-group"
7
- export type { RadioLabelProps } from "./radio-label"
8
- export type { RadioContextType, RadioGroupContextType } from "./context"
@@ -1,137 +0,0 @@
1
- import { tcx } from "@choice-ui/shared"
2
- import { forwardRef, HTMLProps, memo, ReactNode, useId, useMemo } from "react"
3
- import { useEventCallback } from "usehooks-ts"
4
- import { RadioGroupContext, useRadioGroupContext } from "./context"
5
- import { Radio, RadioProps } from "./radio"
6
-
7
- export interface RadioGroupProps extends Omit<HTMLProps<HTMLDivElement>, "value" | "onChange"> {
8
- children?: ReactNode
9
- onChange: (value: string) => void
10
- options?: {
11
- disabled?: boolean
12
- label: string
13
- value: string
14
- }[]
15
- readOnly?: boolean
16
- value: string
17
- variant?: "default" | "accent" | "outline"
18
- }
19
-
20
- type RadioGroupItemProps = Omit<RadioProps, "value" | "onChange"> & {
21
- children: ReactNode
22
- value: string
23
- }
24
-
25
- const RadioGroupItem = memo(
26
- forwardRef<HTMLInputElement, RadioGroupItemProps>(function RadioGroupItem(props, ref) {
27
- const { value, children, className, disabled, ...rest } = props
28
- const {
29
- name,
30
- value: selectedValue,
31
- onChange,
32
- variant,
33
- readOnly: contextReadonly,
34
- } = useRadioGroupContext()
35
- const isChecked = selectedValue === value
36
-
37
- const handleChange = useEventCallback(() => {
38
- if (contextReadonly) return
39
- onChange(value)
40
- })
41
-
42
- return (
43
- <Radio
44
- name={name}
45
- value={isChecked}
46
- disabled={disabled}
47
- readOnly={contextReadonly}
48
- variant={variant}
49
- onChange={handleChange}
50
- className={className}
51
- {...rest}
52
- ref={ref}
53
- >
54
- <Radio.Label>{children}</Radio.Label>
55
- </Radio>
56
- )
57
- }),
58
- )
59
-
60
- RadioGroupItem.displayName = "RadioGroup.Item"
61
-
62
- const RadioGroupBase = forwardRef<HTMLDivElement, RadioGroupProps>(function RadioGroup(props, ref) {
63
- const {
64
- className,
65
- options,
66
- value,
67
- onChange,
68
- disabled,
69
- readOnly = false,
70
- variant = "default",
71
- children,
72
- ...rest
73
- } = props
74
- const id = useId()
75
-
76
- const handleChange = useEventCallback((newValue: string) => {
77
- if (readOnly) return
78
- onChange(newValue)
79
- })
80
-
81
- const contextValue = useMemo(
82
- () => ({
83
- name: id,
84
- value,
85
- onChange: handleChange,
86
- disabled,
87
- readOnly,
88
- variant,
89
- }),
90
- [id, value, handleChange, disabled, readOnly, variant],
91
- )
92
-
93
- // 渲染基于选项的单选按钮
94
- const renderOptionsRadios = useMemo(() => {
95
- if (!options) return null
96
-
97
- return options.map((option) => (
98
- <RadioGroupItem
99
- key={option.value}
100
- value={option.value}
101
- disabled={option.disabled || disabled}
102
- variant={variant}
103
- >
104
- {option.label}
105
- </RadioGroupItem>
106
- ))
107
- }, [disabled, options, variant])
108
-
109
- return (
110
- <RadioGroupContext.Provider value={contextValue}>
111
- <div
112
- className={tcx("flex flex-col gap-2", className)}
113
- ref={ref}
114
- role="radiogroup"
115
- aria-labelledby={props["aria-labelledby"]}
116
- aria-label={props["aria-label"]}
117
- {...rest}
118
- >
119
- {options ? renderOptionsRadios : children}
120
- </div>
121
- </RadioGroupContext.Provider>
122
- )
123
- })
124
-
125
- // 使用 memo 包装组件以避免不必要的重渲染
126
- const MemoizedRadioGroup = memo(RadioGroupBase) as unknown as RadioGroupType
127
-
128
- interface RadioGroupType {
129
- (props: RadioGroupProps & { ref?: React.Ref<HTMLDivElement> }): JSX.Element
130
- Item: typeof RadioGroupItem
131
- displayName?: string
132
- }
133
-
134
- export const RadioGroup = MemoizedRadioGroup
135
-
136
- RadioGroup.Item = RadioGroupItem
137
- RadioGroup.displayName = "RadioGroup"
@@ -1,33 +0,0 @@
1
- import { tcx } from "@choice-ui/shared"
2
- import { forwardRef, HTMLProps, memo, ReactNode } from "react"
3
- import { useRadioContext } from "./context"
4
- import { radioTv } from "./tv"
5
-
6
- export interface RadioLabelProps extends Omit<
7
- HTMLProps<HTMLLabelElement>,
8
- "htmlFor" | "id" | "disabled"
9
- > {
10
- children: ReactNode
11
- }
12
-
13
- export const RadioLabel = memo(
14
- forwardRef<HTMLLabelElement, RadioLabelProps>(function RadioLabel(props, ref) {
15
- const { children, className, ...rest } = props
16
- const { id, descriptionId, disabled } = useRadioContext()
17
- const styles = radioTv({ disabled })
18
-
19
- return (
20
- <label
21
- ref={ref}
22
- id={descriptionId}
23
- htmlFor={id}
24
- className={tcx(styles.label(), className)}
25
- {...rest}
26
- >
27
- {children}
28
- </label>
29
- )
30
- }),
31
- )
32
-
33
- RadioLabel.displayName = "Radio.Label"
package/src/radio.tsx DELETED
@@ -1,109 +0,0 @@
1
- import { tcx } from "@choice-ui/shared"
2
- import { Dot } from "@choiceform/icons-react"
3
- import { forwardRef, HTMLProps, memo, ReactNode, useId } from "react"
4
- import { useEventCallback } from "usehooks-ts"
5
- import { RadioContext } from "./context"
6
- import { RadioLabel } from "./radio-label"
7
- import { radioTv } from "./tv"
8
-
9
- export interface RadioProps extends Omit<HTMLProps<HTMLInputElement>, "value" | "onChange"> {
10
- children?: ReactNode
11
- className?: string
12
- focused?: boolean
13
- onChange: (value: boolean) => void
14
- readOnly?: boolean
15
- value: boolean
16
- variant?: "default" | "accent" | "outline"
17
- }
18
-
19
- const RadioBase = forwardRef<HTMLInputElement, RadioProps>(function Radio(props, ref) {
20
- const {
21
- value,
22
- onChange,
23
- disabled,
24
- readOnly = false,
25
- name,
26
- variant = "default",
27
- className,
28
- focused,
29
- children,
30
- "aria-label": ariaLabel,
31
- "aria-describedby": ariaDescribedby,
32
- onKeyDown,
33
- ...rest
34
- } = props
35
- const id = useId()
36
- const descriptionId = useId()
37
-
38
- const styles = radioTv({
39
- type: "radio",
40
- variant,
41
- disabled,
42
- checked: value,
43
- focused,
44
- })
45
-
46
- const handleChange = useEventCallback((e: React.ChangeEvent<HTMLInputElement>) => {
47
- if (readOnly) return
48
- onChange(e.target.checked)
49
- })
50
-
51
- const handleKeyDown = useEventCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
52
- if (readOnly) return
53
- if (e.key === " " || e.key === "Enter") {
54
- e.preventDefault()
55
- onChange(!value)
56
- }
57
- onKeyDown?.(e)
58
- })
59
-
60
- // 自动将字符串类型的 children 包装成 RadioLabel
61
- const renderChildren = () => {
62
- if (typeof children === "string" || typeof children === "number") {
63
- return <RadioLabel>{children}</RadioLabel>
64
- }
65
- return children
66
- }
67
-
68
- return (
69
- <RadioContext.Provider value={{ id, descriptionId, disabled }}>
70
- <div className={tcx(styles.root(), className)}>
71
- <div className="pointer-events-none relative">
72
- <input
73
- ref={ref}
74
- className={styles.input()}
75
- type="radio"
76
- id={id}
77
- name={name}
78
- checked={value}
79
- disabled={disabled || readOnly}
80
- onChange={handleChange}
81
- aria-label={ariaLabel}
82
- aria-describedby={ariaDescribedby || descriptionId}
83
- aria-checked={value}
84
- aria-disabled={disabled || readOnly}
85
- role="radio"
86
- onKeyDown={handleKeyDown}
87
- {...rest}
88
- />
89
- <div className={styles.box()}>{value && <Dot />}</div>
90
- </div>
91
-
92
- {renderChildren()}
93
- </div>
94
- </RadioContext.Provider>
95
- )
96
- })
97
-
98
- const MemoizedRadio = memo(RadioBase) as unknown as RadioType
99
-
100
- interface RadioType {
101
- (props: RadioProps & { ref?: React.Ref<HTMLInputElement> }): JSX.Element
102
- Label: typeof RadioLabel
103
- displayName?: string
104
- }
105
-
106
- export const Radio = MemoizedRadio as RadioType
107
-
108
- Radio.Label = RadioLabel
109
- Radio.displayName = "Radio"
package/src/tv.ts DELETED
@@ -1,128 +0,0 @@
1
- import { tcv } from "@choice-ui/shared"
2
-
3
- export const radioTv = tcv({
4
- slots: {
5
- root: "flex items-center select-none",
6
- box: ["relative flex size-4 items-center justify-center", "border border-solid"],
7
- input: "peer pointer-events-auto absolute inset-0 appearance-none opacity-0",
8
- label: "pl-2",
9
- },
10
- variants: {
11
- type: {
12
- checkbox: {
13
- box: "rounded-md",
14
- },
15
- radio: {
16
- box: "rounded-full",
17
- },
18
- },
19
- variant: {
20
- default: {},
21
- accent: {},
22
- outline: {},
23
- },
24
- checked: {
25
- true: {},
26
- false: {},
27
- },
28
- disabled: {
29
- true: {},
30
- false: {},
31
- },
32
- focused: {
33
- true: {},
34
- false: {},
35
- },
36
- },
37
- compoundVariants: [
38
- // 未选中状态
39
- {
40
- variant: ["default", "accent"],
41
- checked: false,
42
- disabled: false,
43
- focused: false,
44
- class: {
45
- box: "bg-secondary-background border-default-boundary",
46
- },
47
- },
48
- {
49
- variant: "outline",
50
- checked: false,
51
- disabled: false,
52
- focused: false,
53
- class: {
54
- box: ["border-default-foreground", "peer-focus-visible:border-selected-boundary"],
55
- },
56
- },
57
- // 选中状态 - default
58
- {
59
- variant: "default",
60
- checked: true,
61
- disabled: false,
62
- focused: false,
63
- class: {
64
- box: [
65
- "bg-secondary-background border-default-boundary",
66
- "peer-focus-visible:border-selected-strong-boundary",
67
- ],
68
- },
69
- },
70
- // 选中状态 - accent & outline
71
- {
72
- variant: ["accent", "outline"],
73
- checked: true,
74
- disabled: false,
75
- focused: false,
76
- class: {
77
- box: [
78
- "bg-accent-background border-selected-strong-boundary text-on-accent-foreground",
79
- "peer-focus-visible:border-selected-strong-boundary",
80
- "peer-focus-visible:text-on-accent-foreground",
81
- "peer-focus-visible:shadow-checked-focused",
82
- ],
83
- },
84
- },
85
- {
86
- variant: ["default", "accent", "outline"],
87
- checked: false,
88
- disabled: false,
89
- focused: true,
90
- class: {
91
- box: "border-selected-boundary",
92
- },
93
- },
94
- {
95
- variant: "default",
96
- checked: true,
97
- disabled: false,
98
- focused: true,
99
- class: {
100
- box: "border-selected-strong-boundary",
101
- },
102
- },
103
- {
104
- variant: ["accent", "outline"],
105
- checked: true,
106
- disabled: false,
107
- focused: true,
108
- class: {
109
- box: "text-on-accent-foreground border-selected-strong-boundary shadow-checked-focused",
110
- },
111
- },
112
- {
113
- variant: ["accent", "outline", "default"],
114
- disabled: true,
115
- class: {
116
- root: "text-default-background",
117
- box: "border-disabled-background bg-disabled-background",
118
- label: "text-disabled-foreground",
119
- },
120
- },
121
- ],
122
- defaultVariants: {
123
- variant: "default",
124
- checked: false,
125
- disabled: false,
126
- focused: false,
127
- },
128
- })