Browse Source

Feature: Mobile - Add a Stepper component

Vladimir Sheremet 2 years ago
parent
commit
f3d0c349f1

+ 49 - 0
app/frontend/apps/mobile/components/CommonStepper/CommonStepper.story.vue

@@ -0,0 +1,49 @@
+<!-- Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ -->
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import CommonStepper from './CommonStepper.vue'
+import type { CommonStepperStep } from './types'
+
+const modelValue = ref('step1')
+const steps: Record<string, CommonStepperStep> = {
+  step1: {
+    label: '1',
+    order: 1,
+    errorCount: 0,
+    valid: true,
+    disabled: false,
+    completed: true,
+  },
+  step2: {
+    label: '2',
+    order: 2,
+    errorCount: 0,
+    valid: true,
+    disabled: false,
+    completed: false,
+  },
+  step3: {
+    label: '3',
+    order: 3,
+    errorCount: 0,
+    valid: true,
+    completed: false,
+    disabled: true,
+  },
+  step4: {
+    label: '4',
+    order: 4,
+    errorCount: 3,
+    valid: false,
+    completed: false,
+    disabled: true,
+  },
+}
+</script>
+
+<template>
+  <Story>
+    <CommonStepper v-model="modelValue" :steps="steps"></CommonStepper>
+  </Story>
+</template>

+ 40 - 0
app/frontend/apps/mobile/components/CommonStepper/CommonStepper.vue

@@ -0,0 +1,40 @@
+<!-- Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ -->
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import type { CommonStepperStep as Step } from './types'
+import CommonStepperStep from './CommonStepperStep.vue'
+
+interface Props {
+  modelValue: string
+  steps: Record<string, Step>
+}
+
+const props = defineProps<Props>()
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: string): void
+}>()
+
+const localSteps = computed(() => {
+  return Object.entries(props.steps).sort(([, a], [, b]) => a.order - b.order)
+})
+</script>
+
+<template>
+  <div class="flex justify-center text-base">
+    <template v-for="([name, step], idx) of localSteps" :key="name">
+      <div class="flex" :class="{ 'flex-1': idx !== localSteps.length - 1 }">
+        <CommonStepperStep
+          v-bind="step"
+          :selected="name === modelValue"
+          @click="emit('update:modelValue', name)"
+        />
+        <div
+          v-if="idx !== localSteps.length - 1"
+          class="mx-2 h-px flex-1 self-center bg-white/20"
+        />
+      </div>
+    </template>
+  </div>
+</template>

+ 55 - 0
app/frontend/apps/mobile/components/CommonStepper/CommonStepperStep.vue

@@ -0,0 +1,55 @@
+<!-- Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ -->
+
+<script setup lang="ts">
+import { computed } from 'vue'
+
+const props = defineProps<{
+  label: string
+  selected: boolean
+  completed: boolean
+  valid: boolean
+  disabled: boolean
+  errorCount: number
+}>()
+
+const classes = computed(() => {
+  if (props.selected) return 'bg-white text-black'
+  if (!props.valid) return 'bg-red-dark'
+  if (props.completed) return 'bg-gray-400'
+  return 'text-white/70'
+})
+</script>
+
+<template>
+  <button
+    class="flex h-6 w-6 grow-0 items-center justify-center rounded-full"
+    :disabled="disabled"
+    :class="classes"
+  >
+    <div
+      v-if="errorCount"
+      role="status"
+      :aria-label="$t('Invalid values in step %s', label)"
+      aria-live="assertive"
+      class="absolute ml-6 mb-3 h-4 min-w-[1rem] rounded-full bg-red px-1 text-center text-xs text-black"
+    >
+      {{ errorCount }}
+    </div>
+    <template v-if="selected">{{ label }}</template>
+    <CommonIcon
+      v-else-if="!valid"
+      decorative
+      name="mobile-close"
+      size="tiny"
+      class="text-red-bright"
+    />
+    <CommonIcon
+      v-else-if="completed"
+      :aria-label="$t('Step %s is completed', label)"
+      name="mobile-check"
+      size="tiny"
+      class="text-blue"
+    />
+    <template v-else>{{ label }}</template>
+  </button>
+</template>

+ 70 - 0
app/frontend/apps/mobile/components/CommonStepper/__tests__/CommonStepper.spec.ts

@@ -0,0 +1,70 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+import { renderComponent } from '@tests/support/components'
+import { ref } from 'vue'
+import CommonStepper from '../CommonStepper.vue'
+import type { CommonStepperStep } from '../types'
+
+describe('stepper component', () => {
+  test('renders valid steps', async () => {
+    const modelValue = ref('step1')
+    const steps: Record<string, CommonStepperStep> = {
+      step1: {
+        label: '1',
+        order: 1,
+        errorCount: 0,
+        valid: true,
+        disabled: false,
+        completed: true,
+      },
+      step2: {
+        label: '2',
+        order: 2,
+        errorCount: 0,
+        valid: true,
+        disabled: false,
+        completed: false,
+      },
+      step3: {
+        label: '3',
+        order: 3,
+        errorCount: 0,
+        valid: true,
+        completed: false,
+        disabled: true,
+      },
+      step4: {
+        label: '4',
+        order: 4,
+        errorCount: 3,
+        valid: false,
+        completed: false,
+        disabled: true,
+      },
+    }
+    const view = renderComponent(CommonStepper, {
+      props: {
+        steps,
+      },
+      vModel: {
+        modelValue,
+      },
+    })
+
+    await view.events.click(view.getByText('2'))
+
+    expect(modelValue.value).toBe('step2')
+
+    expect(
+      view.getByRole('status', { name: 'Invalid values in step 4' }),
+    ).toHaveTextContent('3')
+
+    expect(
+      view.getByRole('button', { name: 'Step 1 is completed' }),
+    ).toBeInTheDocument()
+
+    await view.events.click(view.getByRole('button', { name: '3' }))
+
+    expect(modelValue.value).toBe('step2')
+  })
+})

+ 10 - 0
app/frontend/apps/mobile/components/CommonStepper/types.ts

@@ -0,0 +1,10 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+export interface CommonStepperStep {
+  label: string
+  order: number
+  errorCount: number
+  valid: boolean
+  disabled: boolean
+  completed: boolean
+}

+ 1 - 1
app/frontend/apps/mobile/components/layout/LayoutBottomNavigation.vue

@@ -43,7 +43,7 @@ const notificationCount = computed(() => {
         <div
           v-if="notificationCount"
           role="status"
-          :aria-label="__('Unread notifications')"
+          :aria-label="$t('Unread notifications')"
           class="absolute ml-4 h-4 min-w-[1rem] rounded-full bg-blue px-1 text-center text-xs text-black"
         >
           {{ notificationCount }}

+ 40 - 0
app/frontend/apps/mobile/pages/playground/views/PlaygroundOverview.vue

@@ -8,6 +8,8 @@ import { defineFormSchema } from '@mobile/form/defineFormSchema'
 import { useDialog } from '@shared/composables/useDialog'
 import CommonButtonGroup from '@mobile/components/CommonButtonGroup/CommonButtonGroup.vue'
 import { useUserCreate } from '@mobile/entities/user/composables/useUserCreate'
+import CommonStepper from '@mobile/components/CommonStepper/CommonStepper.vue'
+import { ref } from 'vue'
 
 const linkSchemaRaw = [
   {
@@ -296,6 +298,42 @@ const dialog = useDialog({
 })
 
 const { openCreateUserDialog } = useUserCreate()
+
+const currentStep = ref('step2')
+const steps = {
+  step1: {
+    label: '1',
+    order: 1,
+    errorCount: 0,
+    valid: true,
+    disabled: false,
+    completed: true,
+  },
+  step2: {
+    label: '2',
+    order: 2,
+    errorCount: 0,
+    valid: true,
+    disabled: true,
+    completed: false,
+  },
+  step3: {
+    label: '3',
+    order: 3,
+    errorCount: 0,
+    valid: true,
+    completed: true,
+    disabled: true,
+  },
+  step4: {
+    label: '4',
+    order: 4,
+    errorCount: 3,
+    valid: false,
+    completed: false,
+    disabled: true,
+  },
+}
 </script>
 
 <template>
@@ -304,6 +342,8 @@ const { openCreateUserDialog } = useUserCreate()
       Dialog
     </button>
 
+    <CommonStepper v-model="currentStep" class="mx-20" :steps="steps" />
+
     <!-- TODO where to put this? -->
     <button @click="openCreateUserDialog()">Create user</button>
 

+ 8 - 0
i18n/zammad.pot

@@ -5690,6 +5690,10 @@ msgstr ""
 msgid "Invalid token, please contact your admin!"
 msgstr ""
 
+#: app/frontend/apps/mobile/components/CommonStepper/CommonStepperStep.vue
+msgid "Invalid values in step %s"
+msgstr ""
+
 #: app/assets/javascripts/app/controllers/_ui_element/richtext_additions/popup_video.coffee
 msgid "Invalid video URL"
 msgstr ""
@@ -9316,6 +9320,10 @@ msgstr ""
 msgid "Stay on tab"
 msgstr ""
 
+#: app/frontend/apps/mobile/components/CommonStepper/CommonStepperStep.vue
+msgid "Step %s is completed"
+msgstr ""
+
 #: app/assets/javascripts/app/models/core_workflow.coffee
 msgid "Stop after match"
 msgstr ""