CommonSectionCollapse.vue 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. <!-- Copyright (C) 2012-2024 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/composables/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 class="flex flex-col gap-1" :class="{ 'overflow-y-auto': scrollable }">
  54. <header
  55. v-if="!noHeader"
  56. :id="headerId"
  57. class="group flex cursor-default items-center justify-between text-stone-200 dark:text-neutral-500"
  58. :class="{
  59. 'cursor-pointer rounded-md focus-within:outline 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':
  60. !noCollapse,
  61. 'px-1 py-0.5': size === 'small',
  62. '-mx-1': size === 'small' && !noNegativeMargin,
  63. 'px-2 py-2.5': size === 'large',
  64. '-mx-2': size === 'large' && !noNegativeMargin,
  65. }"
  66. @click="!noCollapse && toggleCollapse()"
  67. @keydown.enter="!noCollapse && toggleCollapse()"
  68. >
  69. <slot name="title">
  70. <CommonLabel
  71. class="grow select-none text-current"
  72. :size="size"
  73. tag="h3"
  74. >
  75. {{ $t(title) }}
  76. </CommonLabel>
  77. </slot>
  78. <CollapseButton
  79. v-if="!noCollapse"
  80. :collapsed="isCollapsed"
  81. :owner-id="id"
  82. no-padded
  83. class="focus-visible:bg-transparent focus-visible:text-black group-hover:text-black group-hover:opacity-100 dark:focus-visible:text-white dark:group-hover:text-white"
  84. :class="{ 'opacity-100': isCollapsed }"
  85. orientation="vertical"
  86. @keydown.enter="toggleCollapse()"
  87. />
  88. </header>
  89. <Transition
  90. name="collapse"
  91. :duration="collapseDuration"
  92. @enter="collapseEnter"
  93. @after-enter="collapseAfterEnter"
  94. @leave="collapseLeave"
  95. >
  96. <div
  97. v-show="!isCollapsed || noHeader"
  98. :id="id"
  99. :class="{ 'overflow-y-auto': scrollable }"
  100. >
  101. <slot :header-id="headerId" />
  102. </div>
  103. </Transition>
  104. </div>
  105. </template>