@xy-planning-network/trees 0.7.4 → 0.7.5-rc1
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/trees.es.js +3939 -3561
- package/dist/trees.umd.js +10 -10
- package/package.json +2 -2
- package/src/index.css +0 -16
- package/src/lib-components/forms/BaseInput.vue +90 -75
- package/src/lib-components/forms/Checkbox.vue +50 -34
- package/src/lib-components/forms/DateRangePicker.vue +56 -32
- package/src/lib-components/forms/FieldsetLegend.vue +28 -8
- package/src/lib-components/forms/InputHelp.vue +2 -4
- package/src/lib-components/forms/InputLabel.vue +27 -12
- package/src/lib-components/forms/MultiCheckboxes.vue +117 -74
- package/src/lib-components/forms/Radio.vue +79 -66
- package/src/lib-components/forms/RadioCards.vue +72 -70
- package/src/lib-components/forms/Select.vue +59 -56
- package/src/lib-components/forms/TextArea.vue +54 -47
- package/src/lib-components/forms/Toggle.vue +61 -18
- package/src/lib-components/forms/YesOrNoRadio.vue +75 -67
- package/src/lib-components/lists/DynamicTable.vue +43 -20
- package/types/composables/forms.d.ts +118 -0
- package/types/helpers/Debounce.d.ts +1 -1
- package/types/lib-components/forms/BaseInput.vue.d.ts +58 -34
- package/types/lib-components/forms/Checkbox.vue.d.ts +50 -29
- package/types/lib-components/forms/DateRangePicker.vue.d.ts +71 -39
- package/types/lib-components/forms/FieldsetLegend.vue.d.ts +30 -31
- package/types/lib-components/forms/InputHelp.vue.d.ts +19 -27
- package/types/lib-components/forms/InputLabel.vue.d.ts +28 -32
- package/types/lib-components/forms/MultiCheckboxes.vue.d.ts +77 -56
- package/types/lib-components/forms/Radio.vue.d.ts +64 -56
- package/types/lib-components/forms/RadioCards.vue.d.ts +68 -71
- package/types/lib-components/forms/Select.vue.d.ts +55 -44
- package/types/lib-components/forms/TextArea.vue.d.ts +50 -32
- package/types/lib-components/forms/Toggle.vue.d.ts +29 -24
- package/types/lib-components/forms/YesOrNoRadio.vue.d.ts +51 -38
- package/types/lib-components/indicators/XYSpinner.vue.d.ts +1 -1
- package/types/lib-components/layout/DateFilter.vue.d.ts +28 -20
- package/types/lib-components/layout/SidebarLayout.vue.d.ts +38 -32
- package/types/lib-components/layout/StackedLayout.vue.d.ts +45 -33
- package/types/lib-components/lists/Cards.vue.d.ts +14 -17
- package/types/lib-components/lists/DataTable.vue.d.ts +31 -31
- package/types/lib-components/lists/DetailList.vue.d.ts +38 -34
- package/types/lib-components/lists/DownloadCell.vue.d.ts +18 -15
- package/types/lib-components/lists/DynamicTable.vue.d.ts +32 -32
- package/types/lib-components/lists/StaticTable.vue.d.ts +21 -0
- package/types/lib-components/lists/StaticTableActionSlot.vue.d.ts +27 -0
- package/types/lib-components/lists/Table.vue.d.ts +39 -0
- package/types/lib-components/lists/TableActionButtons.vue.d.ts +11 -23
- package/types/lib-components/navigation/ActionsDropdown.vue.d.ts +11 -23
- package/types/lib-components/navigation/ActionsDropdownCallback.vue.d.ts +18 -0
- package/types/lib-components/navigation/ActionsDropdownEmit.vue.d.ts +22 -0
- package/types/lib-components/navigation/Paginator.vue.d.ts +12 -15
- package/types/lib-components/navigation/Steps.vue.d.ts +54 -39
- package/types/lib-components/navigation/Tabs.vue.d.ts +34 -34
- package/types/lib-components/overlays/ContentModal.vue.d.ts +22 -28
- package/types/lib-components/overlays/Modal.vue.d.ts +48 -43
- package/types/lib-components/overlays/Popover/Popover.vue.d.ts +23 -31
- package/types/lib-components/overlays/Popover/PopoverContent.vue.d.ts +1 -1
- package/types/lib-components/overlays/Slideover.vue.d.ts +30 -22
- package/types/lib-components/overlays/Tooltip.vue.d.ts +20 -28
|
@@ -1,75 +1,122 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import Uniques from "@/helpers/Uniques"
|
|
3
|
-
import { computed, useAttrs, useSlots } from "vue"
|
|
4
2
|
import FieldsetLegend from "./FieldsetLegend.vue"
|
|
5
3
|
import InputLabel from "./InputLabel.vue"
|
|
6
4
|
import InputHelp from "./InputHelp.vue"
|
|
5
|
+
import { useInputField, defaultInputProps } from "@/composables/forms"
|
|
6
|
+
import type { MultiChoiceInput, ColumnedInput } from "@/composables/forms"
|
|
7
|
+
import { computed, ref } from "vue"
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
defineOptions({
|
|
10
|
+
inheritAttrs: false,
|
|
11
|
+
})
|
|
10
12
|
|
|
11
13
|
const props = withDefaults(
|
|
12
|
-
defineProps<
|
|
13
|
-
|
|
14
|
-
disabled?: boolean
|
|
15
|
-
help?: string
|
|
16
|
-
label: string
|
|
17
|
-
value: CheckboxValue
|
|
18
|
-
}[]
|
|
19
|
-
help?: string
|
|
20
|
-
legend?: string
|
|
21
|
-
modelValue: ModelValue
|
|
22
|
-
columns?: 2 | 3 | 4
|
|
23
|
-
}>(),
|
|
24
|
-
{
|
|
25
|
-
help: "",
|
|
26
|
-
legend: "",
|
|
27
|
-
columns: undefined,
|
|
28
|
-
}
|
|
14
|
+
defineProps<MultiChoiceInput & ColumnedInput>(),
|
|
15
|
+
defaultInputProps
|
|
29
16
|
)
|
|
30
17
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
18
|
+
defineEmits(["update:modelValue", "update:error"])
|
|
19
|
+
const targetInput = ref<HTMLInputElement | null>(null)
|
|
20
|
+
const { inputID, isDisabled, modelState, errorState, validate } = useInputField(
|
|
21
|
+
{ props, targetInput }
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const onChange = (e: Event, val: string | number) => {
|
|
25
|
+
const checked = (e.target as HTMLInputElement).checked
|
|
26
|
+
|
|
27
|
+
if (!Array.isArray(modelState.value)) {
|
|
28
|
+
modelState.value = []
|
|
29
|
+
}
|
|
42
30
|
|
|
43
31
|
if (checked) {
|
|
44
|
-
|
|
32
|
+
modelState.value.push(val)
|
|
45
33
|
} else {
|
|
46
|
-
|
|
34
|
+
modelState.value.splice(modelState.value.indexOf(val), 1)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
validate(e)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const selectionCount = computed(() => {
|
|
41
|
+
if (Array.isArray(modelState.value)) {
|
|
42
|
+
return modelState.value.length
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return 0
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const minCount = computed(() => {
|
|
49
|
+
return props.min || 0
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const maxCount = computed(() => {
|
|
53
|
+
return (
|
|
54
|
+
props.max ||
|
|
55
|
+
props.options.filter((opt) => {
|
|
56
|
+
return !opt.disabled
|
|
57
|
+
}).length
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const countError = computed(() => {
|
|
62
|
+
if (selectionCount.value < minCount.value) {
|
|
63
|
+
return `Please select ${minCount.value} of these option${
|
|
64
|
+
minCount.value > 1 ? "s" : ""
|
|
65
|
+
}.`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (selectionCount.value > maxCount.value) {
|
|
69
|
+
return `Please select only ${maxCount.value} of these option${
|
|
70
|
+
maxCount.value > 1 ? "s" : ""
|
|
71
|
+
}.`
|
|
47
72
|
}
|
|
48
73
|
|
|
49
|
-
|
|
74
|
+
return ""
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const setValidationError = () => {
|
|
78
|
+
if (!errorState.value) {
|
|
79
|
+
errorState.value = countError.value
|
|
80
|
+
}
|
|
50
81
|
}
|
|
51
82
|
</script>
|
|
83
|
+
|
|
52
84
|
<template>
|
|
53
85
|
<fieldset
|
|
54
|
-
class="space-y-
|
|
55
|
-
:aria-labelledby="
|
|
56
|
-
:aria-describedby="help ? `${
|
|
86
|
+
class="relative space-y-4"
|
|
87
|
+
:aria-labelledby="label ? `${inputID}-legend` : undefined"
|
|
88
|
+
:aria-describedby="help ? `${inputID}-help` : undefined"
|
|
57
89
|
>
|
|
58
|
-
<div v-if="
|
|
59
|
-
<FieldsetLegend
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
90
|
+
<div v-if="label">
|
|
91
|
+
<FieldsetLegend
|
|
92
|
+
:id="`${inputID}-legend`"
|
|
93
|
+
:label="label"
|
|
94
|
+
:required="minCount > 0"
|
|
95
|
+
/>
|
|
96
|
+
<InputHelp v-if="help" :id="`${inputID}-help`" tag="p" :text="help" />
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div v-if="errorState" class="mt-0.5">
|
|
100
|
+
<p class="text-sm text-red-700">{{ errorState }}</p>
|
|
64
101
|
</div>
|
|
102
|
+
|
|
103
|
+
<input
|
|
104
|
+
v-if="countError || errorState"
|
|
105
|
+
ref="targetInput"
|
|
106
|
+
required
|
|
107
|
+
class="sr-only top-1 left-1"
|
|
108
|
+
aria-hidden
|
|
109
|
+
type="checkbox"
|
|
110
|
+
@invalid="setValidationError"
|
|
111
|
+
/>
|
|
112
|
+
|
|
65
113
|
<div class="flex">
|
|
66
114
|
<div
|
|
67
|
-
class="grid gap-
|
|
115
|
+
class="grid gap-y-6"
|
|
68
116
|
:class="{
|
|
69
|
-
'sm:grid sm:gap-
|
|
117
|
+
'sm:grid sm:gap-x-5 sm:space-y-0': columns !== undefined,
|
|
70
118
|
'sm:grid-cols-2': columns === 2,
|
|
71
119
|
'sm:grid-cols-3': columns === 3,
|
|
72
|
-
'sm:grid-cols-4': columns === 4,
|
|
73
120
|
}"
|
|
74
121
|
>
|
|
75
122
|
<div
|
|
@@ -79,42 +126,38 @@ const onChange = (checked: boolean, val: CheckboxValue) => {
|
|
|
79
126
|
>
|
|
80
127
|
<div class="flex items-center h-5">
|
|
81
128
|
<input
|
|
82
|
-
:id="`${
|
|
83
|
-
:aria-labelledby="`${
|
|
129
|
+
:id="`${inputID}-${index}`"
|
|
130
|
+
:aria-labelledby="`${inputID}-${index}-label`"
|
|
84
131
|
:aria-describedby="
|
|
85
|
-
option
|
|
86
|
-
? `${uuid}-${index}-help`
|
|
87
|
-
: undefined
|
|
132
|
+
option.help ? `${inputID}-${index}-help` : undefined
|
|
88
133
|
"
|
|
89
|
-
:checked="modelValue
|
|
90
|
-
:disabled="option.disabled
|
|
91
|
-
class="
|
|
134
|
+
:checked="modelValue?.includes(option.value)"
|
|
135
|
+
:disabled="option.disabled"
|
|
136
|
+
:class="[
|
|
137
|
+
'h-4 w-4 rounded cursor-pointer',
|
|
138
|
+
'disabled:bg-gray-100 disabled:border-gray-200 disabled:cursor-not-allowed disabled:opacity-100',
|
|
139
|
+
'checked:disabled:bg-xy-blue checked:disabled:border-xy-blue checked:disabled:opacity-50',
|
|
140
|
+
errorState
|
|
141
|
+
? 'border-red-700 focus:ring-red-700'
|
|
142
|
+
: 'border-gray-300 focus:ring-xy-blue-500',
|
|
143
|
+
]"
|
|
92
144
|
type="checkbox"
|
|
93
|
-
v-bind="
|
|
94
|
-
onChange
|
|
95
|
-
onChange(($event.target as HTMLInputElement).checked, option.value)
|
|
96
|
-
},
|
|
97
|
-
...$attrs,
|
|
98
|
-
}"
|
|
145
|
+
v-bind="$attrs"
|
|
146
|
+
@change="onChange($event, option.value)"
|
|
99
147
|
/>
|
|
100
148
|
</div>
|
|
101
149
|
<div class="ml-3">
|
|
102
150
|
<InputLabel
|
|
103
|
-
:id="`${
|
|
104
|
-
|
|
105
|
-
:disabled="
|
|
106
|
-
($attrs.hasOwnProperty('disabled') &&
|
|
107
|
-
$attrs.disabled !== false) ||
|
|
108
|
-
option.disabled === true
|
|
109
|
-
"
|
|
110
|
-
:for="`${uuid}-${index}`"
|
|
151
|
+
:id="`${inputID}-${index}-label`"
|
|
152
|
+
:for="`${inputID}-${index}`"
|
|
111
153
|
:label="option.label"
|
|
154
|
+
:class="
|
|
155
|
+
isDisabled || option.disabled
|
|
156
|
+
? 'cursor-not-allowed'
|
|
157
|
+
: 'cursor-pointer'
|
|
158
|
+
"
|
|
112
159
|
/>
|
|
113
|
-
<InputHelp
|
|
114
|
-
:id="`${uuid}-${index}-help`"
|
|
115
|
-
class="-mt-1"
|
|
116
|
-
:text="option.help"
|
|
117
|
-
/>
|
|
160
|
+
<InputHelp :id="`${inputID}-${index}-help`" :text="option.help" />
|
|
118
161
|
</div>
|
|
119
162
|
</div>
|
|
120
163
|
</div>
|
|
@@ -1,59 +1,74 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import
|
|
3
|
-
import { computed, useAttrs, useSlots } from "vue"
|
|
2
|
+
import { computed, ref } from "vue"
|
|
4
3
|
import FieldsetLegend from "./FieldsetLegend.vue"
|
|
5
4
|
import InputHelp from "./InputHelp.vue"
|
|
6
5
|
import InputLabel from "./InputLabel.vue"
|
|
6
|
+
import { useInputField, defaultInputProps } from "@/composables/forms"
|
|
7
|
+
import type { OptionsInput, ColumnedInput } from "@/composables/forms"
|
|
8
|
+
|
|
9
|
+
defineOptions({
|
|
10
|
+
inheritAttrs: false,
|
|
11
|
+
})
|
|
7
12
|
|
|
8
13
|
const props = withDefaults(
|
|
9
|
-
defineProps<
|
|
10
|
-
|
|
11
|
-
disabled?: boolean
|
|
12
|
-
help?: string
|
|
13
|
-
label: string
|
|
14
|
-
value: string | number
|
|
15
|
-
}[]
|
|
16
|
-
help?: string
|
|
17
|
-
legend?: string
|
|
18
|
-
modelValue?: string | number
|
|
19
|
-
columns?: 2 | 3 | 4
|
|
20
|
-
}>(),
|
|
21
|
-
{
|
|
22
|
-
help: "",
|
|
23
|
-
legend: "",
|
|
24
|
-
modelValue: undefined,
|
|
25
|
-
columns: undefined,
|
|
26
|
-
}
|
|
14
|
+
defineProps<OptionsInput & ColumnedInput>(),
|
|
15
|
+
defaultInputProps
|
|
27
16
|
)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
17
|
+
|
|
18
|
+
defineEmits(["update:modelValue", "update:error"])
|
|
19
|
+
|
|
20
|
+
const radios = ref<HTMLInputElement[]>([])
|
|
21
|
+
// there are multiple radio buttons that could be the target
|
|
22
|
+
// for validation set to the first input
|
|
23
|
+
const targetInput = computed(() => {
|
|
24
|
+
if (radios.value.length === 0) {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return radios.value[0]
|
|
34
29
|
})
|
|
30
|
+
const {
|
|
31
|
+
errorState,
|
|
32
|
+
modelState,
|
|
33
|
+
inputID,
|
|
34
|
+
isDisabled,
|
|
35
|
+
isRequired,
|
|
36
|
+
onInvalid,
|
|
37
|
+
validate,
|
|
38
|
+
} = useInputField({ props, targetInput })
|
|
39
|
+
|
|
40
|
+
const onChange = (e: Event, val: string | number) => {
|
|
41
|
+
modelState.value = val
|
|
42
|
+
validate(e)
|
|
43
|
+
}
|
|
35
44
|
</script>
|
|
45
|
+
|
|
36
46
|
<template>
|
|
37
47
|
<fieldset
|
|
38
|
-
class="space-y-
|
|
39
|
-
:aria-labelledby="
|
|
40
|
-
:aria-describedby="help ? `${
|
|
48
|
+
class="space-y-4"
|
|
49
|
+
:aria-labelledby="label ? `${inputID}-legend` : undefined"
|
|
50
|
+
:aria-describedby="help ? `${inputID}-help` : undefined"
|
|
41
51
|
>
|
|
42
|
-
<div v-if="
|
|
43
|
-
<FieldsetLegend
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
<div v-if="label">
|
|
53
|
+
<FieldsetLegend
|
|
54
|
+
:id="`${inputID}-legend`"
|
|
55
|
+
:label="label"
|
|
56
|
+
:required="isRequired"
|
|
57
|
+
/>
|
|
58
|
+
<InputHelp v-if="help" :id="`${inputID}-help`" tag="p" :text="help" />
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div v-if="errorState" class="mt-0.5">
|
|
62
|
+
<p class="text-sm text-red-700">{{ errorState }}</p>
|
|
48
63
|
</div>
|
|
64
|
+
|
|
49
65
|
<div class="flex">
|
|
50
66
|
<div
|
|
51
|
-
class="grid gap-
|
|
67
|
+
class="grid gap-y-6"
|
|
52
68
|
:class="{
|
|
53
|
-
'sm:grid sm:gap-
|
|
69
|
+
'sm:grid sm:gap-x-5 sm:space-y-0': columns !== undefined,
|
|
54
70
|
'sm:grid-cols-2': columns === 2,
|
|
55
71
|
'sm:grid-cols-3': columns === 3,
|
|
56
|
-
'sm:grid-cols-4': columns === 4,
|
|
57
72
|
}"
|
|
58
73
|
>
|
|
59
74
|
<div
|
|
@@ -63,44 +78,42 @@ const hasLegend = computed(() => {
|
|
|
63
78
|
>
|
|
64
79
|
<div class="flex items-center h-5">
|
|
65
80
|
<input
|
|
66
|
-
:id="`${
|
|
81
|
+
:id="`${inputID}-${index}`"
|
|
82
|
+
ref="radios"
|
|
67
83
|
:aria-describedby="
|
|
68
|
-
option
|
|
69
|
-
? `${uuid}-${index}-help`
|
|
70
|
-
: undefined
|
|
84
|
+
option.help ? `${inputID}-${index}-help` : undefined
|
|
71
85
|
"
|
|
72
|
-
:aria-labelledby="`${
|
|
73
|
-
:checked="
|
|
74
|
-
class="
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
:aria-labelledby="`${inputID}-${index}-label`"
|
|
87
|
+
:checked="modelState === option.value"
|
|
88
|
+
:class="[
|
|
89
|
+
'h-4 w-4 cursor-pointer text-xy-blue',
|
|
90
|
+
'disabled:bg-gray-100 disabled:border-gray-200 disabled:cursor-not-allowed disabled:opacity-100',
|
|
91
|
+
'checked:disabled:bg-xy-blue checked:disabled:border-xy-blue checked:disabled:opacity-50',
|
|
92
|
+
errorState
|
|
93
|
+
? 'border-red-700 focus:ring-red-700'
|
|
94
|
+
: 'border-gray-300 focus:ring-xy-blue-500',
|
|
95
|
+
]"
|
|
96
|
+
:disabled="option.disabled"
|
|
97
|
+
:name="inputID"
|
|
77
98
|
type="radio"
|
|
78
99
|
:value="option.value"
|
|
79
|
-
v-bind="
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
},
|
|
83
|
-
...$attrs,
|
|
84
|
-
}"
|
|
100
|
+
v-bind="$attrs"
|
|
101
|
+
@change="onChange($event, option.value)"
|
|
102
|
+
@invalid="onInvalid"
|
|
85
103
|
/>
|
|
86
104
|
</div>
|
|
87
105
|
<div class="ml-3">
|
|
88
106
|
<InputLabel
|
|
89
|
-
:id="`${
|
|
90
|
-
|
|
91
|
-
:disabled="
|
|
92
|
-
($attrs.hasOwnProperty('disabled') &&
|
|
93
|
-
$attrs.disabled !== false) ||
|
|
94
|
-
option.disabled === true
|
|
95
|
-
"
|
|
96
|
-
:for="`${uuid}-${index}`"
|
|
107
|
+
:id="`${inputID}-${index}-label`"
|
|
108
|
+
:for="`${inputID}-${index}`"
|
|
97
109
|
:label="option.label"
|
|
110
|
+
:class="
|
|
111
|
+
isDisabled || option.disabled
|
|
112
|
+
? 'cursor-not-allowed'
|
|
113
|
+
: 'cursor-pointer'
|
|
114
|
+
"
|
|
98
115
|
/>
|
|
99
|
-
<InputHelp
|
|
100
|
-
:id="`${uuid}-${index}-help`"
|
|
101
|
-
class="-mt-1"
|
|
102
|
-
:text="option.help"
|
|
103
|
-
/>
|
|
116
|
+
<InputHelp :id="`${inputID}-${index}-help`" :text="option.help" />
|
|
104
117
|
</div>
|
|
105
118
|
</div>
|
|
106
119
|
</div>
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import Uniques from "@/helpers/Uniques"
|
|
1
|
+
<script setup lang="ts" generic="T extends InputOption">
|
|
3
2
|
import {
|
|
4
3
|
RadioGroup,
|
|
5
4
|
RadioGroupDescription,
|
|
@@ -7,95 +6,82 @@ import {
|
|
|
7
6
|
RadioGroupOption,
|
|
8
7
|
} from "@headlessui/vue"
|
|
9
8
|
import { CheckCircleIcon } from "@heroicons/vue/solid"
|
|
10
|
-
import { computed, ref, useAttrs } from "vue"
|
|
11
9
|
import InputLabel from "./InputLabel.vue"
|
|
12
10
|
import InputHelp from "./InputHelp.vue"
|
|
13
11
|
import FieldsetLegend from "./FieldsetLegend.vue"
|
|
12
|
+
import { defaultInputProps, useInputField } from "@/composables/forms"
|
|
13
|
+
import type {
|
|
14
|
+
ColumnedInput,
|
|
15
|
+
InputOption,
|
|
16
|
+
OptionsInput,
|
|
17
|
+
} from "@/composables/forms"
|
|
18
|
+
import { computed, ref } from "vue"
|
|
19
|
+
|
|
20
|
+
defineOptions({
|
|
21
|
+
inheritAttrs: false,
|
|
22
|
+
})
|
|
14
23
|
|
|
15
24
|
/*
|
|
16
25
|
* NOTE (spk) headless UI introduced a "name" prop that includes a hidden field
|
|
17
26
|
* to use the modelValue inside of forms. It does not however resolve the issue of
|
|
18
27
|
* supporting HTML5 form validation, so we'll add our own hidden radio buttons to support both.
|
|
19
|
-
*
|
|
20
|
-
* The headless technique does include supporting complex modelValues such as objects, which we may
|
|
21
|
-
* need in the future. We can revist required validation at that time using a singular hidden checkbox.
|
|
22
28
|
*/
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
type RadioCard = {
|
|
27
|
-
disabled?: boolean
|
|
28
|
-
help?: string
|
|
29
|
-
label: string
|
|
30
|
-
sublabel?: string
|
|
31
|
-
value: ModelValue
|
|
30
|
+
interface RadioCards extends OptionsInput {
|
|
31
|
+
options: T[]
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const props = withDefaults(
|
|
35
|
-
defineProps<
|
|
36
|
-
|
|
37
|
-
help?: string
|
|
38
|
-
legend?: string
|
|
39
|
-
modelValue?: ModelValue
|
|
40
|
-
options: RadioCard[]
|
|
41
|
-
}>(),
|
|
42
|
-
{
|
|
43
|
-
columns: undefined,
|
|
44
|
-
help: "",
|
|
45
|
-
legend: "",
|
|
46
|
-
modelValue: undefined,
|
|
47
|
-
}
|
|
35
|
+
defineProps<RadioCards & ColumnedInput>(),
|
|
36
|
+
defaultInputProps
|
|
48
37
|
)
|
|
49
38
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// tracking internal state separate from modelValue
|
|
58
|
-
// allows v-model to be undefined by the consumer but still supports
|
|
59
|
-
// the display requirements of the component.
|
|
60
|
-
// this is usful when the component is used inside a form element and
|
|
61
|
-
// tracking v-model isn't required.
|
|
62
|
-
const internalState = ref()
|
|
63
|
-
const invalid = ref<boolean>()
|
|
64
|
-
const checkedState = computed(() => {
|
|
65
|
-
if (props.modelValue === undefined) {
|
|
66
|
-
return internalState.value
|
|
39
|
+
defineEmits(["update:modelValue", "update:error"])
|
|
40
|
+
const hiddenRadios = ref<HTMLInputElement[]>([])
|
|
41
|
+
// there are multiple radio buttons that could be the target
|
|
42
|
+
// for validation set to the first input
|
|
43
|
+
const targetInput = computed(() => {
|
|
44
|
+
if (hiddenRadios.value.length === 0) {
|
|
45
|
+
return null
|
|
67
46
|
}
|
|
68
47
|
|
|
69
|
-
return
|
|
48
|
+
return hiddenRadios.value[0]
|
|
70
49
|
})
|
|
50
|
+
const {
|
|
51
|
+
inputID,
|
|
52
|
+
isDisabled,
|
|
53
|
+
isRequired,
|
|
54
|
+
nameAttr,
|
|
55
|
+
modelState,
|
|
56
|
+
errorState,
|
|
57
|
+
onInvalid,
|
|
58
|
+
} = useInputField({ props, targetInput })
|
|
71
59
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
60
|
+
const onUpdate = (val: unknown) => {
|
|
61
|
+
if (val) {
|
|
62
|
+
errorState.value = ""
|
|
63
|
+
}
|
|
76
64
|
}
|
|
77
|
-
|
|
78
|
-
const nameAttr = computed(() => {
|
|
79
|
-
return typeof attrs.name === "string" && attrs.name !== "" ? attrs.name : uuid
|
|
80
|
-
})
|
|
81
65
|
</script>
|
|
82
66
|
|
|
83
67
|
<template>
|
|
84
68
|
<RadioGroup
|
|
85
|
-
|
|
86
|
-
:disabled="
|
|
87
|
-
:aria-invalid="
|
|
88
|
-
:aria-errormessage="
|
|
89
|
-
@update:model-value="
|
|
69
|
+
v-model="modelState"
|
|
70
|
+
:disabled="isDisabled"
|
|
71
|
+
:aria-invalid="errorState ? 'true' : null"
|
|
72
|
+
:aria-errormessage="errorState ? `error-${inputID}` : null"
|
|
73
|
+
@update:model-value="onUpdate"
|
|
90
74
|
>
|
|
91
|
-
<RadioGroupLabel v-if="
|
|
92
|
-
<FieldsetLegend tag="div"
|
|
75
|
+
<RadioGroupLabel v-if="label" class="block">
|
|
76
|
+
<FieldsetLegend tag="div" :label="label" :required="isRequired" />
|
|
93
77
|
</RadioGroupLabel>
|
|
78
|
+
|
|
94
79
|
<RadioGroupDescription v-if="help">
|
|
95
80
|
<InputHelp :text="help" />
|
|
96
81
|
</RadioGroupDescription>
|
|
97
|
-
|
|
98
|
-
|
|
82
|
+
|
|
83
|
+
<div v-if="errorState" :id="`error-${inputID}`">
|
|
84
|
+
<p class="text-sm text-red-700">{{ errorState }}</p>
|
|
99
85
|
</div>
|
|
100
86
|
|
|
101
87
|
<div
|
|
@@ -108,17 +94,28 @@ const nameAttr = computed(() => {
|
|
|
108
94
|
<RadioGroupOption
|
|
109
95
|
v-for="option in options"
|
|
110
96
|
:key="option.value"
|
|
111
|
-
v-slot="{
|
|
97
|
+
v-slot="{
|
|
98
|
+
active,
|
|
99
|
+
checked,
|
|
100
|
+
disabled,
|
|
101
|
+
}: {
|
|
102
|
+
active: boolean,
|
|
103
|
+
checked: boolean,
|
|
104
|
+
disabled: boolean,
|
|
105
|
+
}"
|
|
112
106
|
as="template"
|
|
113
|
-
:disabled="
|
|
107
|
+
:disabled="isDisabled || option.disabled"
|
|
114
108
|
:value="option.value"
|
|
115
109
|
>
|
|
116
110
|
<div
|
|
117
|
-
class="relative
|
|
111
|
+
class="relative border rounded-lg shadow-sm p-4 flex focus:outline-none"
|
|
118
112
|
:class="[
|
|
119
|
-
|
|
113
|
+
disabled
|
|
114
|
+
? 'cursor-not-allowed bg-gray-50 border-gray-200 opacity-90'
|
|
115
|
+
: 'cursor-pointer bg-white border-gray-300',
|
|
116
|
+
errorState && !disabled ? 'border-red-700' : '',
|
|
117
|
+
checked ? 'border-transparent' : '',
|
|
120
118
|
active ? 'border-xy-blue ring-2 ring-xy-blue-500' : '',
|
|
121
|
-
disabled ? 'cursor-not-allowed opacity-75' : '',
|
|
122
119
|
]"
|
|
123
120
|
>
|
|
124
121
|
<div class="flex-1 flex pr-1">
|
|
@@ -130,16 +127,18 @@ const nameAttr = computed(() => {
|
|
|
130
127
|
:label="option.label"
|
|
131
128
|
/>
|
|
132
129
|
</RadioGroupLabel>
|
|
130
|
+
|
|
133
131
|
<RadioGroupDescription v-if="option.help" as="div">
|
|
134
132
|
<InputHelp tag="div" class="mt-auto" :text="option.help" />
|
|
135
133
|
</RadioGroupDescription>
|
|
134
|
+
|
|
136
135
|
<div
|
|
137
136
|
v-if="option.sublabel || $slots.sublabel"
|
|
138
137
|
class="mt-auto mb-0"
|
|
139
138
|
>
|
|
140
139
|
<RadioGroupDescription
|
|
141
140
|
as="div"
|
|
142
|
-
class="
|
|
141
|
+
class="text-sm/5 font-medium mt-4 text-gray-800"
|
|
143
142
|
>
|
|
144
143
|
<slot
|
|
145
144
|
name="sublabel"
|
|
@@ -165,16 +164,19 @@ const nameAttr = computed(() => {
|
|
|
165
164
|
]"
|
|
166
165
|
aria-hidden="true"
|
|
167
166
|
/>
|
|
167
|
+
|
|
168
|
+
<!--TODO: (spk) ideally this would trigger a change event -->
|
|
168
169
|
<input
|
|
170
|
+
ref="hiddenRadios"
|
|
169
171
|
class="sr-only top-1 left-1"
|
|
170
172
|
aria-hidden="true"
|
|
171
173
|
:checked="checked"
|
|
172
174
|
:name="nameAttr"
|
|
173
|
-
:required="
|
|
175
|
+
:required="isRequired"
|
|
174
176
|
tabindex="-1"
|
|
175
177
|
type="radio"
|
|
176
178
|
:value="option.value"
|
|
177
|
-
@invalid="
|
|
179
|
+
@invalid="onInvalid"
|
|
178
180
|
/>
|
|
179
181
|
</div>
|
|
180
182
|
</RadioGroupOption>
|