LayoutSidebar.vue 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  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, useTemplateRef, 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 // TS: Does not infer type for some reason?
  46. const resizeLineInstance =
  47. useTemplateRef<InstanceType<typeof ResizeLine>>('resize-line')
  48. const activeElement = useActiveElement()
  49. const handleKeyStroke = (e: KeyboardEvent, adjustment: number) => {
  50. if (
  51. !props.currentWidth ||
  52. activeElement.value !== resizeLineInstance.value?.resizeLine
  53. )
  54. return
  55. e.preventDefault()
  56. const newWidth = Number(props.currentWidth) + adjustment
  57. emit('resize-horizontal', newWidth)
  58. }
  59. const { startResizing, isResizing } = useResizeLine(
  60. (positionX) => emit('resize-horizontal', positionX),
  61. resizeLineInstance.value?.resizeLine,
  62. handleKeyStroke,
  63. {
  64. calculateFromRight: props.position === SidebarPosition.End,
  65. orientation: 'vertical',
  66. },
  67. )
  68. watch(isResizing, (isResizing) => {
  69. if (isResizing) {
  70. emit('resize-horizontal-start')
  71. } else {
  72. emit('resize-horizontal-end')
  73. }
  74. })
  75. const collapseButtonClass = computed(() => {
  76. if (props.position === SidebarPosition.Start)
  77. return 'ltr:rounded-l-none rtl:rounded-r-none'
  78. if (props.position === SidebarPosition.End)
  79. return 'ltr:rounded-r-none rtl:rounded-l-none'
  80. return ''
  81. })
  82. </script>
  83. <template>
  84. <aside
  85. :id="props.id"
  86. class="-:bg-neutral-950 -:max-h-screen relative flex flex-col overflow-y-clip border-neutral-100 dark:border-gray-900"
  87. :class="{
  88. 'py-3': isCollapsed && !noPadding,
  89. 'border-e': position === SidebarPosition.Start,
  90. 'border-s': position === SidebarPosition.End,
  91. }"
  92. >
  93. <CommonButton
  94. v-if="iconCollapsed && isCollapsed"
  95. class="mx-auto"
  96. size="large"
  97. data-test-id="action-button"
  98. variant="neutral"
  99. :icon="iconCollapsed"
  100. @click="toggleCollapse"
  101. />
  102. <div
  103. v-else
  104. class="flex h-full max-w-full flex-col overflow-x-hidden"
  105. :class="{
  106. 'p-3': !isCollapsed && !noPadding,
  107. 'overflow-y-hidden': noScroll,
  108. 'overflow-y-auto': !noScroll,
  109. }"
  110. >
  111. <slot v-bind="{ isCollapsed, toggleCollapse }" />
  112. </div>
  113. <ResizeLine
  114. v-if="resizable"
  115. ref="resize-line"
  116. :label="$t('Resize sidebar')"
  117. class="absolute z-20 has-[+div:hover]:opacity-100"
  118. :class="{
  119. 'ltr:right-0 ltr:translate-x-1/2 rtl:left-0 rtl:-translate-x-1/2':
  120. position === SidebarPosition.Start,
  121. 'ltr:left-0 ltr:-translate-x-1/2 rtl:right-0 rtl:translate-x-1/2':
  122. position === SidebarPosition.End,
  123. peer: !resizeLineInstance?.resizing,
  124. }"
  125. :values="{
  126. max: Number(maxWidth)?.toFixed(2),
  127. min: minWidth,
  128. current: currentWidth,
  129. }"
  130. :disabled="isCollapsed"
  131. @mousedown-event="startResizing"
  132. @touchstart-event="startResizing"
  133. @dblclick-event="$emit('reset-width')"
  134. />
  135. <CollapseButton
  136. v-if="collapsible"
  137. :collapsed="isCollapsed"
  138. :owner-id="id"
  139. class="absolute top-[49px] z-30 peer-hover:opacity-100"
  140. :inverse="position === SidebarPosition.End"
  141. variant="tertiary-gray"
  142. :collapse-label="$t('Collapse sidebar')"
  143. :expand-label="$t('Expand sidebar')"
  144. :class="{
  145. 'ltr:right-0 ltr:translate-x-[calc(100%-10px)] rtl:left-0 rtl:-translate-x-[calc(100%-10px)]':
  146. position === SidebarPosition.Start,
  147. 'ltr:left-0 ltr:-translate-x-[calc(100%-10px)] rtl:right-0 rtl:translate-x-[calc(100%-10px)]':
  148. position === SidebarPosition.End,
  149. 'ltr:translate-x-[calc(100%-7.5px)] rtl:-translate-x-[calc(100%-7.5px)]':
  150. isCollapsed && position === SidebarPosition.Start,
  151. 'ltr:-translate-x-[calc(100%-7.5px)] rtl:translate-x-[calc(100%-7.5px)]':
  152. isCollapsed && position === SidebarPosition.End,
  153. }"
  154. :button-class="collapseButtonClass"
  155. @click="(node: MouseEvent) => (node.target as HTMLButtonElement)?.blur()"
  156. @toggle-collapse="toggleCollapse"
  157. />
  158. </aside>
  159. </template>