LayoutSidebar.vue 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { useActiveElement } from '@vueuse/core'
  4. import { computed, ref, watch } from 'vue'
  5. import CollapseButton from '#desktop/components/CollapseButton/CollapseButton.vue'
  6. import { useCollapseHandler } from '#desktop/components/CollapseButton/composables/useCollapseHandler.ts'
  7. import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
  8. import ResizeLine from '#desktop/components/ResizeLine/ResizeLine.vue'
  9. import { useResizeLine } from '#desktop/components/ResizeLine/useResizeLine.ts'
  10. import { SidebarPosition } from './types.ts'
  11. interface Props {
  12. name: string
  13. /**
  14. @property currentWidth
  15. @property minWidth
  16. @property maxWidth
  17. - used for accessibility
  18. / */
  19. currentWidth?: number | string
  20. minWidth?: number | string
  21. maxWidth?: number | string
  22. noScroll?: boolean
  23. collapsible?: boolean
  24. iconCollapsed?: string
  25. position?: SidebarPosition
  26. resizable?: boolean
  27. id: string
  28. noPadding?: boolean
  29. }
  30. const props = withDefaults(defineProps<Props>(), {
  31. position: SidebarPosition.Start,
  32. hideButtonWhenCollapsed: false,
  33. })
  34. const emit = defineEmits<{
  35. 'resize-horizontal': [number]
  36. 'resize-horizontal-start': []
  37. 'resize-horizontal-end': []
  38. 'reset-width': []
  39. collapse: [boolean]
  40. expand: [boolean]
  41. }>()
  42. const { toggleCollapse, isCollapsed } = useCollapseHandler(emit, {
  43. storageKey: `${props.name}-sidebar-collapsed`,
  44. })
  45. // a11y keyboard navigation
  46. const resizeLineComponent = ref<InstanceType<typeof ResizeLine>>()
  47. const activeElement = useActiveElement()
  48. const handleKeyStroke = (e: KeyboardEvent, adjustment: number) => {
  49. if (
  50. !props.currentWidth ||
  51. activeElement.value !== resizeLineComponent.value?.resizeLine
  52. )
  53. return
  54. e.preventDefault()
  55. const newWidth = Number(props.currentWidth) + adjustment
  56. emit('resize-horizontal', newWidth)
  57. }
  58. const { startResizing, isResizing } = useResizeLine(
  59. (positionX) => emit('resize-horizontal', positionX),
  60. resizeLineComponent.value?.resizeLine,
  61. handleKeyStroke,
  62. {
  63. calculateFromRight: props.position === SidebarPosition.End,
  64. orientation: 'vertical',
  65. },
  66. )
  67. watch(isResizing, (isResizing) => {
  68. if (isResizing) {
  69. emit('resize-horizontal-start')
  70. } else {
  71. emit('resize-horizontal-end')
  72. }
  73. })
  74. const collapseButtonClass = computed(() => {
  75. if (props.position === SidebarPosition.Start)
  76. return 'ltr:rounded-l-none rtl:rounded-r-none'
  77. if (props.position === SidebarPosition.End)
  78. return 'ltr:rounded-r-none rtl:rounded-l-none'
  79. return ''
  80. })
  81. </script>
  82. <template>
  83. <aside
  84. :id="props.id"
  85. class="-:bg-neutral-950 -:max-h-screen relative flex flex-col border-neutral-100 dark:border-gray-900"
  86. :class="{
  87. 'py-3': isCollapsed && !noPadding,
  88. 'border-e': position === SidebarPosition.Start,
  89. 'border-s': position === SidebarPosition.End,
  90. }"
  91. >
  92. <CommonButton
  93. v-if="iconCollapsed && isCollapsed"
  94. class="mx-auto"
  95. size="medium"
  96. data-test-id="action-button"
  97. variant="neutral"
  98. :icon="iconCollapsed"
  99. @click="toggleCollapse"
  100. />
  101. <div
  102. v-else
  103. class="flex h-full max-w-full flex-col overflow-x-hidden"
  104. :class="{
  105. 'p-3': !isCollapsed && !noPadding,
  106. 'overflow-y-hidden': noScroll,
  107. 'overflow-y-auto': !noScroll,
  108. }"
  109. >
  110. <slot v-bind="{ isCollapsed, toggleCollapse }" />
  111. </div>
  112. <ResizeLine
  113. v-if="resizable"
  114. ref="resizeLineComponent"
  115. :label="$t('Resize sidebar')"
  116. class="resize-line absolute z-20 has-[+div:hover]:opacity-100"
  117. :class="{
  118. 'ltr:right-0 ltr:translate-x-1/2 rtl:left-0 rtl:-translate-x-1/2':
  119. position === SidebarPosition.Start,
  120. 'ltr:left-0 ltr:-translate-x-1/2 rtl:right-0 rtl:translate-x-1/2':
  121. position === SidebarPosition.End,
  122. peer: !resizeLineComponent?.resizing,
  123. }"
  124. :values="{
  125. max: Number(maxWidth)?.toFixed(2),
  126. min: minWidth,
  127. current: currentWidth,
  128. }"
  129. :disabled="isCollapsed"
  130. @mousedown-event="startResizing"
  131. @touchstart-event="startResizing"
  132. @dblclick-event="$emit('reset-width')"
  133. />
  134. <CollapseButton
  135. v-if="collapsible"
  136. :collapsed="isCollapsed"
  137. :owner-id="id"
  138. class="absolute top-[49px] z-30 peer-hover:opacity-100"
  139. :inverse="position === SidebarPosition.End"
  140. variant="tertiary-gray"
  141. :collapse-label="$t('Collapse sidebar')"
  142. :expand-label="$t('Expand sidebar')"
  143. :class="{
  144. 'ltr:right-0 ltr:translate-x-[calc(100%-10px)] rtl:left-0 rtl:-translate-x-[calc(100%-10px)]':
  145. position === SidebarPosition.Start,
  146. 'ltr:left-0 ltr:-translate-x-[calc(100%-10px)] rtl:right-0 rtl:translate-x-[calc(100%-10px)]':
  147. position === SidebarPosition.End,
  148. 'ltr:translate-x-[calc(100%-7.5px)] rtl:-translate-x-[calc(100%-7.5px)]':
  149. isCollapsed && position === SidebarPosition.Start,
  150. 'ltr:-translate-x-[calc(100%-7.5px)] rtl:translate-x-[calc(100%-7.5px)]':
  151. isCollapsed && position === SidebarPosition.End,
  152. }"
  153. :button-class="collapseButtonClass"
  154. @click="(node: MouseEvent) => (node.target as HTMLButtonElement)?.blur()"
  155. @toggle-collapse="toggleCollapse"
  156. />
  157. </aside>
  158. </template>