Browse Source

Maintenance: Desktop-View - Reimplement sidebar collapse/resizing mechanism according to the new design.

Benjamin Scharf 6 months ago
parent
commit
af839d3d30

+ 35 - 34
app/frontend/apps/desktop/components/CollapseButton/CollapseButton.vue

@@ -5,6 +5,7 @@ import { computed } from 'vue'
 
 import { useTouchDevice } from '#shared/composables/useTouchDevice.ts'
 import { EnumTextDirection } from '#shared/graphql/types.ts'
+import { i18n } from '#shared/i18n/index.ts'
 import { useLocaleStore } from '#shared/stores/locale.ts'
 
 import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
@@ -13,15 +14,19 @@ const { isTouchDevice } = useTouchDevice()
 
 interface Props {
   ownerId: string
-  isCollapsed?: boolean
-  group?: 'heading' | 'sidebar'
+  collapsed?: boolean
   orientation?: 'horizontal' | 'vertical'
+  expandLabel?: string
+  collapseLabel?: string
   inverse?: boolean
+  variant?: 'none' | 'tertiary-gray'
+  noPadded?: boolean
+  buttonClass?: string
 }
 
 const props = withDefaults(defineProps<Props>(), {
   orientation: 'horizontal',
-  isCollapsed: false,
+  collapsed: false,
 })
 
 defineEmits<{
@@ -32,55 +37,51 @@ const locale = useLocaleStore()
 
 const collapseButtonIcon = computed(() => {
   if (props.orientation === 'vertical')
-    return props.isCollapsed ? 'arrows-expand' : 'arrows-collapse'
+    return props.collapsed ? 'arrows-expand' : 'arrows-collapse'
 
   if (
     (props.inverse && locale.localeData?.dir !== EnumTextDirection.Rtl) ||
     (!props.inverse && locale.localeData?.dir === EnumTextDirection.Rtl)
   )
-    return props.isCollapsed ? 'arrow-bar-left' : 'arrow-bar-right'
+    return props.collapsed ? 'arrow-bar-left' : 'arrow-bar-right'
 
-  return props.isCollapsed ? 'arrow-bar-right' : 'arrow-bar-left'
+  return props.collapsed ? 'arrow-bar-right' : 'arrow-bar-left'
 })
 
-const parentGroupClass = computed(() => {
-  // Tailwindcss must be able to scan the class names to generate CSS
-  // https://tailwindcss.com/docs/content-configuration#dynamic-class-names
-  switch (props.group) {
-    case 'heading':
-      return 'group-hover/heading:opacity-100'
-    case 'sidebar':
-      return 'group-hover/sidebar:opacity-100'
-    default:
-      return ''
-  }
+// :TODO think if we add this variant as a Variant of CommonButton
+const variantClass = computed(() => {
+  if (props.variant === 'tertiary-gray')
+    return 'bg-neutral-500 focus-visible:bg-blue-800 active:dark:bg-blue-800 focus:dark:bg-blue-800 active:bg-blue-800 focus:bg-blue-800 hover:bg-blue-600 hover:dark:bg-blue-900 text-black dark:bg-gray-200 dark:text-white'
+
+  return ''
 })
+
+const labels = computed(() => ({
+  expand: props.expandLabel || i18n.t('Expand this element'),
+  collapse: props.collapseLabel || i18n.t('Collapse this element'),
+}))
 </script>
 
 <template>
-  <div>
+  <div
+    class="flex items-center justify-center focus-within:opacity-100 hover:opacity-100"
+    :class="{
+      'opacity-0': !isTouchDevice,
+      'p-2': !noPadded,
+    }"
+  >
     <CommonButton
-      v-tooltip="
-        props.isCollapsed
-          ? $t('Expand this element')
-          : $t('Collapse this element')
-      "
-      :class="[
-        { 'opacity-0 transition-opacity': !isTouchDevice && parentGroupClass },
-        'focus:opacity-100',
-        parentGroupClass,
-      ]"
-      class="collapse-button"
+      v-tooltip="collapsed ? labels.expand : labels.collapse"
+      class="hover:outline-transparent focus:outline-transparent focus-visible:outline-transparent dark:hover:outline-transparent dark:focus:outline-transparent"
+      :class="[variantClass, buttonClass]"
       :icon="collapseButtonIcon"
-      :aria-expanded="!props.isCollapsed"
+      :aria-expanded="!collapsed"
+      variant="none"
       :aria-controls="ownerId"
       :aria-label="
-        props.isCollapsed
-          ? $t('Expand this element')
-          : $t('Collapse this element')
+        collapsed ? $t('Expand this element') : $t('Collapse this element')
       "
       size="small"
-      variant="subtle"
       @click="$emit('toggle-collapse', $event)"
     />
   </div>

+ 70 - 27
app/frontend/apps/desktop/components/CollapseButton/__tests__/CollapseButton.spec.ts

@@ -10,60 +10,60 @@ import CollapseButton from '#desktop/components/CollapseButton/CollapseButton.vu
 describe('CollapseButton', () => {
   it.each([
     {
-      isCollapsed: true,
+      collapsed: true,
       orientation: 'horizontal',
       inverse: false,
       icon: 'arrow-bar-right',
     },
     {
-      isCollapsed: false,
+      collapsed: false,
       orientation: 'horizontal',
       inverse: false,
       icon: 'arrow-bar-left',
     },
     {
-      isCollapsed: true,
+      collapsed: true,
       orientation: 'vertical',
       inverse: false,
       icon: 'arrows-expand',
     },
     {
-      isCollapsed: false,
+      collapsed: false,
       orientation: 'vertical',
       inverse: false,
       icon: 'arrows-collapse',
     },
     {
-      isCollapsed: true,
+      collapsed: true,
       orientation: 'horizontal',
       inverse: true,
       icon: 'arrow-bar-left',
     },
     {
-      isCollapsed: false,
+      collapsed: false,
       orientation: 'horizontal',
       inverse: true,
       icon: 'arrow-bar-right',
     },
     {
-      isCollapsed: true,
+      collapsed: true,
       orientation: 'vertical',
       inverse: true,
       icon: 'arrows-expand',
     },
     {
-      isCollapsed: false,
+      collapsed: false,
       orientation: 'vertical',
       inverse: true,
       icon: 'arrows-collapse',
     },
   ])(
-    'displays correct LTR icon (isCollapsed: $isCollapsed, orientation: $orientation, inverse: $inverse)',
-    async ({ isCollapsed, orientation, inverse, icon }) => {
+    'displays correct LTR icon (collapsed: $collapsed, orientation: $orientation, inverse: $inverse)',
+    async ({ collapsed, orientation, inverse, icon }) => {
       const wrapper = renderComponent(CollapseButton, {
         props: {
           ownerId: 'test',
-          isCollapsed,
+          collapsed,
           inverse,
           orientation,
         },
@@ -75,56 +75,56 @@ describe('CollapseButton', () => {
 
   it.each([
     {
-      isCollapsed: true,
+      collapsed: true,
       orientation: 'horizontal',
       inverse: false,
       icon: 'arrow-bar-left',
     },
     {
-      isCollapsed: false,
+      collapsed: false,
       orientation: 'horizontal',
       inverse: false,
       icon: 'arrow-bar-right',
     },
     {
-      isCollapsed: true,
+      collapsed: true,
       orientation: 'vertical',
       inverse: false,
       icon: 'arrows-expand',
     },
     {
-      isCollapsed: false,
+      collapsed: false,
       orientation: 'vertical',
       inverse: false,
       icon: 'arrows-collapse',
     },
     {
-      isCollapsed: true,
+      collapsed: true,
       orientation: 'horizontal',
       inverse: true,
       icon: 'arrow-bar-right',
     },
     {
-      isCollapsed: false,
+      collapsed: false,
       orientation: 'horizontal',
       inverse: true,
       icon: 'arrow-bar-left',
     },
     {
-      isCollapsed: true,
+      collapsed: true,
       orientation: 'vertical',
       inverse: true,
       icon: 'arrows-expand',
     },
     {
-      isCollapsed: false,
+      collapsed: false,
       orientation: 'vertical',
       inverse: true,
       icon: 'arrows-collapse',
     },
   ])(
-    'displays correct RTL icon (isCollapsed: $isCollapsed, orientation: $orientation, inverse: $inverse)',
-    async ({ isCollapsed, orientation, inverse, icon }) => {
+    'displays correct RTL icon (collapsed: $collapsed, orientation: $orientation, inverse: $inverse)',
+    async ({ collapsed, orientation, inverse, icon }) => {
       const locale = useLocaleStore()
 
       locale.localeData = {
@@ -134,7 +134,7 @@ describe('CollapseButton', () => {
       const wrapper = renderComponent(CollapseButton, {
         props: {
           ownerId: 'test',
-          isCollapsed,
+          collapsed,
           inverse,
           orientation,
         },
@@ -148,7 +148,7 @@ describe('CollapseButton', () => {
     const wrapper = renderComponent(CollapseButton, {
       props: {
         ownerId: 'test',
-        isCollapsed: true,
+        collapsed: true,
       },
     })
 
@@ -171,15 +171,40 @@ describe('CollapseButton', () => {
     const wrapper = renderComponent(CollapseButton, {
       props: {
         ownerId: 'test',
-        group: 'sidebar',
       },
     })
-    expect(wrapper.getByRole('button')).toHaveClasses([
-      'transition-opacity',
+    expect(wrapper.getByRole('button').parentElement).toHaveClasses([
       'opacity-0',
     ])
   })
 
+  it.each(['tertiary-gray', 'none'])(
+    'renders variant %s correctly',
+    (variant) => {
+      const wrapper = renderComponent(CollapseButton, {
+        props: {
+          variant,
+          ownerId: 'test',
+        },
+      })
+      if (variant === 'tertiary-gray') {
+        expect(wrapper.getByRole('button')).toHaveClasses([
+          'focus-visible:bg-blue-800',
+          'active:dark:bg-blue-800',
+          'focus:dark:bg-blue-800',
+          'active:bg-blue-800',
+          'focus:bg-blue-800',
+          'hover:bg-blue-600',
+          'hover:dark:bg-blue-900',
+          'text-black',
+          'dark:bg-gray-200',
+          'dark:text-white',
+        ])
+      }
+      expect(wrapper.getByRole('button')).toHaveClasses([])
+    },
+  )
+
   it('shows always for touch devices', () => {
     // Impersonate a touch device by mocking the corresponding media query.
     Object.defineProperty(window, 'matchMedia', {
@@ -193,7 +218,6 @@ describe('CollapseButton', () => {
     const wrapper = renderComponent(CollapseButton, {
       props: {
         ownerId: 'test',
-        group: 'test',
       },
     })
 
@@ -203,4 +227,23 @@ describe('CollapseButton', () => {
       'group-hover/test:opacity-100',
     ])
   })
+
+  it('supports custom labels for expand and collapse', async () => {
+    const wrapper = renderComponent(CollapseButton, {
+      props: {
+        ownerId: 'test',
+        collapsed: true,
+        expandLabel: 'expand foo',
+        collapseLabel: 'collapse foo',
+      },
+    })
+
+    expect(wrapper.getByLabelText('expand foo')).toBeInTheDocument()
+
+    await wrapper.rerender({
+      collapsed: false,
+    })
+
+    expect(wrapper.getByLabelText('collapse foo')).toBeInTheDocument()
+  })
 })

+ 2 - 0
app/frontend/apps/desktop/components/CommonButton/CommonButton.vue

@@ -71,6 +71,8 @@ const variantClasses = computed(() => {
         'text-gray-100',
         'dark:text-neutral-400',
       ]
+    case 'none':
+      return []
     case 'secondary':
     default:
       return ['-:bg-transparent', '-:hover:bg-transparent', 'text-blue-800']

+ 18 - 17
app/frontend/apps/desktop/components/CommonFlyout/CommonFlyout.vue

@@ -23,8 +23,8 @@ import { getFirstFocusableElement } from '#shared/utils/getFocusableElements.ts'
 
 import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
 import CommonOverlayContainer from '#desktop/components/CommonOverlayContainer/CommonOverlayContainer.vue'
-import { useResizeWidthHandle } from '#desktop/components/ResizeHandle/composables/useResizeWidthHandle.ts'
-import ResizeHandle from '#desktop/components/ResizeHandle/ResizeHandle.vue'
+import { useResizeLine } from '#desktop/components/ResizeLine/composables/useResizeLine.ts'
+import ResizeLine from '#desktop/components/ResizeLine/ResizeLine.vue'
 
 import CommonFlyoutActionFooter from './CommonFlyoutActionFooter.vue'
 import { closeFlyout } from './useFlyout.ts'
@@ -127,7 +127,7 @@ if (props.persistResizeWidth) {
   flyoutContainerWidth = ref(flyoutSize[props.size || 'medium'])
 }
 
-const resizeHandleComponent = ref<InstanceType<typeof ResizeHandle>>()
+const resizeHandleComponent = ref<InstanceType<typeof ResizeLine>>()
 
 const resizeCallback = (valueX: number) => {
   if (valueX >= flyoutMaxWidth.value) return
@@ -140,7 +140,7 @@ const activeElement = useActiveElement()
 const handleKeyStroke = (e: KeyboardEvent, adjustment: number) => {
   if (
     !flyoutContainerWidth.value ||
-    activeElement.value !== resizeHandleComponent.value?.$el
+    activeElement.value !== resizeHandleComponent.value?.resizeLine
   )
     return
 
@@ -153,9 +153,9 @@ const handleKeyStroke = (e: KeyboardEvent, adjustment: number) => {
   resizeCallback(newWidth)
 }
 
-const { startResizing, isResizingHorizontal } = useResizeWidthHandle(
+const { startResizing, isResizingHorizontal } = useResizeLine(
   resizeCallback,
-  resizeHandleComponent,
+  resizeHandleComponent.value?.resizeLine,
   handleKeyStroke,
   {
     calculateFromRight: true,
@@ -302,19 +302,20 @@ onMounted(() => {
       </slot>
     </footer>
 
-    <ResizeHandle
+    <ResizeLine
       v-if="resizable"
       ref="resizeHandleComponent"
-      class="absolute top-1/2 -translate-y-1/2 ltr:left-0 rtl:right-0"
-      :aria-label="$t('Resize side panel')"
-      role="separator"
-      tabindex="0"
-      aria-orientation="horizontal"
-      :aria-valuenow="flyoutContainerWidth"
-      :aria-valuemax="flyoutMaxWidth"
-      @mousedown="startResizing"
-      @touchstart="startResizing"
-      @dblclick="resetWidth()"
+      :label="$t('Resize side panel')"
+      class="absolute top-[7px] h-[calc(100%-14px)] overflow-clip ltr:left-0 ltr:-translate-x-1/2 rtl:right-0 rtl:translate-x-1/2"
+      button-class="ltr:rounded-tl-sm rtl:rounded-tr-sm ltr:rounded-bl-sm rtl:rounded-br-sm"
+      orientation="vertical"
+      :values="{
+        current: flyoutContainerWidth,
+        max: flyoutMaxWidth,
+      }"
+      @mousedown-event="startResizing"
+      @touchstart-event="startResizing"
+      @dblclick-event="resetWidth()"
     />
   </CommonOverlayContainer>
 </template>

+ 2 - 2
app/frontend/apps/desktop/components/NavigationMenu/NavigationMenu.vue

@@ -104,13 +104,13 @@ const { collapseDuration, collapseEnter, collapseAfterEnter, collapseLeave } =
     <li
       v-for="category in categories"
       :key="category.label"
-      class="bg-neutral-00 relative z-0 mb-4"
+      class="bg-neutral-00 relative z-0 mb-1"
       :class="{ 'overflow-clip': collapsedCategories.has(category.label) }"
     >
       <template v-if="permittedEntries[category.label].length > 0">
         <NavigationMenuHeader
           :id="category.id"
-          class="mb-1 px-2"
+          class="mb-1"
           :collapsed="collapsedCategories.has(category.label)"
           :title="category.label"
           :icon="category.icon"

+ 8 - 11
app/frontend/apps/desktop/components/NavigationMenu/NavigationMenuHeader.vue

@@ -22,30 +22,27 @@ withDefaults(defineProps<Props>(), {
 <template>
   <!--  eslint-disable vuejs-accessibility/no-static-element-interactions-->
   <header
-    class="group/heading flex cursor-default justify-between px-0 text-base font-normal leading-snug text-stone-200 active:text-stone-200 dark:text-neutral-500 dark:active:text-neutral-500"
+    class="group flex cursor-default justify-between rounded-md px-2 py-2.5 text-base font-normal leading-snug text-stone-200 focus-within:text-black 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:text-neutral-500 dark:focus-within:text-white dark:hover:bg-blue-900 hover:dark:text-white"
     :class="{ 'cursor-pointer': collapsible }"
     @click="collapsible && $emit('toggle-collapsed', title)"
     @keydown.enter="collapsible && $emit('toggle-collapsed', title)"
   >
     <slot name="title">
-      <h4 class="grow text-base ltr:mr-auto rtl:ml-auto">
+      <h4
+        class="grow select-none text-base text-current ltr:mr-auto rtl:ml-auto"
+      >
         {{ $t(title) }}
       </h4>
     </slot>
+
     <CollapseButton
       v-if="collapsible"
-      :is-collapsed="collapsed"
+      :collapsed="collapsed"
       :owner-id="id"
-      group="heading"
-      class="mt-0.5 rtl:order-1"
+      no-padded
+      class="opacity-0 focus-visible:bg-transparent focus-visible:text-black group-hover:text-black group-hover:opacity-100 rtl:order-1 dark:focus-visible:text-white dark:group-hover:text-white"
       orientation="vertical"
       @keydown.enter="collapsible && $emit('toggle-collapsed', title)"
     />
   </header>
 </template>
-
-<style scoped>
-header:hover :deep(.collapse-button) {
-  @apply outline outline-1 outline-offset-1 outline-blue-600 dark:outline-blue-900;
-}
-</style>

+ 0 - 7
app/frontend/apps/desktop/components/ResizeHandle/ResizeHandle.vue

@@ -1,7 +0,0 @@
-<!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
-
-<template>
-  <div class="cursor-col-resize p-1">
-    <div class="h-12 w-0.5 rounded-sm bg-neutral-300 dark:bg-neutral-500" />
-  </div>
-</template>

+ 157 - 0
app/frontend/apps/desktop/components/ResizeLine/ResizeLine.vue

@@ -0,0 +1,157 @@
+<!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
+<script lang="ts" setup>
+import { useEventListener } from '@vueuse/core'
+import { computed, readonly, ref } from 'vue'
+
+import getUuid from '#shared/utils/getUuid.ts'
+
+interface Props {
+  label: string
+  /**
+   * horizontal line or vertical line
+   * */
+  orientation?: 'horizontal' | 'vertical'
+  values?: {
+    /**
+     * Maximum width/height in px value of what the container can be resized to
+     * */
+    max?: number | string
+    /**
+     * Minimum width/height in px value of what the container can be resized to
+     * */
+    min?: number | string
+    /**
+     * Current width/height in px value of the container
+     * */
+    current?: number | string
+  }
+  disabled?: boolean
+  buttonClass?: string
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  orientation: 'vertical',
+})
+
+const resizeLine = ref<HTMLDivElement>()
+const resizing = ref(false)
+
+const emit = defineEmits<{
+  'mousedown-event': [MouseEvent]
+  'touchstart-event': [TouchEvent]
+  'dblclick-event': [MouseEvent]
+}>()
+
+const resizeOrientation = computed(() =>
+  // Vertical resize line should have horizontal aria-orientation  -> container width
+  // Horizontal resize line should have vertical aria-orientation -> container height
+  props.orientation === 'horizontal' ? 'vertical' : 'horizontal',
+)
+
+const addRemoveResizingListener = (event: 'mouseup' | 'touchend') => {
+  useEventListener(
+    event,
+    () => {
+      resizing.value = false
+    },
+    { once: true },
+  )
+}
+
+const handleMousedown = (event: MouseEvent) => {
+  if (props.disabled) return
+
+  emit('mousedown-event', event)
+  resizing.value = true
+
+  addRemoveResizingListener('mouseup')
+}
+
+const handleTouchstart = (event: TouchEvent) => {
+  if (props.disabled) return
+
+  emit('touchstart-event', event)
+  resizing.value = true
+
+  addRemoveResizingListener('touchend')
+}
+
+const handleDoubleClick = (event: MouseEvent) => {
+  if (props.disabled) return
+
+  emit('dblclick-event', event)
+  resizeLine.value?.blur()
+}
+
+const id = getUuid()
+
+defineExpose({
+  resizeLine,
+  resizing: readonly(resizing),
+})
+</script>
+<template>
+  <div class="hover-area" :class="[`hover-area--${props.orientation}`]">
+    <button
+      ref="resizeLine"
+      v-tooltip="label"
+      :aria-describedby="id"
+      :disabled="disabled"
+      tabindex="0"
+      class="line"
+      :class="[
+        { '!bg-blue-800': resizing, 'cursor-col-resize': !disabled },
+        buttonClass,
+      ]"
+      @mousedown="handleMousedown"
+      @blur="resizing = false"
+      @touchstart="handleTouchstart"
+      @dblclick="handleDoubleClick"
+    />
+
+    <span
+      v-if="!disabled"
+      :id="id"
+      role="separator"
+      class="invisible absolute -z-20"
+      :aria-orientation="resizeOrientation"
+      :aria-valuenow="values?.current ?? undefined"
+      :aria-valuemax="values?.max ?? undefined"
+      :aria-valuemin="values?.min ?? undefined"
+    />
+  </div>
+</template>
+
+<style scoped>
+.line {
+  @apply focus:outline-none;
+
+  &:focus-within {
+    background-color: theme('colors.blue.800') !important;
+  }
+}
+
+.hover-area {
+  @apply flex justify-center opacity-0 focus-within:opacity-100 hover:opacity-100;
+
+  .line:not(:disabled) {
+    @apply bg-neutral-500 hover:bg-blue-600 dark:bg-gray-200 dark:hover:bg-blue-900;
+  }
+
+  &--horizontal {
+    @apply -:w-full h-[12px];
+
+    .line {
+      @apply h-1 w-full;
+    }
+  }
+
+  &--vertical {
+    @apply -:h-full w-[12px];
+
+    .line {
+      @apply h-full w-1;
+    }
+  }
+}
+</style>

+ 90 - 0
app/frontend/apps/desktop/components/ResizeLine/__tests__/ResizeLine.spec.ts

@@ -0,0 +1,90 @@
+// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+import { fireEvent } from '@testing-library/vue'
+import { expect } from 'vitest'
+
+import renderComponent from '#tests/support/components/renderComponent.ts'
+
+import ResizeLine from '#desktop/components/ResizeLine/ResizeLine.vue'
+
+describe('ResizeLine', () => {
+  it('does not emit events or display line background styling when disabled.', async () => {
+    const wrapper = renderComponent(ResizeLine, {
+      props: {
+        disabled: true,
+        label: 'test-label',
+      },
+    })
+
+    expect(wrapper.queryByRole('separator')).not.toBeInTheDocument()
+    expect(wrapper.getByRole('button')).toBeDisabled()
+
+    await wrapper.events.click(wrapper.getByRole('button'))
+    expect(wrapper.emitted('mousedown-event')).toBeUndefined()
+
+    await fireEvent.touch(wrapper.getByRole('button'))
+    expect(wrapper.emitted('touchstart-event')).toBeUndefined()
+
+    await wrapper.events.dblClick(wrapper.getByRole('button'))
+    expect(wrapper.emitted('dblclick-event')).toBeUndefined()
+  })
+
+  it('emits events and displays line background styling when enabled.', async () => {
+    const wrapper = renderComponent(ResizeLine, {
+      props: {
+        label: 'test-label',
+      },
+    })
+
+    await wrapper.events.click(wrapper.getByRole('button'))
+    expect(wrapper.emitted('mousedown-event')).toBeTruthy()
+
+    await fireEvent.touchStart(wrapper.getByRole('button'))
+    expect(wrapper.emitted('touchstart-event')).toBeTruthy()
+
+    await wrapper.events.dblClick(wrapper.getByRole('button'))
+    expect(wrapper.emitted('dblclick-event')).toBeTruthy()
+  })
+
+  it('has correct a11y labels', async () => {
+    const wrapper = renderComponent(ResizeLine, {
+      props: {
+        label: 'test-label',
+        values: {
+          max: 100,
+          min: 10,
+          current: 50,
+        },
+      },
+    })
+
+    expect(wrapper.getByRole('separator')).toHaveAttribute(
+      'aria-valuemax',
+      '100',
+    )
+    expect(wrapper.getByRole('separator')).toHaveAttribute(
+      'aria-valuemin',
+      '10',
+    )
+    expect(wrapper.getByRole('separator')).toHaveAttribute(
+      'aria-valuenow',
+      '50',
+    )
+
+    expect(wrapper.getByRole('separator')).toHaveAttribute(
+      'aria-orientation',
+      'horizontal',
+    )
+
+    expect(wrapper.getByLabelText('test-label')).toBeInTheDocument()
+
+    await wrapper.rerender({
+      orientation: 'horizontal',
+    })
+
+    expect(wrapper.getByRole('separator')).toHaveAttribute(
+      'aria-orientation',
+      'vertical',
+    )
+  })
+})

+ 6 - 6
app/frontend/apps/desktop/components/ResizeHandle/composables/useResizeWidthHandle.ts → app/frontend/apps/desktop/components/ResizeLine/composables/useResizeLine.ts

@@ -12,9 +12,9 @@ import { ref, onUnmounted, type Ref } from 'vue'
 import { EnumTextDirection } from '#shared/graphql/types.ts'
 import { useLocaleStore } from '#shared/stores/locale.ts'
 
-export const useResizeWidthHandle = (
+export const useResizeLine = (
   resizeCallback: (positionX: number) => void,
-  handleRef: MaybeComputedElementRef<MaybeElement>,
+  resizeLineElementRef: MaybeComputedElementRef<MaybeElement>,
   keyStrokeCallback: (e: KeyboardEvent, adjustment: number) => void,
   options?: {
     calculateFromRight?: boolean
@@ -24,7 +24,7 @@ export const useResizeWidthHandle = (
 
   const locale = useLocaleStore()
   const { width } = useElementBounding(
-    handleRef as MaybeComputedElementRef<MaybeElement>,
+    resizeLineElementRef as MaybeComputedElementRef<MaybeElement>,
   )
   const { width: screenWidth } = useWindowSize()
 
@@ -74,7 +74,7 @@ export const useResizeWidthHandle = (
     document.addEventListener('mousemove', resize)
   }
 
-  const startResizing = (e: MouseEvent) => {
+  const startResizing = (e: MouseEvent | TouchEvent) => {
     // Do not react on double click event.
     if (e.detail > 1) return
 
@@ -98,7 +98,7 @@ export const useResizeWidthHandle = (
         keyStrokeCallback(e, locale.localeData?.dir === 'rtl' ? 5 : -5)
       }
     },
-    { target: handleRef as Ref<EventTarget> },
+    { target: resizeLineElementRef as Ref<EventTarget> },
   )
 
   onKeyStroke(
@@ -110,7 +110,7 @@ export const useResizeWidthHandle = (
         keyStrokeCallback(e, locale.localeData?.dir === 'rtl' ? -5 : 5)
       }
     },
-    { target: handleRef as Ref<EventTarget> },
+    { target: resizeLineElementRef as Ref<EventTarget> },
   )
 
   return {

Some files were not shown because too many files changed in this diff