Browse Source

Fixes: Mobile - Add a space at the bottom for iOS PWA

Vladimir Sheremet 2 years ago
parent
commit
de38674ca9

+ 0 - 47
app/frontend/apps/mobile/pages/ticket/__tests__/ticket-detail-view.spec.ts

@@ -176,53 +176,6 @@ describe('user avatars', () => {
   })
 })
 
-test('can refresh data by pulling up', async () => {
-  const { waitUntilTicketLoaded } = mockTicketDetailViewGql()
-
-  const view = await visitView('/tickets/1')
-
-  await waitUntilTicketLoaded()
-
-  const articlesElement = view.getByRole('group', { name: 'Articles' })
-
-  const startEvent = new TouchEvent('touchstart', {
-    touches: [{ clientY: 300 } as Touch],
-  })
-
-  articlesElement.dispatchEvent(startEvent)
-
-  const moveEvent = new TouchEvent('touchmove', {
-    touches: [{ clientY: 100 } as Touch],
-  })
-
-  Object.defineProperty(document.documentElement, 'scrollHeight', {
-    value: 200,
-  })
-  Object.defineProperty(document.documentElement, 'scrollTop', {
-    value: 0,
-  })
-  Object.defineProperty(document.documentElement, 'clientHeight', {
-    value: 200,
-  })
-
-  articlesElement.dispatchEvent(moveEvent)
-
-  await flushPromises()
-
-  expect(view.getByIconName('mobile-arrow-down')).toHaveStyle({
-    transform: 'rotate(180deg)',
-  })
-
-  const touchEnd = new TouchEvent('touchend')
-  articlesElement.dispatchEvent(touchEnd)
-
-  await flushPromises()
-
-  expect(view.getAllByIconName('mobile-loading')).not.toHaveLength(0)
-
-  // TODO test api call
-})
-
 test("redirects to error page, if can't find ticket", async () => {
   const { calls } = mockGraphQLApi(TicketDocument).willFailWithNotFoundError(
     'The ticket 9866 could not be found',

+ 0 - 17
app/frontend/apps/mobile/pages/ticket/components/TicketDetailView/ArticlesList.vue

@@ -1,12 +1,10 @@
 <!-- Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/ -->
 
 <script setup lang="ts">
-// TODO scroll to bottom when data is loaded(?)
 import { toRef, shallowRef } from 'vue'
 import CommonSectionPopup from '@mobile/components/CommonSectionPopup/CommonSectionPopup.vue'
 import type { TicketArticle, TicketById } from '@shared/entities/ticket/types'
 import ArticleBubble from './ArticleBubble.vue'
-import ArticlesPullDown from './ArticlesPullDown.vue'
 import ArticleSeparatorNew from './ArticleSeparatorNew.vue'
 import ArticleSeparatorMore from './ArticleSeparatorMore.vue'
 import ArticleSeparatorDate from './ArticleSeparatorDate.vue'
@@ -30,16 +28,6 @@ const { contextOptions, articleContextShown, showArticleContext } =
   useTicketArticleContext()
 
 const articlesElement = shallowRef<HTMLElement>()
-const loaderElement = shallowRef<{
-  stopLoader(): void
-}>()
-
-const loadMoreArticles = () => {
-  // start loading instead
-  setTimeout(() => {
-    loaderElement.value?.stopLoader()
-  }, 1000)
-}
 
 const { rows } = useTicketArticleRows(
   toRef(props, 'articles'),
@@ -96,9 +84,4 @@ const filterAttachments = (article: TicketArticle) => {
     v-model:state="articleContextShown"
     :items="contextOptions"
   />
-  <ArticlesPullDown
-    ref="loaderElement"
-    :articles-element="articlesElement"
-    @load="loadMoreArticles"
-  />
 </template>

+ 0 - 127
app/frontend/apps/mobile/pages/ticket/components/TicketDetailView/ArticlesPullDown.vue

@@ -1,127 +0,0 @@
-<!-- Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/ -->
-
-<script setup lang="ts">
-import { useEventListener } from '@vueuse/core'
-import { ref, computed, watchEffect, toRef } from 'vue'
-
-interface Props {
-  articlesElement?: HTMLElement
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<{
-  (e: 'load'): void
-}>()
-
-const articlesElement = toRef(props, 'articlesElement')
-
-const isMoving = ref(false)
-const startPoint = ref(0)
-const differenceY = ref(0)
-const loaderShown = ref(false)
-const arrowShown = ref(false)
-
-const stopLoader = () => {
-  loaderShown.value = false
-  arrowShown.value = false
-}
-
-defineExpose({ stopLoader })
-
-// don't start moving untill the user has moved the mouse at least 10px
-const MIN_PULL_LENGTH = 10
-// start loading if the end position is at least 80px away from the start position
-const MAX_PULL_LENGTH = 80
-
-const rotateDegree = computed(() => {
-  return (differenceY.value / MAX_PULL_LENGTH) * 180
-})
-
-useEventListener(
-  articlesElement,
-  'touchstart',
-  (event: TouchEvent) => {
-    if (loaderShown.value) return
-    startPoint.value = event.touches[0].clientY
-    isMoving.value = true
-    differenceY.value = 0
-  },
-  {
-    // Make sure that the event listener is marked as passive, since `touchstart` is a high-frequency event.
-    //   https://developer.chrome.com/en/docs/lighthouse/best-practices/uses-passive-event-listeners/
-    passive: true,
-  },
-)
-useEventListener(articlesElement, 'touchend', () => {
-  if (differenceY.value === MAX_PULL_LENGTH) {
-    loaderShown.value = true
-    setTimeout(() => {
-      window.scrollTo({
-        behavior: 'smooth',
-        left: 0,
-        top: document.documentElement.scrollHeight,
-      })
-    })
-    emit('load')
-  }
-  isMoving.value = false
-  arrowShown.value = false
-  differenceY.value = 0
-  startPoint.value = 0
-})
-useEventListener(
-  articlesElement,
-  'touchmove',
-  async (event: TouchEvent) => {
-    const page = document.documentElement
-    const isBottom = page.scrollHeight - page.scrollTop <= page.clientHeight
-    if (isBottom) {
-      arrowShown.value = true
-      const difference = event.touches[0].clientY - startPoint.value
-      if (difference >= -MIN_PULL_LENGTH) {
-        differenceY.value = 0
-        return
-      }
-      differenceY.value = Math.min(Math.abs(difference), MAX_PULL_LENGTH)
-    }
-  },
-  {
-    // Make sure that the event listener is marked as passive, since `touchmove` is a high-frequency event.
-    //   https://developer.chrome.com/en/docs/lighthouse/best-practices/uses-passive-event-listeners/
-    passive: true,
-  },
-)
-
-watchEffect(() => {
-  const parent = articlesElement.value?.parentElement
-  if (parent) {
-    parent.style.transform = `translateY(-${differenceY.value}px)`
-    parent.style.transition = 'transform 0.4s'
-    // don't select text when pulling
-    if (differenceY.value > 0) {
-      parent.style.userSelect = 'none'
-    } else {
-      parent.style.userSelect = ''
-    }
-  }
-})
-</script>
-
-<template>
-  <div
-    class="flex h-0 translate-y-10 items-center justify-center"
-    :class="{ invisible: !arrowShown }"
-  >
-    <CommonIcon
-      name="mobile-arrow-down"
-      size="small"
-      :style="{
-        transform: `${rotateDegree ? `rotate(${rotateDegree}deg)` : ''}`,
-        transition: !rotateDegree ? 'transform 0.2s' : '',
-      }"
-    />
-  </div>
-  <div v-if="loaderShown" class="flex items-center justify-center">
-    <CommonIcon name="mobile-loading" animation="spin" />
-  </div>
-</template>

+ 1 - 7
app/frontend/apps/mobile/pages/ticket/components/TicketDetailView/TicketDetailViewReplyButton.vue

@@ -10,7 +10,7 @@ const { newTicketArticlePresent, showArticleReplyDialog } =
 <template>
   <button
     type="button"
-    class="Reply fixed bottom-0 flex w-screen items-center justify-center bg-gray-600 pt-3"
+    class="fixed bottom-0 flex w-screen items-center justify-center bg-gray-600 pt-3 pb-safe-4"
     @click="showArticleReplyDialog"
   >
     <template v-if="newTicketArticlePresent">
@@ -21,9 +21,3 @@ const { newTicketArticlePresent, showArticleReplyDialog } =
     </template>
   </button>
 </template>
-
-<style land="scss" scoped>
-.Reply {
-  padding-bottom: calc(var(--safe-bottom, 0) + theme('height.4'));
-}
-</style>

+ 1 - 1
app/frontend/apps/mobile/pages/ticket/views/TicketCreate.vue

@@ -507,7 +507,7 @@ export default {
     :class="{
       'bg-gray-light backdrop-blur-lg': !isScrolledToBottom,
     }"
-    class="bottom-navigation fixed bottom-0 z-10 h-32 w-full px-4 transition"
+    class="pb-safe fixed bottom-0 z-10 w-full px-4 transition"
   >
     <FormKit
       :variant="lastStepName === activeStep ? 'submit' : 'primary'"

+ 1 - 1
app/frontend/apps/mobile/pages/ticket/views/TicketDetailArticlesView.vue

@@ -252,7 +252,7 @@ const { stickyStyles, headerElement } = useStickyHeader([
       <TicketTitle v-if="ticket" :ticket="ticket" />
     </CommonLoader>
   </div>
-  <div class="flex flex-1 flex-col pb-20" :style="stickyStyles.body">
+  <div class="flex flex-1 flex-col pb-safe-20" :style="stickyStyles.body">
     <CommonLoader
       data-test-id="loader-list"
       :loading="isLoadingTicket"

+ 3 - 11
app/frontend/apps/mobile/pages/ticket/views/TicketDetailView.vue

@@ -231,7 +231,7 @@ const bannerClasses = computed(() => {
 
   if (route.name !== 'TicketDetailArticlesView') return null
 
-  return articleReplyDialog.isOpened.value ? null : 'ReplyButtonPadding'
+  return articleReplyDialog.isOpened.value ? null : '-translate-y-12'
 })
 </script>
 
@@ -239,7 +239,7 @@ const bannerClasses = computed(() => {
   <RouterView />
   <div
     class="transition-all"
-    :class="{ 'pb-12': needSpaceForSaveBanner }"
+    :class="{ 'pb-safe-12': needSpaceForSaveBanner }"
   ></div>
   <!-- submit form is always present in the DOM, so we can access FormKit validity state -->
   <!-- if it's visible, it's moved to the [data-ticket-edit-form] element, which is in TicketInformationDetail -->
@@ -280,7 +280,7 @@ const bannerClasses = computed(() => {
     >
       <div
         v-if="canUpdateTicket && isDirty"
-        class="fixed bottom-2 z-10 flex rounded-lg bg-gray-300 text-white transition ltr:left-2 ltr:right-2 rtl:right-2 rtl:left-2"
+        class="mb-safe fixed bottom-2 z-10 flex rounded-lg bg-gray-300 text-white transition ltr:left-2 ltr:right-2 rtl:right-2 rtl:left-2"
         :class="bannerClasses"
       >
         <div class="relative flex flex-1 items-center gap-2 p-1.5">
@@ -316,11 +316,3 @@ const bannerClasses = computed(() => {
     </Transition>
   </Teleport>
 </template>
-
-<style lang="scss" scoped>
-.ReplyButtonPadding {
-  --reply-size: calc(0px - theme('height.12') - var(--safe-bottom, 0px));
-
-  transform: translateY(var(--reply-size));
-}
-</style>

+ 10 - 0
app/frontend/apps/mobile/styles/main.scss

@@ -96,3 +96,13 @@
     @apply translate-y-0 text-xs opacity-75;
   }
 }
+
+@layer utilities {
+  .pb-safe {
+    padding-bottom: env(safe-area-inset-bottom);
+  }
+
+  .mb-safe {
+    margin-bottom: env(safe-area-inset-bottom);
+  }
+}

+ 13 - 2
tailwind.config.js

@@ -64,7 +64,6 @@ module.exports = {
       'dark-blue': '#045972',
       orange: '#F39804',
     },
-    extend: {},
     minWidth: {
       '1/2-2': 'calc(100% / 2 - theme(spacing.2))',
     },
@@ -72,7 +71,19 @@ module.exports = {
   plugins: [
     lineClampPlugin,
     formKitTailwind,
-    plugin(({ addVariant }) => {
+    plugin(({ addVariant, matchUtilities, theme }) => {
+      matchUtilities(
+        {
+          'pb-safe': (value) => ({
+            paddingBottom: `calc(var(--safe-bottom, 0) + ${value})`,
+          }),
+          'mb-safe': (value) => ({
+            marginBottom: `calc(var(--safe-bottom, 0) + ${value})`,
+          }),
+        },
+        { values: theme('padding') },
+      )
+
       addVariant('formkit-populated', [
         '&[data-populated]',
         '[data-populated] &',