LayoutSidebar.vue 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  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 { useResizeWidthHandle } from '#desktop/components/ResizeHandle/composables/useResizeWidthHandle.ts'
  9. import ResizeHandle from '#desktop/components/ResizeHandle/ResizeHandle.vue'
  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
  20. minWidth?: number
  21. maxWidth?: number
  22. noScroll?: boolean
  23. collapsible?: boolean
  24. iconCollapsed?: string
  25. hideButtonWhenCollapsed?: boolean // Adjust it used as a default for all sidebars
  26. position?: SidebarPosition
  27. resizable?: boolean
  28. id: string
  29. noPadding?: boolean
  30. }
  31. const props = withDefaults(defineProps<Props>(), {
  32. position: SidebarPosition.Start,
  33. hideButtonWhenCollapsed: false,
  34. })
  35. const emit = defineEmits<{
  36. 'resize-horizontal': [number]
  37. 'resize-horizontal-start': []
  38. 'resize-horizontal-end': []
  39. 'reset-width': []
  40. collapse: [boolean]
  41. expand: [boolean]
  42. }>()
  43. const { toggleCollapse, isCollapsed } = useCollapseHandler(emit, {
  44. storageKey: `${props.name}-sidebar-collapsed`,
  45. })
  46. const showCollapseButtonOnCollapse = computed(() => {
  47. return props.hideButtonWhenCollapsed ? !isCollapsed.value : true
  48. })
  49. // a11y keyboard navigation
  50. const resizeHandleComponent = ref<InstanceType<typeof ResizeHandle>>()
  51. const activeElement = useActiveElement()
  52. const handleKeyStroke = (e: KeyboardEvent, adjustment: number) => {
  53. if (
  54. !props.currentWidth ||
  55. activeElement.value !== resizeHandleComponent.value?.$el
  56. )
  57. return
  58. e.preventDefault()
  59. const newWidth = props.currentWidth + adjustment
  60. if (
  61. props.maxWidth &&
  62. props.minWidth &&
  63. (newWidth >= props.maxWidth || newWidth <= props.minWidth)
  64. )
  65. return
  66. emit('resize-horizontal', newWidth)
  67. }
  68. const { startResizing, isResizingHorizontal } = useResizeWidthHandle(
  69. (positionX) => emit('resize-horizontal', positionX),
  70. resizeHandleComponent,
  71. handleKeyStroke,
  72. {
  73. calculateFromRight: props.position === SidebarPosition.End,
  74. },
  75. )
  76. watch(isResizingHorizontal, (isResizing) => {
  77. if (isResizing) {
  78. emit('resize-horizontal-start')
  79. } else {
  80. emit('resize-horizontal-end')
  81. }
  82. })
  83. </script>
  84. <template>
  85. <aside
  86. :id="props.id"
  87. class="group/sidebar -:bg-neutral-950 -:max-h-screen relative flex flex-col border-neutral-100 dark:border-gray-900"
  88. :class="{
  89. 'py-3': isCollapsed && !noPadding,
  90. 'border-e': position === SidebarPosition.Start,
  91. 'border-s': position === SidebarPosition.End,
  92. }"
  93. >
  94. <CollapseButton
  95. v-if="collapsible && showCollapseButtonOnCollapse"
  96. :is-collapsed="isCollapsed"
  97. :owner-id="id"
  98. group="sidebar"
  99. class="absolute top-[49px] z-20"
  100. :inverse="position === SidebarPosition.End"
  101. :class="{
  102. 'ltr:right-0 ltr:translate-x-1/2 rtl:left-0 rtl:-translate-x-1/2':
  103. position === SidebarPosition.Start,
  104. 'ltr:left-0 ltr:-translate-x-1/2 rtl:right-0 rtl:translate-x-1/2':
  105. position === SidebarPosition.End,
  106. }"
  107. @toggle-collapse="toggleCollapse"
  108. />
  109. <ResizeHandle
  110. v-if="resizable && !isCollapsed"
  111. ref="resizeHandleComponent"
  112. class="absolute top-1/2 -translate-y-1/2"
  113. :class="{
  114. 'ltr:right-0 rtl:left-0': position === SidebarPosition.Start,
  115. 'ltr:left-0 rtl:right-0': position === SidebarPosition.End,
  116. }"
  117. :aria-label="$t('Resize sidebar')"
  118. :aria-valuenow="currentWidth"
  119. :aria-valuemax="maxWidth?.toFixed(2)"
  120. :aria-valuemin="minWidth"
  121. role="separator"
  122. aria-orientation="horizontal"
  123. tabindex="0"
  124. @mousedown="startResizing"
  125. @touchstart="startResizing"
  126. @dblclick="$emit('reset-width')"
  127. />
  128. <CommonButton
  129. v-if="iconCollapsed && isCollapsed"
  130. class="mx-auto"
  131. size="medium"
  132. data-test-id="action-button"
  133. variant="neutral"
  134. :icon="iconCollapsed"
  135. @click="toggleCollapse"
  136. />
  137. <div
  138. v-else
  139. class="flex h-full flex-col"
  140. :class="{
  141. 'p-3': !isCollapsed && !noPadding,
  142. 'overflow-y-hidden': noScroll,
  143. 'overflow-y-auto': !noScroll,
  144. }"
  145. >
  146. <slot v-bind="{ isCollapsed, toggleCollapse }" />
  147. </div>
  148. </aside>
  149. </template>