@xy-planning-network/trees 0.7.5-dev → 0.7.5-rc2

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 (51) hide show
  1. package/dist/trees.es.js +4058 -3743
  2. package/dist/trees.umd.js +10 -10
  3. package/package.json +2 -2
  4. package/src/lib-components/forms/BaseInput.vue +80 -42
  5. package/src/lib-components/forms/Checkbox.vue +36 -26
  6. package/src/lib-components/forms/DateRangePicker.vue +50 -30
  7. package/src/lib-components/forms/FieldsetLegend.vue +23 -3
  8. package/src/lib-components/forms/InputHelp.vue +1 -1
  9. package/src/lib-components/forms/InputLabel.vue +24 -4
  10. package/src/lib-components/forms/MultiCheckboxes.vue +95 -44
  11. package/src/lib-components/forms/Radio.vue +59 -37
  12. package/src/lib-components/forms/RadioCards.vue +45 -57
  13. package/src/lib-components/forms/Select.vue +37 -40
  14. package/src/lib-components/forms/TextArea.vue +36 -28
  15. package/src/lib-components/forms/Toggle.vue +9 -6
  16. package/src/lib-components/forms/YesOrNoRadio.vue +49 -35
  17. package/src/lib-components/lists/DynamicTable.vue +43 -20
  18. package/types/composables/forms.d.ts +110 -2
  19. package/types/lib-components/forms/BaseInput.vue.d.ts +60 -34
  20. package/types/lib-components/forms/Checkbox.vue.d.ts +50 -29
  21. package/types/lib-components/forms/DateRangePicker.vue.d.ts +71 -39
  22. package/types/lib-components/forms/FieldsetLegend.vue.d.ts +28 -27
  23. package/types/lib-components/forms/InputHelp.vue.d.ts +19 -27
  24. package/types/lib-components/forms/InputLabel.vue.d.ts +28 -27
  25. package/types/lib-components/forms/MultiCheckboxes.vue.d.ts +75 -47
  26. package/types/lib-components/forms/Radio.vue.d.ts +62 -47
  27. package/types/lib-components/forms/RadioCards.vue.d.ts +68 -71
  28. package/types/lib-components/forms/Select.vue.d.ts +55 -39
  29. package/types/lib-components/forms/TextArea.vue.d.ts +50 -32
  30. package/types/lib-components/forms/Toggle.vue.d.ts +27 -32
  31. package/types/lib-components/forms/YesOrNoRadio.vue.d.ts +50 -32
  32. package/types/lib-components/indicators/XYSpinner.vue.d.ts +1 -1
  33. package/types/lib-components/layout/DateFilter.vue.d.ts +28 -20
  34. package/types/lib-components/layout/SidebarLayout.vue.d.ts +38 -32
  35. package/types/lib-components/layout/StackedLayout.vue.d.ts +45 -33
  36. package/types/lib-components/lists/Cards.vue.d.ts +14 -17
  37. package/types/lib-components/lists/DataTable.vue.d.ts +31 -31
  38. package/types/lib-components/lists/DetailList.vue.d.ts +38 -34
  39. package/types/lib-components/lists/DownloadCell.vue.d.ts +18 -15
  40. package/types/lib-components/lists/DynamicTable.vue.d.ts +32 -32
  41. package/types/lib-components/lists/TableActionButtons.vue.d.ts +11 -23
  42. package/types/lib-components/navigation/ActionsDropdown.vue.d.ts +11 -23
  43. package/types/lib-components/navigation/Paginator.vue.d.ts +12 -15
  44. package/types/lib-components/navigation/Steps.vue.d.ts +54 -39
  45. package/types/lib-components/navigation/Tabs.vue.d.ts +34 -34
  46. package/types/lib-components/overlays/ContentModal.vue.d.ts +22 -28
  47. package/types/lib-components/overlays/Modal.vue.d.ts +47 -42
  48. package/types/lib-components/overlays/Popover/Popover.vue.d.ts +23 -31
  49. package/types/lib-components/overlays/Popover/PopoverContent.vue.d.ts +1 -1
  50. package/types/lib-components/overlays/Slideover.vue.d.ts +30 -22
  51. package/types/lib-components/overlays/Tooltip.vue.d.ts +20 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xy-planning-network/trees",
3
- "version": "0.7.5-dev",
3
+ "version": "0.7.5-rc2",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "repository": "github:xy-planning-network/trees",
@@ -52,7 +52,7 @@
52
52
  "tsc-alias": "^1.8.5",
53
53
  "typescript": "^5.0.4",
54
54
  "vite": "^4.3.9",
55
- "vue-tsc": "^1.2.0"
55
+ "vue-tsc": "^1.8.18"
56
56
  },
57
57
  "dependencies": {
58
58
  "@floating-ui/vue": "^1.0.1",
@@ -1,69 +1,107 @@
1
1
  <script setup lang="ts">
2
2
  import InputLabel from "./InputLabel.vue"
3
3
  import InputHelp from "./InputHelp.vue"
4
- import { useInputField } from "@/composables/forms"
4
+ import {
5
+ useInputField,
6
+ defaultInputProps,
7
+ emailPattern,
8
+ looseToNumber,
9
+ passwordPattern,
10
+ phonePattern,
11
+ } from "@/composables/forms"
12
+ import type { TextLikeInput } from "@/composables/forms"
13
+ import { computed, ref } from "vue"
5
14
 
6
15
  defineOptions({
7
16
  inheritAttrs: false,
8
17
  })
9
18
 
10
- type TextLikeInputs =
11
- | "date"
12
- | "datetime-local"
13
- | "email"
14
- | "month"
15
- | "number"
16
- | "password"
17
- | "search"
18
- | "tel"
19
- | "text"
20
- | "time"
21
- | "url"
22
- | "week"
19
+ const props = withDefaults(defineProps<TextLikeInput>(), defaultInputProps)
23
20
 
24
- withDefaults(
25
- defineProps<{
26
- type: TextLikeInputs
27
- help?: string
28
- label?: string
29
- modelValue?: string | number
30
- }>(),
31
- {
32
- help: "",
33
- label: "",
34
- modelValue: "",
21
+ defineEmits(["update:modelValue", "update:error"])
22
+ const targetInput = ref<HTMLInputElement | null>(null)
23
+ const {
24
+ errorState,
25
+ modelState,
26
+ inputID,
27
+ isRequired,
28
+ onInvalid,
29
+ inputValidation,
30
+ } = useInputField({ props, targetInput })
31
+
32
+ // A wrapper component may need to have direct access
33
+ // to the underlying HTMLInputElement that BaseInput binds to
34
+ // example: GoogleMaps Autocomplete inputs
35
+ defineExpose({ input: targetInput })
36
+
37
+ const typeAttributes = computed(() => {
38
+ switch (props.type) {
39
+ case "number":
40
+ return {
41
+ max: Number.MAX_SAFE_INTEGER,
42
+ min: Number.MIN_SAFE_INTEGER,
43
+ }
44
+ case "email":
45
+ return {
46
+ pattern: emailPattern,
47
+ }
48
+ case "password":
49
+ return {
50
+ pattern: passwordPattern,
51
+ }
52
+ case "tel":
53
+ return {
54
+ pattern: phonePattern,
55
+ }
56
+ default:
57
+ return {}
35
58
  }
36
- )
59
+ })
37
60
 
38
- const emit = defineEmits(["update:modelValue"])
39
- const { inputID, isValid } = useInputField()
61
+ const onInput = (e: Event) => {
62
+ let val = (e.target as HTMLInputElement).value
63
+
64
+ if (props.type === "number") {
65
+ val = looseToNumber(val)
66
+ }
67
+
68
+ modelState.value = val
69
+
70
+ inputValidation(e)
71
+ }
40
72
  </script>
41
73
 
42
74
  <template>
43
75
  <div>
44
- <div class="mb-1">
45
- <InputLabel :id="`${inputID}-label`" :for="inputID" :label="label" />
46
- </div>
76
+ <InputLabel
77
+ :id="`${inputID}-label`"
78
+ class="mb-2"
79
+ :for="inputID"
80
+ :label="label"
81
+ :required="isRequired"
82
+ />
47
83
  <input
48
84
  :id="inputID"
85
+ ref="targetInput"
49
86
  :aria-labelledby="label ? `${inputID}-label` : undefined"
50
87
  :aria-describedby="help ? `${inputID}-help` : undefined"
51
88
  :class="[
52
89
  'block w-full rounded-md border-0 py-2 shadow-sm ring-1 ring-inset focus:ring-2 sm:text-sm sm:leading-6',
53
- 'disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-600 disabled:ring-gray-200',
54
- isValid
55
- ? 'text-gray-800 ring-gray-300 placeholder:text-gray-400 focus:ring-xy-blue-500'
56
- : 'text-red-900 ring-red-700 placeholder:text-red-300 focus:ring-red-700',
90
+ 'disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-700 disabled:ring-gray-200',
91
+ errorState
92
+ ? 'text-red-900 ring-red-700 placeholder:text-red-300 focus:ring-red-700'
93
+ : 'text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-xy-blue-500',
57
94
  ]"
95
+ :placeholder="placeholder"
58
96
  :type="type"
59
- :value="modelValue"
60
- v-bind="$attrs"
61
- @input="
62
- emit('update:modelValue', ($event.target as HTMLInputElement).value)
63
- "
97
+ :value="modelState"
98
+ v-bind="{ ...typeAttributes, ...$attrs }"
99
+ @input="onInput"
100
+ @invalid="onInvalid"
64
101
  />
65
- <div class="mt-1">
66
- <InputHelp :id="`${inputID}-help`" :text="help" />
102
+ <InputHelp :id="`${inputID}-help`" class="mt-1" :text="help" />
103
+ <div v-if="errorState" class="mt-0.5">
104
+ <p class="text-sm text-red-700">{{ errorState }}</p>
67
105
  </div>
68
106
  </div>
69
107
  </template>
@@ -1,49 +1,55 @@
1
1
  <script setup lang="ts">
2
2
  import InputLabel from "./InputLabel.vue"
3
3
  import InputHelp from "./InputHelp.vue"
4
- import { useInputField } from "@/composables/forms"
4
+ import { useInputField, defaultInputProps } from "@/composables/forms"
5
+ import type { BooleanInput } from "@/composables/forms"
6
+ import { ref } from "vue"
5
7
 
6
8
  defineOptions({
7
9
  inheritAttrs: false,
8
10
  })
9
11
 
10
- withDefaults(
11
- defineProps<{
12
- label?: string
13
- help?: string
14
- modelValue: boolean
15
- }>(),
16
- {
17
- label: "",
18
- help: "",
19
- }
20
- )
21
- const emits = defineEmits(["update:modelValue"])
22
- const { inputID, isDisabled, isValid } = useInputField()
12
+ const props = withDefaults(defineProps<BooleanInput>(), defaultInputProps)
13
+
14
+ defineEmits(["update:modelValue", "update:error"])
15
+ const targetInput = ref<HTMLInputElement | null>(null)
16
+ const {
17
+ inputID,
18
+ isDisabled,
19
+ isRequired,
20
+ errorState,
21
+ modelState,
22
+ validate,
23
+ onInvalid,
24
+ } = useInputField({ props, targetInput })
25
+
26
+ const onChange = (e: Event) => {
27
+ modelState.value = (e.target as HTMLInputElement).checked
28
+ validate(e)
29
+ }
23
30
  </script>
24
31
 
25
32
  <template>
26
33
  <div class="relative flex items-start">
27
- <div class="flex items-center h-6">
34
+ <div class="flex items-center h-5">
28
35
  <input
29
36
  :id="inputID"
37
+ ref="targetInput"
30
38
  :aria-labelledby="label ? `${inputID}-label` : undefined"
31
39
  :aria-describedby="help ? `${inputID}-help` : undefined"
32
- :checked="modelValue"
40
+ :checked="modelState || undefined"
33
41
  :class="[
34
- 'h-4 w-4 rounded',
35
- 'border-gray-300 text-xy-blue',
42
+ 'h-4 w-4 rounded text-xy-blue cursor-pointer',
36
43
  'disabled:bg-gray-100 disabled:border-gray-200 disabled:cursor-not-allowed disabled:opacity-100',
37
44
  'checked:disabled:bg-xy-blue checked:disabled:border-xy-blue checked:disabled:opacity-50',
38
- isValid ? 'focus:ring-xy-blue-500' : 'focus:ring-red-700',
45
+ errorState
46
+ ? 'border-red-700 focus:ring-red-700'
47
+ : 'border-gray-300 focus:ring-xy-blue-500',
39
48
  ]"
40
49
  type="checkbox"
41
- v-bind="{
42
- onChange: ($event) => {
43
- emits('update:modelValue', ($event.target as HTMLInputElement).checked)
44
- },
45
- ...$attrs,
46
- }"
50
+ v-bind="$attrs"
51
+ @change="onChange"
52
+ @invalid="onInvalid"
47
53
  />
48
54
  </div>
49
55
  <div class="ml-3">
@@ -51,9 +57,13 @@ const { inputID, isDisabled, isValid } = useInputField()
51
57
  :id="`${inputID}-label`"
52
58
  :for="inputID"
53
59
  :label="label"
54
- :class="isDisabled && 'cursor-not-allowed'"
60
+ :class="isDisabled ? 'cursor-not-allowed' : 'cursor-pointer'"
61
+ :required="isRequired"
55
62
  />
56
63
  <InputHelp :id="`${inputID}-help`" :text="help"></InputHelp>
64
+ <div v-if="errorState" class="mt-0.5">
65
+ <p class="text-sm text-red-700">{{ errorState }}</p>
66
+ </div>
57
67
  </div>
58
68
  </div>
59
69
  </template>
@@ -1,42 +1,38 @@
1
1
  <script setup lang="ts">
2
2
  import flatpickr from "flatpickr"
3
3
  import "flatpickr/dist/flatpickr.min.css"
4
- import { onMounted } from "vue"
5
- import BaseInput from "./BaseInput.vue"
6
- import { useInputField } from "@/composables/forms"
4
+ import { onMounted, ref } from "vue"
5
+ import { defaultInputProps, useInputField } from "@/composables/forms"
6
+ import type { DateRangeInput } from "@/composables/forms"
7
7
 
8
8
  defineOptions({
9
9
  inheritAttrs: false,
10
10
  })
11
11
 
12
- const props = withDefaults(
13
- defineProps<{
14
- modelValue: {
15
- minDate: number
16
- maxDate: number
12
+ const props = withDefaults(defineProps<DateRangeInput>(), {
13
+ ...defaultInputProps,
14
+ maxRange: 0,
15
+ modelValue: () => {
16
+ return {
17
+ maxDate: 0,
18
+ minDate: 0,
17
19
  }
18
- maxRange?: number
19
- startDate?: number
20
- label?: string
21
- help?: string
22
- }>(),
23
- {
24
- maxRange: 0,
25
- startDate: 0,
26
- label: "",
27
- help: "",
28
- }
29
- )
20
+ },
21
+ placeholder: "mm-dd-yyyy range",
22
+ startDate: 0,
23
+ })
30
24
 
31
- const emits = defineEmits(["update:modelValue"])
32
- const { inputID } = useInputField()
25
+ const targetInput = ref<HTMLInputElement | null>(null)
26
+ const { errorState, modelState, inputID, isRequired, onInvalid, validate } =
27
+ useInputField({ props, targetInput })
33
28
 
34
29
  const updateModelValue = (value: { minDate: number; maxDate: number }) => {
35
- emits("update:modelValue", value)
30
+ modelState.value = value
36
31
  }
37
32
 
38
33
  onMounted(() => {
39
34
  const opts: flatpickr.Options.Options = {
35
+ allowInput: true,
40
36
  dateFormat: "m-d-Y",
41
37
  mode: "range",
42
38
  maxDate: new Date(), // So far, we cannot have options past today for ranges
@@ -92,11 +88,35 @@ onMounted(() => {
92
88
  </script>
93
89
 
94
90
  <template>
95
- <BaseInput
96
- :id="inputID"
97
- type="text"
98
- placeholder="mm-dd-yyyy range"
99
- :label="label"
100
- :help="help"
101
- />
91
+ <div>
92
+ <InputLabel
93
+ :id="`${inputID}-label`"
94
+ class="mb-2"
95
+ :for="inputID"
96
+ :label="label"
97
+ :required="isRequired"
98
+ />
99
+ <input
100
+ :id="inputID"
101
+ ref="targetInput"
102
+ :aria-labelledby="label ? `${inputID}-label` : undefined"
103
+ :aria-describedby="help ? `${inputID}-help` : undefined"
104
+ :class="[
105
+ 'block w-full rounded-md border-0 py-2 shadow-sm ring-1 ring-inset focus:ring-2 sm:text-sm sm:leading-6',
106
+ 'disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-700 disabled:ring-gray-200',
107
+ errorState
108
+ ? 'text-red-900 ring-red-700 placeholder:text-red-300 focus:ring-red-700'
109
+ : 'text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-xy-blue-500',
110
+ ]"
111
+ :placeholder="placeholder"
112
+ v-bind="$attrs"
113
+ type="text"
114
+ @input="validate"
115
+ @invalid="onInvalid"
116
+ />
117
+ <InputHelp :id="`${inputID}-help`" class="mt-1" :text="help" />
118
+ <div v-if="errorState" class="mt-0.5">
119
+ <p class="text-sm text-red-700">{{ errorState }}</p>
120
+ </div>
121
+ </div>
102
122
  </template>
@@ -1,23 +1,43 @@
1
1
  <script setup lang="ts">
2
- withDefaults(
2
+ import { computed } from "vue"
3
+
4
+ const props = withDefaults(
3
5
  defineProps<{
4
6
  label?: string
7
+ required?: boolean
5
8
  tag?: string
6
9
  }>(),
7
10
  {
8
11
  label: "",
12
+ required: false,
9
13
  tag: "legend",
10
14
  }
11
15
  )
16
+
17
+ const labelDisplay = computed((): string => {
18
+ if (!props.required) {
19
+ return props.label
20
+ }
21
+
22
+ // remove trailing * on the existing label
23
+ const regex = /\*$/gm
24
+
25
+ if (regex.exec(props.label) !== null) {
26
+ return props.label.substring(0, props.label.length - 1)
27
+ }
28
+
29
+ return props.label
30
+ })
12
31
  </script>
13
32
 
14
33
  <template>
15
34
  <component
16
35
  :is="tag"
17
36
  v-if="label"
18
- class="block text-base/6 font-medium text-gray-800"
37
+ class="block text-base leading-snug font-medium text-gray-800"
19
38
  v-bind="$attrs"
20
39
  >
21
- {{ label }}
40
+ {{ labelDisplay }}
41
+ <span v-if="props.required" class="text-red-700/80">*</span>
22
42
  </component>
23
43
  </template>
@@ -14,7 +14,7 @@ withDefaults(
14
14
  <component
15
15
  :is="tag"
16
16
  v-if="text"
17
- class="text-sm leading-6 font-normal text-gray-600"
17
+ class="text-sm leading-snug font-normal text-gray-600"
18
18
  v-bind="$attrs"
19
19
  >
20
20
  {{ text }}
@@ -1,23 +1,43 @@
1
1
  <script setup lang="ts">
2
- withDefaults(
2
+ import { computed } from "vue"
3
+
4
+ const props = withDefaults(
3
5
  defineProps<{
4
6
  label?: string
7
+ required?: boolean
5
8
  tag?: string
6
9
  }>(),
7
10
  {
8
11
  label: "",
12
+ required: false,
9
13
  tag: "label",
10
14
  }
11
15
  )
16
+
17
+ const labelDisplay = computed((): string => {
18
+ if (!props.required) {
19
+ return props.label
20
+ }
21
+
22
+ // remove trailing * on the existing label
23
+ const regex = /\*$/gm
24
+
25
+ if (regex.exec(props.label) !== null) {
26
+ return props.label.substring(0, props.label.length - 1)
27
+ }
28
+
29
+ return props.label
30
+ })
12
31
  </script>
13
32
 
14
33
  <template>
15
34
  <component
16
35
  :is="tag"
17
- v-if="label"
18
- class="block text-sm/6 font-medium text-gray-800"
36
+ v-if="labelDisplay"
37
+ class="block text-sm leading-snug font-medium text-gray-800"
19
38
  v-bind="$attrs"
20
39
  >
21
- {{ label }}
40
+ {{ labelDisplay }}
41
+ <span v-if="props.required" class="text-red-700/80">*</span>
22
42
  </component>
23
43
  </template>
@@ -2,68 +2,117 @@
2
2
  import FieldsetLegend from "./FieldsetLegend.vue"
3
3
  import InputLabel from "./InputLabel.vue"
4
4
  import InputHelp from "./InputHelp.vue"
5
- import { useInputField } from "@/composables/forms"
5
+ import { useInputField, defaultInputProps } from "@/composables/forms"
6
+ import type { MultiChoiceInput, ColumnedInput } from "@/composables/forms"
7
+ import { computed, ref } from "vue"
6
8
 
7
9
  defineOptions({
8
10
  inheritAttrs: false,
9
11
  })
10
12
 
11
- type CheckboxValue = string | number
12
- type ModelValue = CheckboxValue[]
13
-
14
13
  const props = withDefaults(
15
- defineProps<{
16
- options: {
17
- disabled?: boolean
18
- help?: string
19
- label: string
20
- value: CheckboxValue
21
- }[]
22
- help?: string
23
- label?: string
24
- modelValue: ModelValue
25
- columns?: 2 | 3
26
- }>(),
27
- {
28
- help: "",
29
- label: "",
30
- columns: undefined,
31
- }
14
+ defineProps<MultiChoiceInput & ColumnedInput>(),
15
+ defaultInputProps
32
16
  )
33
17
 
34
- const emit = defineEmits<{
35
- (e: "update:modelValue", modelValue: ModelValue): void
36
- }>()
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
+ )
37
23
 
38
- const { inputID, isDisabled } = useInputField()
24
+ const onChange = (e: Event, val: string | number) => {
25
+ const checked = (e.target as HTMLInputElement).checked
39
26
 
40
- const onChange = (checked: boolean, val: CheckboxValue) => {
41
- let updateModelValue = [...props.modelValue]
27
+ if (!Array.isArray(modelState.value)) {
28
+ modelState.value = []
29
+ }
42
30
 
43
31
  if (checked) {
44
- updateModelValue.push(val)
32
+ modelState.value.push(val)
45
33
  } else {
46
- updateModelValue.splice(updateModelValue.indexOf(val), 1)
34
+ modelState.value.splice(modelState.value.indexOf(val), 1)
47
35
  }
48
36
 
49
- emit("update:modelValue", updateModelValue)
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
+ }.`
72
+ }
73
+
74
+ return ""
75
+ })
76
+
77
+ const setValidationError = () => {
78
+ if (!errorState.value) {
79
+ errorState.value = countError.value
80
+ }
50
81
  }
51
82
  </script>
52
83
 
53
84
  <template>
54
85
  <fieldset
55
- class="space-y-5"
86
+ class="relative space-y-4"
56
87
  :aria-labelledby="label ? `${inputID}-legend` : undefined"
57
88
  :aria-describedby="help ? `${inputID}-help` : undefined"
58
89
  >
59
90
  <div v-if="label">
60
- <FieldsetLegend :id="`${inputID}-legend`" :label="label" />
91
+ <FieldsetLegend
92
+ :id="`${inputID}-legend`"
93
+ :label="label"
94
+ :required="minCount > 0"
95
+ />
61
96
  <InputHelp v-if="help" :id="`${inputID}-help`" tag="p" :text="help" />
62
97
  </div>
63
98
 
99
+ <div v-if="errorState" class="mt-0.5">
100
+ <p class="text-sm text-red-700">{{ errorState }}</p>
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
+
64
113
  <div class="flex">
65
114
  <div
66
- class="grid gap-y-5"
115
+ class="grid gap-y-6"
67
116
  :class="{
68
117
  'sm:grid sm:gap-x-5 sm:space-y-0': columns !== undefined,
69
118
  'sm:grid-cols-2': columns === 2,
@@ -75,28 +124,26 @@ const onChange = (checked: boolean, val: CheckboxValue) => {
75
124
  :key="option.value"
76
125
  class="flex items-start"
77
126
  >
78
- <div class="flex items-center h-6">
127
+ <div class="flex items-center h-5">
79
128
  <input
80
129
  :id="`${inputID}-${index}`"
81
130
  :aria-labelledby="`${inputID}-${index}-label`"
82
131
  :aria-describedby="
83
132
  option.help ? `${inputID}-${index}-help` : undefined
84
133
  "
85
- :checked="modelValue.includes(option.value)"
134
+ :checked="modelValue?.includes(option.value)"
86
135
  :disabled="option.disabled"
87
136
  :class="[
88
- 'h-4 w-4 rounded',
89
- 'border-gray-300 text-xy-blue focus:ring-xy-blue-500',
137
+ 'h-4 w-4 rounded cursor-pointer',
90
138
  'disabled:bg-gray-100 disabled:border-gray-200 disabled:cursor-not-allowed disabled:opacity-100',
91
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',
92
143
  ]"
93
144
  type="checkbox"
94
- v-bind="{
95
- onChange: ($event) => {
96
- onChange(($event.target as HTMLInputElement).checked, option.value)
97
- },
98
- ...$attrs,
99
- }"
145
+ v-bind="$attrs"
146
+ @change="onChange($event, option.value)"
100
147
  />
101
148
  </div>
102
149
  <div class="ml-3">
@@ -104,7 +151,11 @@ const onChange = (checked: boolean, val: CheckboxValue) => {
104
151
  :id="`${inputID}-${index}-label`"
105
152
  :for="`${inputID}-${index}`"
106
153
  :label="option.label"
107
- :class="(isDisabled || option.disabled) && 'cursor-not-allowed'"
154
+ :class="
155
+ isDisabled || option.disabled
156
+ ? 'cursor-not-allowed'
157
+ : 'cursor-pointer'
158
+ "
108
159
  />
109
160
  <InputHelp :id="`${inputID}-${index}-help`" :text="option.help" />
110
161
  </div>