CommonButtonGroup.vue 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { computed } from 'vue'
  4. import { type Props as IconProps } from '#shared/components/CommonIcon/CommonIcon.vue'
  5. import { useSessionStore } from '#shared/stores/session.ts'
  6. import type { CommonButtonOption } from './types.ts'
  7. export interface Props {
  8. modelValue?: string | number
  9. mode?: 'full' | 'compressed'
  10. controls?: string
  11. as?: 'tabs' | 'buttons'
  12. options: CommonButtonOption[]
  13. }
  14. const props = withDefaults(defineProps<Props>(), {
  15. mode: 'compressed',
  16. as: 'buttons',
  17. })
  18. const emit = defineEmits<{
  19. 'update:modelValue': [value?: string | number]
  20. }>()
  21. const session = useSessionStore()
  22. const filteredOptions = computed(() => {
  23. return props.options.filter(
  24. (option) =>
  25. !option.hidden &&
  26. (!option.permissions || session.hasPermission(option.permissions)),
  27. )
  28. })
  29. const getIconProps = (option: CommonButtonOption): IconProps => {
  30. if (!option.icon) return {} as IconProps
  31. if (typeof option.icon === 'string') {
  32. return { name: option.icon, size: 'small' }
  33. }
  34. return option.icon
  35. }
  36. const onButtonClick = (option: CommonButtonOption) => {
  37. if (option.disabled) return
  38. option.onAction?.()
  39. emit('update:modelValue', option.value)
  40. }
  41. const isTabs = computed(() => props.as === 'tabs')
  42. </script>
  43. <template>
  44. <div
  45. class="flex max-w-[100vw] shrink-0 gap-2 overflow-x-auto"
  46. :class="{ 'w-full': mode === 'full' }"
  47. :role="isTabs ? 'tablist' : undefined"
  48. >
  49. <Component
  50. :is="option.link ? 'CommonLink' : 'button'"
  51. v-for="option of filteredOptions"
  52. :key="option.label"
  53. :type="option.link ? undefined : 'button'"
  54. :role="isTabs ? 'tab' : undefined"
  55. :disabled="option.disabled"
  56. :link="option.link"
  57. :data-value="option.value"
  58. :class="[
  59. option.class,
  60. {
  61. 'opacity-50': option.disabled,
  62. '!bg-gray-200':
  63. option.selected ||
  64. (option.value != null && modelValue === option.value),
  65. 'flex-1 py-2': mode === 'full',
  66. 'py-1': mode === 'compressed',
  67. },
  68. ]"
  69. class="flex flex-col items-center justify-center gap-1 rounded-xl bg-gray-500 px-3 text-sm text-white"
  70. :aria-controls="isTabs ? controls || option.controls : undefined"
  71. :aria-selected="isTabs ? modelValue === option.value : undefined"
  72. @click="onButtonClick(option)"
  73. >
  74. <CommonIcon v-if="option.icon" v-bind="getIconProps(option)" decorative />
  75. <span>{{ $t(option.label, ...(option.labelPlaceholder || [])) }}</span>
  76. </Component>
  77. </div>
  78. </template>