FieldToggleInput.vue 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { computed, nextTick, toRef, watch } from 'vue'
  4. import stopEvent from '#shared/utils/events.ts'
  5. import useValue from '../../composables/useValue.ts'
  6. import { getToggleClasses } from './initializeToggleClasses.ts'
  7. import type { FormFieldContext } from '../../types/field.ts'
  8. const props = defineProps<{
  9. context: FormFieldContext<{
  10. // TODO: need to be changed to "options", because otherwise core workflow can not handle this
  11. variants?: {
  12. true?: string
  13. false?: string
  14. }
  15. size?: 'medium' | 'small'
  16. }>
  17. }>()
  18. const context = toRef(props, 'context')
  19. const { localValue } = useValue(context)
  20. const variants = computed(() => props.context.variants || {})
  21. watch(
  22. () => props.context.variants,
  23. (variants) => {
  24. if (!variants) {
  25. console.warn(
  26. 'FieldToggleInput: variants prop is required, but not provided',
  27. )
  28. return
  29. }
  30. if (localValue.value === undefined) {
  31. const options = Object.keys(variants)
  32. if (options.length === 1) {
  33. nextTick(() => {
  34. localValue.value = options[0] === 'true'
  35. })
  36. }
  37. return
  38. }
  39. const valueString = localValue.value ? 'true' : 'false'
  40. // if current value is not removed from options, we don't need to reset it
  41. if (valueString in variants) return
  42. // current value was removed from options, so we need reset it
  43. // if other value exists, fallback to it, otherwise set to undefined
  44. const newValueString = localValue.value ? 'false' : 'true'
  45. const newValue = newValueString in variants ? !localValue.value : undefined
  46. localValue.value = newValue
  47. },
  48. { immediate: true },
  49. )
  50. const disabled = computed(() => {
  51. if (props.context.disabled) return true
  52. const nextValueString = localValue.value ? 'false' : 'true'
  53. // if can't select next value, disable the toggle
  54. return !(nextValueString in variants.value)
  55. })
  56. const updateLocalValue = (e: Event) => {
  57. stopEvent(e)
  58. if (disabled.value) return
  59. const newValue = localValue.value ? 'false' : 'true'
  60. if (newValue in variants.value) {
  61. localValue.value = newValue === 'true'
  62. }
  63. }
  64. const ariaChecked = computed(() => (localValue.value ? 'true' : 'false'))
  65. const buttonSizeClasses = computed(() => {
  66. if (context.value.size === 'small') return 'w-8 h-5'
  67. return 'w-10 h-6'
  68. })
  69. const knobSizeClasses = computed(() => {
  70. if (context.value.size === 'small') return 'w-[18px] h-[18px]'
  71. return 'w-[22px] h-[22px]'
  72. })
  73. const knobTranslateClasses = computed(() => {
  74. if (context.value.size === 'small')
  75. return 'ltr:translate-x-[13px] rtl:-translate-x-[13px]'
  76. return 'ltr:translate-x-[17px] rtl:-translate-x-[17px]'
  77. })
  78. const classMap = getToggleClasses()
  79. </script>
  80. <template>
  81. <button
  82. :id="context.id"
  83. type="button"
  84. role="switch"
  85. class="formkit-disabled:pointer-events-none relative inline-flex flex-shrink-0 cursor-pointer items-center rounded-full transition-colors duration-200 ease-in-out"
  86. :class="[
  87. context.classes.input,
  88. classMap.track,
  89. buttonSizeClasses,
  90. {
  91. [classMap.trackOn]: localValue,
  92. },
  93. ]"
  94. :aria-labelledby="`label-${context.id}`"
  95. :aria-disabled="disabled"
  96. :aria-checked="ariaChecked"
  97. :aria-describedby="context.describedBy"
  98. :tabindex="context.disabled ? '-1' : '0'"
  99. :v-bind="context.attrs"
  100. @click="updateLocalValue"
  101. @keydown.space="updateLocalValue"
  102. >
  103. <div
  104. class="pointer-events-none inline-block transform rounded-full transition duration-200 ease-in-out"
  105. :class="[
  106. classMap.knob,
  107. knobSizeClasses,
  108. {
  109. 'ltr:translate-x-px rtl:-translate-x-px': !localValue,
  110. [knobTranslateClasses]: localValue,
  111. },
  112. ]"
  113. ></div>
  114. </button>
  115. </template>