@byyuurin/ui 0.0.9 → 0.0.10

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 (91) hide show
  1. package/README.md +0 -3
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +1 -1
  4. package/dist/runtime/app/injections.d.ts +9299 -3
  5. package/dist/runtime/app/injections.js +35 -0
  6. package/dist/runtime/components/Accordion.vue +16 -20
  7. package/dist/runtime/components/Alert.vue +1 -1
  8. package/dist/runtime/components/Badge.vue +1 -1
  9. package/dist/runtime/components/Breadcrumb.vue +17 -21
  10. package/dist/runtime/components/Calendar.vue +15 -6
  11. package/dist/runtime/components/Carousel.vue +5 -3
  12. package/dist/runtime/components/Checkbox.vue +12 -7
  13. package/dist/runtime/components/Drawer.vue +12 -12
  14. package/dist/runtime/components/DropdownMenu.vue +143 -0
  15. package/dist/runtime/components/DropdownMenuContent.vue +188 -0
  16. package/dist/runtime/components/Form.vue +311 -0
  17. package/dist/runtime/components/FormItem.vue +129 -0
  18. package/dist/runtime/components/Input.vue +27 -13
  19. package/dist/runtime/components/InputNumber.vue +22 -14
  20. package/dist/runtime/components/Link.vue +17 -2
  21. package/dist/runtime/components/Modal.vue +11 -11
  22. package/dist/runtime/components/PinInput.vue +22 -13
  23. package/dist/runtime/components/Popover.vue +3 -3
  24. package/dist/runtime/components/RadioGroup.vue +50 -46
  25. package/dist/runtime/components/Select.vue +90 -80
  26. package/dist/runtime/components/Slider.vue +12 -7
  27. package/dist/runtime/components/Switch.vue +12 -6
  28. package/dist/runtime/components/Table.vue +21 -8
  29. package/dist/runtime/components/Tabs.vue +12 -11
  30. package/dist/runtime/components/Textarea.vue +19 -13
  31. package/dist/runtime/components/Toast.vue +6 -3
  32. package/dist/runtime/components/Tooltip.vue +3 -3
  33. package/dist/runtime/composables/useFormItem.d.ts +27 -0
  34. package/dist/runtime/composables/useFormItem.js +64 -0
  35. package/dist/runtime/composables/useTheme.js +2 -1
  36. package/dist/runtime/index.d.ts +3 -0
  37. package/dist/runtime/index.js +3 -0
  38. package/dist/runtime/theme/app.d.ts +1 -0
  39. package/dist/runtime/theme/app.js +2 -1
  40. package/dist/runtime/theme/badge.d.ts +21 -45
  41. package/dist/runtime/theme/breadcrumb.d.ts +3 -3
  42. package/dist/runtime/theme/button.d.ts +111 -57
  43. package/dist/runtime/theme/calendar.d.ts +2 -2
  44. package/dist/runtime/theme/chip.d.ts +11 -44
  45. package/dist/runtime/theme/drawer.d.ts +68 -33
  46. package/dist/runtime/theme/dropdown-menu.d.ts +71 -0
  47. package/dist/runtime/theme/dropdown-menu.js +83 -0
  48. package/dist/runtime/theme/form-item.d.ts +76 -0
  49. package/dist/runtime/theme/form-item.js +34 -0
  50. package/dist/runtime/theme/form.d.ts +8 -0
  51. package/dist/runtime/theme/form.js +7 -0
  52. package/dist/runtime/theme/index.d.ts +3 -0
  53. package/dist/runtime/theme/index.js +3 -0
  54. package/dist/runtime/theme/input-number.d.ts +41 -61
  55. package/dist/runtime/theme/input.d.ts +99 -71
  56. package/dist/runtime/theme/input.js +2 -2
  57. package/dist/runtime/theme/modal.d.ts +5 -33
  58. package/dist/runtime/theme/pinInput.d.ts +42 -42
  59. package/dist/runtime/theme/pinInput.js +1 -1
  60. package/dist/runtime/theme/progress.d.ts +117 -53
  61. package/dist/runtime/theme/select.d.ts +100 -84
  62. package/dist/runtime/theme/select.js +2 -1
  63. package/dist/runtime/theme/separator.d.ts +13 -28
  64. package/dist/runtime/theme/table.d.ts +3 -0
  65. package/dist/runtime/theme/table.js +2 -1
  66. package/dist/runtime/theme/tabs.d.ts +51 -68
  67. package/dist/runtime/theme/textarea.d.ts +37 -43
  68. package/dist/runtime/theme/textarea.js +1 -1
  69. package/dist/runtime/theme/toast-provider.d.ts +26 -41
  70. package/dist/runtime/types/components.d.ts +3 -0
  71. package/dist/runtime/types/form.d.ts +45 -0
  72. package/dist/runtime/types/form.js +0 -0
  73. package/dist/runtime/types/index.d.ts +5 -2
  74. package/dist/runtime/types/index.js +1 -0
  75. package/dist/runtime/types/utils.d.ts +32 -11
  76. package/dist/runtime/utils/extend-theme.js +15 -4
  77. package/dist/runtime/utils/form.d.ts +5 -0
  78. package/dist/runtime/utils/form.js +24 -0
  79. package/dist/runtime/utils/index.d.ts +2 -0
  80. package/dist/runtime/utils/index.js +4 -0
  81. package/dist/runtime/utils/link.d.ts +4 -26
  82. package/dist/runtime/utils/link.js +10 -3
  83. package/dist/shared/ui.3e7fad19.mjs +5 -0
  84. package/dist/shared/ui.3e7fad19.mjs.map +1 -0
  85. package/dist/unocss.mjs +2 -2
  86. package/dist/unocss.mjs.map +1 -1
  87. package/dist/unplugin.mjs +1 -1
  88. package/dist/vite.mjs +1 -1
  89. package/package.json +16 -14
  90. package/dist/shared/ui.1a1f119c.mjs +0 -5
  91. package/dist/shared/ui.1a1f119c.mjs.map +0 -1
@@ -7,9 +7,9 @@ import type { input } from '../theme'
7
7
  import type { ComponentAttrs } from '../types'
8
8
 
9
9
  export interface InputEmits {
10
- (e: 'update:modelValue', payload: string | number): void
11
- (e: 'blur', event: FocusEvent): void
12
- (e: 'change', event: Event): void
10
+ 'update:modelValue': [payload: string | number]
11
+ 'blur': [event: FocusEvent]
12
+ 'change': [event: Event]
13
13
  }
14
14
 
15
15
  export interface InputSlots {
@@ -48,6 +48,7 @@ import { Primitive } from 'reka-ui'
48
48
  import { computed, onMounted, ref } from 'vue'
49
49
  import { useButtonGroup } from '../composables/useButtonGroup'
50
50
  import { useComponentIcons } from '../composables/useComponentIcons'
51
+ import { useFormItem } from '../composables/useFormItem'
51
52
  import { useTheme } from '../composables/useTheme'
52
53
  import { looseToNumber } from '../utils'
53
54
 
@@ -68,15 +69,27 @@ const [modelValue, modelModifiers] = defineModel<string | number>()
68
69
 
69
70
  const inputRef = ref<HTMLInputElement | null>(null)
70
71
 
71
- const { size, orientation } = useButtonGroup(props)
72
+ const {
73
+ size: formItemSize,
74
+ id,
75
+ name,
76
+ highlight,
77
+ disabled,
78
+ ariaAttrs,
79
+ emitFormBlur,
80
+ emitFormInput,
81
+ emitFormChange,
82
+ emitFormFocus,
83
+ } = useFormItem<InputProps>(props, { deferInputValidation: true })
84
+ const { size: buttonGroupSize, orientation } = useButtonGroup(props)
72
85
  const { isLeading, leadingIconName, isTrailing, trailingIconName } = useComponentIcons(props)
73
86
 
74
87
  const { generateStyle } = useTheme()
75
88
  const style = computed(() => generateStyle('input', {
76
89
  ...props,
77
- // @ts-expect-error ignore type
78
- type: props.type,
79
- size: size.value,
90
+ type: props.type as InputVariants['type'],
91
+ size: buttonGroupSize.value || formItemSize.value,
92
+ highlight: highlight.value,
80
93
  groupOrientation: orientation.value,
81
94
  leading: isLeading.value || !!slots.leading,
82
95
  trailing: isTrailing.value || !!slots.trailing,
@@ -95,6 +108,7 @@ function updateInput(value: string) {
95
108
  value = looseToNumber(value)
96
109
 
97
110
  modelValue.value = value
111
+ emitFormInput()
98
112
  }
99
113
 
100
114
  function onInput(event: Event) {
@@ -112,10 +126,12 @@ function onChange(event: Event) {
112
126
  (event.target as HTMLInputElement).value = value.trim()
113
127
 
114
128
  emit('change', event)
129
+ emitFormChange()
115
130
  }
116
131
 
117
132
  function onBlur(event: FocusEvent) {
118
133
  emit('blur', event)
134
+ emitFormBlur()
119
135
  }
120
136
 
121
137
  defineExpose({
@@ -133,7 +149,7 @@ onMounted(() => {
133
149
  <Primitive
134
150
  :as="as"
135
151
  :class="style.base({ class: [props.class, props.ui?.base] })"
136
- :aria-disabled="props.disabled ? true : undefined"
152
+ :aria-disabled="disabled ? true : undefined"
137
153
  >
138
154
  <span v-if="isLeading || slots.leading" :class="style.leading({ class: props.ui?.leading })">
139
155
  <slot name="leading">
@@ -145,20 +161,18 @@ onMounted(() => {
145
161
  </span>
146
162
 
147
163
  <input
148
- :id="id"
149
164
  ref="inputRef"
165
+ :class="style.input({ class: props.ui?.input })"
150
166
  :type="props.type"
151
167
  :value="modelValue"
152
- :name="props.name"
153
168
  :placeholder="props.placeholder"
154
- :class="style.input({ class: props.ui?.input })"
155
- :disabled="props.disabled"
156
169
  :required="props.required"
157
170
  :autocomplete="props.autocomplete"
158
- v-bind="$attrs"
171
+ v-bind="{ ...$attrs, ...ariaAttrs, id, name, disabled }"
159
172
  @input="onInput"
160
173
  @blur="onBlur"
161
174
  @change="onChange"
175
+ @focus="emitFormFocus"
162
176
  />
163
177
 
164
178
  <slot></slot>
@@ -5,9 +5,9 @@ import type { inputNumber } from '../theme'
5
5
  import type { ButtonProps, ComponentAttrs } from '../types'
6
6
 
7
7
  export interface InputNumberEmits {
8
- (e: 'update:modelValue', payload: number): void
9
- (e: 'blur', event: FocusEvent): void
10
- (e: 'change', payload: Event): void
8
+ 'update:modelValue': [payload: number]
9
+ 'blur': [event: FocusEvent]
10
+ 'change': [payload: Event]
11
11
  }
12
12
 
13
13
  export interface InputNumberSlots {
@@ -63,6 +63,7 @@ export interface InputNumberProps extends ComponentAttrs<typeof inputNumber>, Pi
63
63
  import { reactivePick } from '@vueuse/core'
64
64
  import { NumberFieldDecrement, NumberFieldIncrement, NumberFieldInput, NumberFieldRoot, useForwardPropsEmits } from 'reka-ui'
65
65
  import { computed, onMounted, ref } from 'vue'
66
+ import { useFormItem } from '../composables/useFormItem'
66
67
  import { useLocale } from '../composables/useLocale'
67
68
  import { useTheme } from '../composables/useTheme'
68
69
  import Button from './Button.vue'
@@ -82,12 +83,18 @@ const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', '
82
83
 
83
84
  const inputRef = ref<InstanceType<typeof NumberFieldInput> | null>(null)
84
85
 
85
- const { t } = useLocale()
86
+ const { t, code: codeLocale } = useLocale()
87
+ const locale = computed(() => props.locale || codeLocale.value)
88
+ const { id, name, size, highlight, disabled, ariaAttrs, emitFormBlur, emitFormFocus, emitFormInput, emitFormChange } = useFormItem<InputNumberProps>(props)
86
89
  const { theme, generateStyle } = useTheme()
87
90
  const incrementIcon = computed(() => props.incrementIcon || (props.orientation === 'horizontal' ? theme.value.app.icons.plus : theme.value.app.icons.chevronUp))
88
91
  const decrementIcon = computed(() => props.decrementIcon || (props.orientation === 'horizontal' ? theme.value.app.icons.minus : theme.value.app.icons.chevronDown))
89
92
 
90
- const style = computed(() => generateStyle('inputNumber', props))
93
+ const style = computed(() => generateStyle('inputNumber', {
94
+ ...props,
95
+ size: size.value,
96
+ highlight: highlight.value,
97
+ }))
91
98
 
92
99
  onMounted(() => {
93
100
  setTimeout(() => {
@@ -108,35 +115,36 @@ function onUpdate(value: number) {
108
115
  // @ts-expect-error - 'target' does not exist in type 'EventInit'
109
116
  const event = new Event('change', { target: { value } })
110
117
  emit('change', event)
118
+ emitFormChange()
119
+ emitFormInput()
111
120
  }
112
121
 
113
122
  function onBlur(event: FocusEvent) {
114
123
  emit('blur', event)
124
+ emitFormBlur()
115
125
  }
116
126
  </script>
117
127
 
118
128
  <template>
119
129
  <NumberFieldRoot
120
- v-bind="rootProps"
121
- :id="props.id"
122
- :name="props.name"
123
- :disabled="props.disabled"
124
- :locale="props.locale"
130
+ v-bind="{ ...rootProps, id, name, disabled }"
125
131
  :class="style.base({ class: [props.class, props.ui?.base] })"
126
- :aria-disabled="props.disabled ? true : undefined"
132
+ :locale="locale"
133
+ :aria-disabled="disabled ? true : undefined"
127
134
  @update:model-value="onUpdate"
128
135
  >
129
136
  <NumberFieldInput
130
- v-bind="$attrs"
137
+ v-bind="{ ...$attrs, ...ariaAttrs }"
131
138
  ref="inputRef"
132
139
  :placeholder="props.placeholder"
133
140
  :required="props.required"
134
141
  :class="style.input({ class: props.ui?.input })"
135
142
  @blur="onBlur"
143
+ @focus="emitFormFocus"
136
144
  />
137
145
 
138
146
  <div :class="style.increment({ class: props.ui?.increment })">
139
- <NumberFieldIncrement as-child :disabled="props.disabled">
147
+ <NumberFieldIncrement as-child :disabled="disabled">
140
148
  <slot name="increment">
141
149
  <Button
142
150
  :icon="incrementIcon"
@@ -150,7 +158,7 @@ function onBlur(event: FocusEvent) {
150
158
  </div>
151
159
 
152
160
  <div :class="style.decrement({ class: props.ui?.decrement })">
153
- <NumberFieldDecrement as-child :disabled="props.disabled">
161
+ <NumberFieldDecrement as-child :disabled="disabled">
154
162
  <slot name="decrement">
155
163
  <Button
156
164
  :icon="decrementIcon"
@@ -168,6 +168,9 @@ const isExternalLink = computed(() => {
168
168
  if (!to)
169
169
  return false
170
170
 
171
+ if (props.target === '_blank')
172
+ return true
173
+
171
174
  return typeof to === 'string' && hasProtocol(to, { acceptRelative: true })
172
175
  })
173
176
 
@@ -175,6 +178,9 @@ function isLinkActive({ route: linkRoute, isActive, isExactActive }: any) {
175
178
  if (props.active !== undefined)
176
179
  return props.active
177
180
 
181
+ if (isExternalLink.value || !props.to)
182
+ return false
183
+
178
184
  if (props.exactQuery === 'partial') {
179
185
  if (!isPartiallyEqual(linkRoute?.query, route.value?.query))
180
186
  return false
@@ -275,19 +281,28 @@ function resolveLinkClass({ route, isActive, isExactActive }: any = {}) {
275
281
  v-else
276
282
  v-slot="{ href, navigate, route: linkRoute, isActive, isExactActive }"
277
283
  v-bind="linkProps"
278
- :to="to || '#'"
284
+ :to="isExternalLink ? '#' : to || '#'"
279
285
  custom
280
286
  >
281
287
  <template v-if="custom">
282
288
  <slot
283
289
  v-bind="{
284
290
  ...$attrs,
291
+ ...isExternalLink
292
+ ? {
293
+ href: to || props.href,
294
+ target: props.target,
295
+ }
296
+ : {
297
+ href: to ? href : undefined,
298
+ target: undefined,
299
+ },
285
300
  as,
286
301
  type,
287
302
  disabled,
288
- href: to ? href : undefined,
289
303
  navigate,
290
304
  active: isLinkActive({ route: linkRoute, isActive, isExactActive }),
305
+ isExternal: isExternalLink,
291
306
  }"
292
307
  >
293
308
  {{ props.label }}
@@ -1,8 +1,8 @@
1
1
  <script lang="ts">
2
2
  import type { VariantProps } from '@byyuurin/ui-kit'
3
- import type { DialogContentProps, DialogRootEmits, DialogRootProps } from 'reka-ui'
3
+ import type { DialogContentEmits, DialogContentProps, DialogRootEmits, DialogRootProps } from 'reka-ui'
4
4
  import type { modal } from '../theme'
5
- import type { ButtonProps, ComponentAttrs } from '../types'
5
+ import type { ButtonProps, ComponentAttrs, EmitsToProps } from '../types'
6
6
 
7
7
  export interface ModalEmits extends DialogRootEmits {
8
8
  'after-leave': []
@@ -10,13 +10,13 @@ export interface ModalEmits extends DialogRootEmits {
10
10
 
11
11
  export interface ModalSlots {
12
12
  default?: (props: { open: boolean }) => any
13
- content?: (props?: {}) => any
14
- header?: (props?: {}) => any
15
- title?: (props?: {}) => any
16
- description?: (props?: {}) => any
17
- close?: (props?: {}) => any
18
- body?: (props?: {}) => any
19
- footer?: (props?: {}) => any
13
+ content?: any
14
+ header?: any
15
+ title?: any
16
+ description?: any
17
+ close?: (props: { ui: ComponentAttrs<typeof modal>['ui'] }) => any
18
+ body?: any
19
+ footer?: any
20
20
  }
21
21
 
22
22
  type ModalVariants = VariantProps<typeof modal>
@@ -25,7 +25,7 @@ export interface ModalProps extends ComponentAttrs<typeof modal>, DialogRootProp
25
25
  title?: string
26
26
  description?: string
27
27
  size?: ModalVariants['size']
28
- content?: Omit<DialogContentProps, 'as' | 'asChild' | 'forceMount'>
28
+ content?: Omit<DialogContentProps, 'as' | 'asChild' | 'forceMount'> & Partial<EmitsToProps<DialogContentEmits>>
29
29
  /** @default true */
30
30
  portal?: boolean
31
31
  /** @default true */
@@ -119,7 +119,7 @@ const style = computed(() => generateStyle('modal', props))
119
119
  </DialogTitle>
120
120
 
121
121
  <DialogClose v-if="props.close || slots.close" as-child>
122
- <slot name="close">
122
+ <slot name="close" :ui="props.ui">
123
123
  <Button
124
124
  variant="ghost"
125
125
  :icon="props.closeIcon || theme.app.icons.close"
@@ -5,10 +5,10 @@ import type { pinInput } from '../theme'
5
5
  import type { ComponentAttrs } from '../types'
6
6
 
7
7
  export interface PinInputEmits {
8
- (event: 'update:modelValue', value: string[]): void
9
- (event: 'complete', value: string[]): void
10
- (event: 'change', payload: Event): void
11
- (event: 'blur', payload: Event): void
8
+ 'update:modelValue': [value: string[]]
9
+ 'complete': [value: string[]]
10
+ 'change': [payload: Event]
11
+ 'blur': [payload: Event]
12
12
  }
13
13
 
14
14
  type PinInputVariants = VariantProps<typeof pinInput>
@@ -26,6 +26,7 @@ export interface PinInputProps extends ComponentAttrs<typeof pinInput>, Pick<Pin
26
26
  import { reactivePick } from '@vueuse/core'
27
27
  import { PinInputInput, PinInputRoot, useForwardPropsEmits } from 'reka-ui'
28
28
  import { computed, ref } from 'vue'
29
+ import { useFormItem } from '../composables/useFormItem'
29
30
  import { useTheme } from '../composables/useTheme'
30
31
  import { looseToNumber } from '../utils'
31
32
 
@@ -41,41 +42,49 @@ const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultValue', 'disa
41
42
 
42
43
  const completed = ref(false)
43
44
 
45
+ const { id, name, size, highlight, disabled, ariaAttrs, emitFormInput, emitFormChange, emitFormFocus, emitFormBlur } = useFormItem<PinInputProps>(props)
44
46
  const { generateStyle } = useTheme()
45
- const style = computed(() => generateStyle('pinInput', props))
47
+ const style = computed(() => generateStyle('pinInput', {
48
+ ...props,
49
+ size: size.value,
50
+ highlight: highlight.value,
51
+ }))
46
52
 
47
53
  function onComplete(value: string[]) {
48
54
  // @ts-expect-error - 'target' does not exist in type 'EventInit'
49
55
  const event = new Event('change', { target: { value } })
50
56
  emit('change', event)
57
+ emitFormChange()
51
58
  }
52
59
 
53
60
  function onBlur(event: FocusEvent) {
54
- if (!event.relatedTarget || completed.value)
61
+ if (!event.relatedTarget || completed.value) {
55
62
  emit('blur', event)
63
+ emitFormBlur()
64
+ }
56
65
  }
57
66
  </script>
58
67
 
59
68
  <template>
60
69
  <PinInputRoot
61
- v-bind="rootProps"
62
- :id="props.id"
63
- :name="props.name"
70
+ v-bind="{ ...rootProps, ...ariaAttrs, id, name }"
64
71
  :class="style.root({ class: [props.class, props.ui?.root] })"
72
+ @update:model-value="emitFormInput"
65
73
  @complete="onComplete"
66
74
  >
67
75
  <span
68
76
  v-for="(ids, index) in looseToNumber(props.length)"
69
77
  :key="ids"
70
78
  :class="style.container({ class: props.ui?.container })"
71
- :aria-disabled="props.disabled ? true : undefined"
79
+ :aria-disabled="disabled ? true : undefined"
72
80
  >
73
81
  <PinInputInput
74
- :index="index"
75
- :class="style.base({ class: props.ui?.base })"
76
82
  v-bind="$attrs"
77
- :disabled="props.disabled"
83
+ :class="style.base({ class: props.ui?.base })"
84
+ :index="index"
85
+ :disabled="disabled"
78
86
  @blur="onBlur"
87
+ @focus="emitFormFocus"
79
88
  />
80
89
  </span>
81
90
  </PinInputRoot>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
- import type { HoverCardRootProps, PopoverArrowProps, PopoverContentProps, PopoverRootEmits, PopoverRootProps } from 'reka-ui'
2
+ import type { HoverCardRootProps, PopoverArrowProps, PopoverContentEmits, PopoverContentProps, PopoverRootEmits, PopoverRootProps } from 'reka-ui'
3
3
  import type { popover } from '../theme'
4
- import type { ComponentAttrs } from '../types'
4
+ import type { ComponentAttrs, EmitsToProps } from '../types'
5
5
 
6
6
  export interface PopoverEmits extends PopoverRootEmits {}
7
7
 
@@ -17,7 +17,7 @@ export interface PopoverProps extends ComponentAttrs<typeof popover>, PopoverRoo
17
17
  */
18
18
  mode?: 'click' | 'hover'
19
19
  /** @default { side: 'bottom', sideOffset: 8, collisionPadding: 8 } */
20
- content?: Omit<PopoverContentProps, 'as' | 'asChild' | 'forceMount'>
20
+ content?: Omit<PopoverContentProps, 'as' | 'asChild' | 'forceMount'> & Partial<EmitsToProps<PopoverContentEmits>>
21
21
  arrow?: boolean | Omit<PopoverArrowProps, 'as' | 'asChild'>
22
22
  /** @default true */
23
23
  portal?: boolean
@@ -1,44 +1,35 @@
1
1
  <script lang="ts">
2
2
  import type { VariantProps } from '@byyuurin/ui-kit'
3
- import type { AcceptableValue, PrimitiveProps, RadioGroupRootProps } from 'reka-ui'
3
+ import type { PrimitiveProps, RadioGroupRootProps } from 'reka-ui'
4
4
  import type { radioGroup } from '../theme'
5
- import type { ComponentAttrs } from '../types'
5
+ import type { AcceptableValue, ComponentAttrs } from '../types'
6
6
 
7
7
  export interface RadioGroupEmits {
8
- (event: 'update:modelValue', payload: string): void
9
- (event: 'change', payload: Event): void
8
+ 'update:modelValue': [payload: string]
9
+ 'change': [payload: Event]
10
10
  }
11
11
 
12
- type SlotProps<T> = (props: { item: NormalizeItem<T>, modelValue?: AcceptableValue }) => any
12
+ export type RadioGroupValue = AcceptableValue
13
13
 
14
- export interface RadioGroupSlots<T> {
15
- legend?: (props?: {}) => any
14
+ export type RadioGroupItem = {
15
+ label?: string
16
+ description?: string
17
+ disabled?: boolean
18
+ value?: RadioGroupValue
19
+ [key: string]: any
20
+ } | RadioGroupValue
21
+
22
+ type SlotProps<T extends RadioGroupItem> = (props: { item: T & { id: string }, modelValue?: RadioGroupValue }) => any
23
+
24
+ export interface RadioGroupSlots<T extends RadioGroupItem = RadioGroupItem> {
25
+ legend?: any
16
26
  label?: SlotProps<T>
17
27
  description?: SlotProps<T>
18
28
  }
19
29
 
20
- type NormalizeItem<T> = { id: string } & (
21
- T extends RadioOption
22
- ? T
23
- : {
24
- id: string
25
- label: string
26
- value: any
27
- description: string
28
- disabled: boolean
29
- }
30
- )
31
-
32
30
  type RadioGroupVariants = VariantProps<typeof radioGroup>
33
31
 
34
- export interface RadioOption {
35
- label?: string
36
- description?: string
37
- disabled?: boolean
38
- value?: string
39
- }
40
-
41
- export interface RadioGroupProps<T> extends ComponentAttrs<typeof radioGroup>, Pick<RadioGroupRootProps, 'defaultValue' | 'disabled' | 'loop' | 'modelValue' | 'name' | 'required'> {
32
+ export interface RadioGroupProps<T extends RadioGroupItem = RadioGroupItem> extends ComponentAttrs<typeof radioGroup>, Pick<RadioGroupRootProps, 'defaultValue' | 'disabled' | 'loop' | 'modelValue' | 'name' | 'required'> {
42
33
  /**
43
34
  * The element or component this component should render as.
44
35
  * @default "div"
@@ -70,10 +61,11 @@ export interface RadioGroupProps<T> extends ComponentAttrs<typeof radioGroup>, P
70
61
  }
71
62
  </script>
72
63
 
73
- <script lang="ts" setup generic="T extends RadioOption | AcceptableValue">
64
+ <script lang="ts" setup generic="T extends RadioGroupItem">
74
65
  import { reactivePick } from '@vueuse/core'
75
66
  import { Label, RadioGroupIndicator, RadioGroupItem, RadioGroupRoot, useForwardPropsEmits } from 'reka-ui'
76
67
  import { computed, useId } from 'vue'
68
+ import { useFormItem } from '../composables/useFormItem'
77
69
  import { useTheme } from '../composables/useTheme'
78
70
  import { get } from '../utils'
79
71
 
@@ -88,20 +80,33 @@ const emit = defineEmits<RadioGroupEmits>()
88
80
  const slots = defineSlots<RadioGroupSlots<T>>()
89
81
 
90
82
  const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'orientation', 'loop', 'required'), emit)
91
- const id = useId()
83
+
84
+ const { id: _id, name, size, disabled, ariaAttrs, emitFormChange, emitFormInput } = useFormItem<RadioGroupProps<T>>(props)
85
+ const id = _id.value ?? useId()
92
86
 
93
87
  const { generateStyle } = useTheme()
94
- const style = computed(() => generateStyle('radioGroup', props))
88
+ const style = computed(() => generateStyle('radioGroup', {
89
+ ...props,
90
+ size: size.value,
91
+ disabled: disabled.value,
92
+ }))
93
+
94
+ function normalizeItem(item: any) {
95
+ if (item === null) {
96
+ return {
97
+ id: `${id}:null`,
98
+ label: undefined,
99
+ value: undefined,
100
+ }
101
+ }
95
102
 
96
- function normalizeItem(item: any): NormalizeItem<T> {
97
- if (['string', 'number', 'boolean'].includes(typeof item)) {
103
+ if (typeof item === 'string' || typeof item === 'number') {
98
104
  return {
99
105
  id: `${id}:${item}`,
106
+ label: String(item),
100
107
  value: item,
101
- label: item,
102
- description: '',
103
- disabled: props.disabled,
104
- } as any
108
+ disabled: disabled.value,
109
+ }
105
110
  }
106
111
 
107
112
  const value = get(item, props.valueKey)
@@ -110,11 +115,11 @@ function normalizeItem(item: any): NormalizeItem<T> {
110
115
 
111
116
  return {
112
117
  ...item,
113
- value,
118
+ id: `${id}:${value}`,
114
119
  label,
120
+ value,
115
121
  description,
116
- id: `${id}:${value}`,
117
- disabled: props.disabled || item.disabled,
122
+ disabled: disabled.value || item.disabled,
118
123
  }
119
124
  }
120
125
 
@@ -129,20 +134,19 @@ function onUpdate(value: any) {
129
134
  // @ts-expect-error - 'target' does not exist in type 'EventInit'
130
135
  const event = new Event('change', { target: { value } })
131
136
  emit('change', event)
137
+ emitFormChange()
138
+ emitFormInput()
132
139
  }
133
140
  </script>
134
141
 
135
142
  <template>
136
143
  <RadioGroupRoot
137
- :id="id"
138
144
  v-slot="{ modelValue }"
139
- v-bind="rootProps"
140
- :name="props.name"
141
- :disabled="props.disabled"
145
+ v-bind="{ ...rootProps, id, name, disabled }"
142
146
  :class="style.root({ class: [props.class, props.ui?.root] })"
143
147
  @update:model-value="onUpdate"
144
148
  >
145
- <fieldset :class="style.fieldset({ class: props.ui?.fieldset })">
149
+ <fieldset v-bind="ariaAttrs" :class="style.fieldset({ class: props.ui?.fieldset })">
146
150
  <legend v-if="props.legend || slots.legend" :class="style.legend({ class: props.ui?.legend })">
147
151
  <slot name="legend">
148
152
  {{ props.legend }}
@@ -162,10 +166,10 @@ function onUpdate(value: any) {
162
166
 
163
167
  <div :class="style.wrapper({ class: props.ui?.wrapper })">
164
168
  <Label :for="item.id" :class="style.label({ class: props.ui?.label })">
165
- <slot name="label" :item="item" :model-value="modelValue">{{ item.label }}</slot>
169
+ <slot name="label" :item="item" :model-value="(modelValue as RadioGroupValue)">{{ item.label }}</slot>
166
170
  </Label>
167
171
  <p v-if="item.description || slots.description" :class="style.description({ class: props.ui?.description })">
168
- <slot name="description" :item="item" :model-value="modelValue">
172
+ <slot name="description" :item="item" :model-value="(modelValue as RadioGroupValue)">
169
173
  {{ item.description }}
170
174
  </slot>
171
175
  </p>