ChoiceChecklist.vue 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. <template>
  2. <div class="choice-checklist" :id="`${id}-wrapper`">
  3. <div class="choice-checklist-controls">
  4. <button type="button" class="choice-checklist-control" @click="selectAll">
  5. Select all
  6. </button>
  7. <button type="button" class="choice-checklist-control" @click="selectNone">
  8. Select none
  9. </button>
  10. </div>
  11. <fieldset class="choice-checklist-fieldset">
  12. <legend class="visually-hidden">{{ label || name }}</legend>
  13. <label
  14. v-for="(choice, index) in choices"
  15. :key="choice.value"
  16. class="choice-checklist-item"
  17. :for="`${id}-${index}`"
  18. >
  19. <input
  20. :id="`${id}-${index}`"
  21. type="checkbox"
  22. :checked="isSelected(choice.value)"
  23. @change="handleToggle(choice.value)"
  24. />
  25. <span>{{ choiceLabel(choice) }}</span>
  26. </label>
  27. </fieldset>
  28. <input
  29. :id="`${id}-value`"
  30. :name="name"
  31. type="text"
  32. class="visually-hidden choice-checklist-value"
  33. :value="modelValue"
  34. :required="required"
  35. tabindex="-1"
  36. aria-hidden="true"
  37. />
  38. </div>
  39. </template>
  40. <script setup>
  41. import { computed } from 'vue'
  42. import {
  43. allChoiceValues,
  44. choiceLabel,
  45. formatChecklistValue,
  46. parseChecklistValue,
  47. toggleChoice
  48. } from '../utils/choiceChecklistHelpers.js'
  49. const props = defineProps({
  50. id: {
  51. type: String,
  52. required: true
  53. },
  54. name: {
  55. type: String,
  56. required: true
  57. },
  58. label: {
  59. type: String,
  60. default: ''
  61. },
  62. choices: {
  63. type: Array,
  64. required: true
  65. },
  66. modelValue: {
  67. type: String,
  68. default: ''
  69. },
  70. required: {
  71. type: Boolean,
  72. default: false
  73. }
  74. })
  75. const emit = defineEmits(['update:modelValue'])
  76. const selectedValues = computed(() => parseChecklistValue(props.modelValue))
  77. function isSelected(value) {
  78. return selectedValues.value.includes(value)
  79. }
  80. function emitSelection(selected) {
  81. emit('update:modelValue', formatChecklistValue(selected))
  82. }
  83. function handleToggle(value) {
  84. emitSelection(toggleChoice(selectedValues.value, value))
  85. }
  86. function selectAll() {
  87. emitSelection(allChoiceValues(props.choices))
  88. }
  89. function selectNone() {
  90. emitSelection([])
  91. }
  92. </script>
  93. <style scoped>
  94. .choice-checklist {
  95. display: flex;
  96. flex-direction: column;
  97. gap: 0.5em;
  98. }
  99. .choice-checklist-controls {
  100. display: flex;
  101. gap: 0.75em;
  102. }
  103. .choice-checklist-control {
  104. background: none;
  105. border: none;
  106. color: inherit;
  107. cursor: pointer;
  108. font: inherit;
  109. padding: 0;
  110. text-decoration: underline;
  111. }
  112. .choice-checklist-fieldset {
  113. border: none;
  114. display: grid;
  115. gap: 0.5em 1em;
  116. grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
  117. margin: 0;
  118. padding: 0;
  119. }
  120. .choice-checklist-item {
  121. align-items: center;
  122. display: flex;
  123. gap: 0.4em;
  124. margin: 0;
  125. }
  126. .choice-checklist-item input[type="checkbox"] {
  127. margin: 0;
  128. }
  129. .visually-hidden {
  130. border: 0;
  131. clip: rect(0 0 0 0);
  132. height: 1px;
  133. margin: -1px;
  134. overflow: hidden;
  135. padding: 0;
  136. position: absolute;
  137. white-space: nowrap;
  138. width: 1px;
  139. }
  140. </style>