CommonSectionCollapse.vue 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. <!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { computed, watch } from 'vue'
  4. import CollapseButton from '#desktop/components/CollapseButton/CollapseButton.vue'
  5. import { useCollapseHandler } from '#desktop/components/CollapseButton/useCollapseHandler.ts'
  6. import { useTransitionCollapse } from '#desktop/composables/useTransitionCollapse.ts'
  7. export interface Props {
  8. id: string
  9. title?: string
  10. size?: 'small' | 'large'
  11. noCollapse?: boolean
  12. noNegativeMargin?: boolean
  13. noHeader?: boolean
  14. scrollable?: boolean
  15. }
  16. const props = withDefaults(defineProps<Props>(), {
  17. size: 'small',
  18. })
  19. const modelValue = defineModel<boolean>({
  20. default: false,
  21. })
  22. const emit = defineEmits<{
  23. collapse: [boolean]
  24. expand: [boolean]
  25. }>()
  26. const headerId = computed(() => `${props.id}-header`)
  27. const { toggleCollapse, isCollapsed } = useCollapseHandler(emit)
  28. const { collapseDuration, collapseEnter, collapseAfterEnter, collapseLeave } =
  29. useTransitionCollapse()
  30. watch(
  31. modelValue,
  32. (newValue) => {
  33. if (isCollapsed.value === newValue) return
  34. isCollapsed.value = newValue
  35. },
  36. {
  37. immediate: true,
  38. },
  39. )
  40. watch(
  41. isCollapsed,
  42. (newValue) => {
  43. if (modelValue.value === newValue) return
  44. modelValue.value = newValue
  45. },
  46. {
  47. immediate: true,
  48. },
  49. )
  50. </script>
  51. <template>
  52. <!-- eslint-disable vuejs-accessibility/no-static-element-interactions-->
  53. <div
  54. class="flex flex-col gap-1"
  55. :class="{ 'overflow-y-auto outline-none': scrollable }"
  56. >
  57. <header
  58. v-if="!noHeader"
  59. :id="headerId"
  60. class="group flex cursor-default items-center justify-between text-stone-200 dark:text-neutral-500"
  61. :class="{
  62. 'cursor-pointer rounded-md focus-within:outline-1 focus-within:outline-offset-1 focus-within:outline-blue-800 hover:bg-blue-600 hover:text-black dark:hover:bg-blue-900 hover:dark:text-white':
  63. !noCollapse,
  64. 'px-1 py-0.5': size === 'small',
  65. '-mx-1': size === 'small' && !noNegativeMargin,
  66. 'px-2 py-2.5': size === 'large',
  67. '-mx-2': size === 'large' && !noNegativeMargin,
  68. }"
  69. @click="!noCollapse && toggleCollapse()"
  70. @keydown.enter="!noCollapse && toggleCollapse()"
  71. >
  72. <slot name="title">
  73. <CommonLabel
  74. class="grow text-current! select-none"
  75. :size="size"
  76. tag="h3"
  77. >
  78. {{ $t(title) }}
  79. </CommonLabel>
  80. </slot>
  81. <CollapseButton
  82. v-if="!noCollapse"
  83. :collapsed="isCollapsed"
  84. :owner-id="id"
  85. no-padded
  86. class="group-hover:text-black! group-hover:opacity-100 focus-visible:bg-transparent focus-visible:text-black dark:group-hover:text-white! dark:focus-visible:text-white"
  87. :class="{ 'opacity-100': isCollapsed }"
  88. orientation="vertical"
  89. @keydown.enter="toggleCollapse()"
  90. />
  91. </header>
  92. <Transition
  93. name="collapse"
  94. :duration="collapseDuration"
  95. @enter="collapseEnter"
  96. @after-enter="collapseAfterEnter"
  97. @leave="collapseLeave"
  98. >
  99. <div
  100. v-show="!isCollapsed || noHeader"
  101. :id="id"
  102. :data-test-id="id"
  103. :class="{ 'overflow-y-auto outline-none': scrollable }"
  104. >
  105. <slot :header-id="headerId" />
  106. </div>
  107. </Transition>
  108. </div>
  109. </template>