Browse Source

Fixes: Mobile - Fix small select field height and "More" icons in articles

Vladimir Sheremet 2 years ago
parent
commit
35166ecdca

+ 1 - 1
app/frontend/apps/mobile/components/CommonButtonGroup/CommonButtonGroup.story.vue

@@ -12,7 +12,7 @@ const alert = () => window.alert('click')
     <CommonButtonGroup
       :options="[
         { label: 'link %s', labelPlaceholder: ['text'], link: '/example' },
-        { label: 'button', onAction: alert, selected: true },
+        { label: 'button', onAction: alert },
         {
           label: 'with-icon',
           onAction: alert,

+ 26 - 5
app/frontend/apps/mobile/components/CommonButtonGroup/CommonButtonGroup.vue

@@ -5,10 +5,18 @@ import { type Props as IconProps } from '@shared/components/CommonIcon/CommonIco
 import type { CommonButtonOption } from './types'
 
 export interface Props {
+  modelValue?: string | number
+  mode?: 'full' | 'compressed'
   options: CommonButtonOption[]
 }
 
-defineProps<Props>()
+withDefaults(defineProps<Props>(), {
+  mode: 'compressed',
+})
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value?: string | number): void
+}>()
 
 const getIconProps = (option: CommonButtonOption): IconProps => {
   if (!option.icon) return {} as IconProps
@@ -17,19 +25,32 @@ const getIconProps = (option: CommonButtonOption): IconProps => {
   }
   return option.icon
 }
+
+const onButtonClick = (option: CommonButtonOption) => {
+  if (option.disabled) return
+  option.onAction?.()
+  emit('update:modelValue', option.value)
+}
 </script>
 
 <template>
-  <div class="flex w-full gap-3">
+  <div
+    class="flex max-w-[100vw] overflow-x-auto"
+    :class="{ 'w-full': mode === 'full' }"
+  >
     <Component
       :is="option.link ? 'CommonLink' : 'button'"
       v-for="option of options"
       :key="option.label"
       :disabled="option.disabled"
       :link="option.link"
-      class="flex flex-1 flex-col items-center justify-center gap-1 rounded-xl bg-gray-500 p-2 text-white"
-      :class="{ 'bg-gray-200': option.selected }"
-      @click="!option.disabled && option.onAction?.()"
+      class="flex flex-col items-center justify-center gap-1 rounded-xl bg-gray-500 py-1 px-3 text-base text-white ltr:mr-2 rtl:ml-2"
+      :class="{
+        'bg-gray-600/50 text-white/30': option.disabled,
+        'bg-gray-200': option.value != null && modelValue === option.value,
+        'flex-1': mode === 'full',
+      }"
+      @click="onButtonClick(option)"
     >
       <CommonIcon v-if="option.icon" v-bind="getIconProps(option)" decorative />
       <span>{{ $t(option.label, ...(option.labelPlaceholder || [])) }}</span>

+ 15 - 4
app/frontend/apps/mobile/components/CommonButtonGroup/__tests__/CommonButtonGroup.spec.ts

@@ -11,13 +11,24 @@ describe('buttons group', () => {
     const onAction = vi.fn()
 
     const options: CommonButtonOption[] = [
-      { label: 'link %s', labelPlaceholder: ['text'], link: '/example' },
-      { label: 'button', onAction, selected: true },
-      { label: 'with-icon', onAction, icon: 'mobile-home', disabled: true },
+      {
+        label: 'link %s',
+        labelPlaceholder: ['text'],
+        link: '/example',
+        value: 'link',
+      },
+      { label: 'button', onAction, value: 'button' },
+      {
+        label: 'with-icon',
+        onAction,
+        icon: 'mobile-home',
+        disabled: true,
+        value: 'icon',
+      },
     ]
 
     const view = renderComponent(CommonButtonGroup, {
-      props: { options },
+      props: { options, modelValue: 'button' },
       router: true,
       store: true,
     })

+ 1 - 1
app/frontend/apps/mobile/components/CommonButtonGroup/types.ts

@@ -4,10 +4,10 @@ import { type Props as IconProps } from '@shared/components/CommonIcon/CommonIco
 
 export interface CommonButtonOption {
   link?: string
+  value?: string | number
   onAction?(): void | Promise<void>
   label: string
   labelPlaceholder?: string[]
   disabled?: boolean
-  selected?: boolean
   icon?: string | IconProps
 }

+ 0 - 54
app/frontend/apps/mobile/components/CommonButtonPills/CommonButtonPills.vue

@@ -1,54 +0,0 @@
-<!-- Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ -->
-
-<script setup lang="ts">
-import { useSessionStore } from '@shared/stores/session'
-import { useVModel } from '@vueuse/core'
-import { computed } from 'vue'
-import type { ButtonPillOption } from './types'
-
-interface Props {
-  modelValue?: string | number
-  noBorder?: boolean
-  options: ButtonPillOption[]
-}
-
-const props = defineProps<Props>()
-const emit = defineEmits<{
-  (e: 'update:modelValue', value: string | number): void
-}>()
-const localValue = useVModel(props, 'modelValue', emit)
-
-const session = useSessionStore()
-
-const filteredOptions = computed(() => {
-  return props.options.filter((option) => {
-    if (!option.permissions) return true
-    return session.hasPermission(option.permissions)
-  })
-})
-</script>
-
-<template>
-  <div
-    class="flex max-w-[100vw] overflow-x-auto"
-    :class="{ 'border-b border-white/10': !noBorder }"
-    data-test-id="buttonPills"
-  >
-    <button
-      v-for="option of filteredOptions"
-      :key="option.value"
-      :disabled="option.disabled"
-      class="rounded-xl py-1 px-3 text-base ltr:mr-2 rtl:ml-2"
-      :class="{
-        ['bg-gray-600/50 text-white/30']: option.disabled,
-        ['bg-gray-200']: !option.disabled && modelValue === option.value,
-        ['bg-gray-600 text-white/60']:
-          !option.disabled && modelValue !== option.value,
-      }"
-      @click="localValue = option.value"
-      @keydown.space="localValue = option.value"
-    >
-      {{ $t(option.label, ...(option.labelPlaceholder || [])) }}
-    </button>
-  </div>
-</template>

+ 0 - 110
app/frontend/apps/mobile/components/CommonButtonPills/__tests__/CommonButtonPills.spec.ts

@@ -1,110 +0,0 @@
-// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
-
-import { i18n } from '@shared/i18n'
-import { renderComponent } from '@tests/support/components'
-import { ref } from 'vue'
-import CommonButtonPills from '../CommonButtonPills.vue'
-import type { ButtonPillOption } from '../types'
-
-describe('buttons component', () => {
-  it('renders buttons', async () => {
-    const options: ButtonPillOption[] = [
-      {
-        label: 'Button 1',
-        value: '1',
-      },
-      {
-        label: 'Button 2',
-        value: '2',
-      },
-      {
-        label: 'Button 3',
-        value: '3',
-        permissions: ['no.permissions'],
-      },
-    ]
-
-    const modelValue = ref('1')
-
-    const view = renderComponent(CommonButtonPills, {
-      props: {
-        options,
-      },
-      vModel: {
-        modelValue,
-      },
-      store: true,
-    })
-
-    const button1 = view.getByRole('button', { name: 'Button 1' })
-    const button2 = view.getByRole('button', { name: 'Button 2' })
-
-    expect(button1).toBeInTheDocument()
-    expect(button2).toBeInTheDocument()
-
-    expect(view.queryByText('Button 3')).not.toBeInTheDocument()
-
-    expect(button1).toHaveClass('bg-gray-200')
-    expect(button2).not.toHaveClass('bg-gray-200')
-    expect(button2).toHaveClass('bg-gray-600')
-
-    await view.events.click(button2)
-
-    expect(button2).toHaveClass('bg-gray-200')
-    expect(button1).not.toHaveClass('bg-gray-200')
-    expect(button1).toHaveClass('bg-gray-600')
-
-    expect(modelValue.value).toBe('2')
-    expect(view.emitted()['update:modelValue']).toBeTruthy()
-  })
-
-  it('translates text', () => {
-    i18n.setTranslationMap(new Map([['Button %s', 'Translated %s']]))
-
-    const view = renderComponent(CommonButtonPills, {
-      props: {
-        options: [
-          { label: 'Button %s', labelPlaceholder: ['text'], value: '1' },
-        ],
-        modelValue: '1',
-      },
-      store: true,
-    })
-
-    expect(view.getByText('Translated text')).toBeInTheDocument()
-  })
-
-  it('cannot select disabled option', async () => {
-    const options: ButtonPillOption[] = [
-      {
-        label: 'Button 1',
-        value: '1',
-      },
-      {
-        label: 'Button 2',
-        disabled: true,
-        value: '2',
-      },
-    ]
-
-    const modelValue = ref('1')
-
-    const view = renderComponent(CommonButtonPills, {
-      props: {
-        options,
-      },
-      vModel: {
-        modelValue,
-      },
-      store: true,
-    })
-
-    const button2 = view.getByRole('button', { name: 'Button 2' })
-
-    expect(button2).toBeDisabled()
-
-    await view.events.click(view.getByRole('button', { name: 'Button 2' }))
-
-    expect(modelValue.value).toBe('1')
-  })
-})

+ 0 - 9
app/frontend/apps/mobile/components/CommonButtonPills/types.ts

@@ -1,9 +0,0 @@
-// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
-
-export type ButtonPillOption = {
-  value: string | number
-  label?: string
-  disabled?: boolean
-  labelPlaceholder?: string[]
-  permissions?: string[]
-}

+ 1 - 1
app/frontend/apps/mobile/components/CommonSelect/CommonSelect.vue

@@ -9,7 +9,7 @@ import CommonSelectItem from './CommonSelectItem.vue'
 export interface Props {
   // we cannot move types into separate file, because Vue would not be able to
   // transform these into runtime types
-  modelValue?: string | number | boolean | (string | number | boolean)[]
+  modelValue?: string | number | boolean | (string | number | boolean)[] | null
   options: SelectOption[]
   /**
    * Do not modify local value

+ 4 - 0
app/frontend/apps/mobile/components/CommonSelect/CommonSelectItem.vue

@@ -50,6 +50,8 @@ const label = computed(() => {
         'opacity-30': option.disabled,
       }"
       size="base"
+      :aria-hidden="!selected"
+      :aria-label="$t('Selected')"
       :name="selected ? 'mobile-check-box-yes' : 'mobile-check-box-no'"
       class="mr-3 text-white/50"
     />
@@ -88,6 +90,8 @@ const label = computed(() => {
         invisible: !selected,
         'opacity-30': option.disabled,
       }"
+      :aria-label="$t('Selected')"
+      :aria-hidden="!selected"
       size="tiny"
       name="mobile-check"
     />

+ 63 - 0
app/frontend/apps/mobile/components/CommonSelectPill/CommonSelectPill.vue

@@ -0,0 +1,63 @@
+<!-- Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ -->
+
+<script setup lang="ts">
+import CommonSelect from '@mobile/components/CommonSelect/CommonSelect.vue'
+import type { SelectOption } from '@shared/components/Form/fields/FieldSelect/types'
+import { computed } from 'vue'
+
+const props = defineProps<{
+  modelValue?: string | number | boolean | (string | number | boolean)[] | null
+  options: SelectOption[]
+  placeholder?: string
+  multiple?: boolean
+  noClose?: boolean
+  noOptionsLabelTranslation?: boolean
+}>()
+
+const emit = defineEmits<{
+  (
+    event: 'update:modelValue',
+    value: string | number | (string | number)[],
+  ): void
+  (e: 'select', option: SelectOption): void
+}>()
+
+const dialogProps = computed(() => {
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  const { placeholder, ...dialogProps } = props
+  return dialogProps
+})
+
+const defaultLabel = computed(() => {
+  const option = props.options.find(
+    (option) => option.value === props.modelValue,
+  )
+  return option?.label || props.placeholder || ''
+})
+</script>
+
+<template>
+  <CommonSelect
+    #default="{ open }"
+    v-bind="dialogProps"
+    @update:model-value="emit('update:modelValue', $event)"
+    @select="emit('select', $event)"
+  >
+    <button
+      type="button"
+      class="inline-flex w-auto cursor-pointer rounded-lg bg-gray-600 py-1 ltr:pl-2 ltr:pr-1 rtl:pr-2 rtl:pl-1"
+      @click="open"
+      @keydown.space="open"
+    >
+      <slot>
+        {{ defaultLabel }}
+      </slot>
+      <CommonIcon
+        class="self-center"
+        name="mobile-caret-down"
+        size="tiny"
+        decorative
+      />
+    </button>
+  </CommonSelect>
+</template>

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