LayoutSidebar.vue 5.6 KB

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