Browse Source

Feature: Mobile - Added improvements to the form field button.

Dominik Klein 2 years ago
parent
commit
52befd034a

+ 13 - 2
app/frontend/apps/mobile/form/theme/global/getCoreClasses.ts

@@ -4,7 +4,7 @@ import type { FormThemeClasses, FormThemeExtension } from '@shared/types/form'
 
 type Classes = Record<string, string>
 
-export const addFloatingLabel = (classes: Classes): Classes => {
+export const addFloatingLabel = (classes: Classes = {}): Classes => {
   const inputClass = classes.input || ''
   const labelClass = classes.label || ''
   return {
@@ -16,7 +16,7 @@ export const addFloatingLabel = (classes: Classes): Classes => {
   }
 }
 
-export const addDateLabel = (classes: Classes): Classes => {
+export const addDateLabel = (classes: Classes = {}): Classes => {
   const newClasses = addFloatingLabel(classes)
   return {
     ...newClasses,
@@ -24,6 +24,15 @@ export const addDateLabel = (classes: Classes): Classes => {
   }
 }
 
+export const addButtonVariants = (classes: Classes = {}): Classes => {
+  return {
+    wrapper: `${classes.wrapper || ''} relative`,
+    input: `${
+      classes.input || ''
+    } formkit-variant-primary:bg-blue formkit-variant-secondary:bg-transparent`,
+  }
+}
+
 const getCoreClasses: FormThemeExtension = (classes: FormThemeClasses) => {
   return {
     global: {},
@@ -45,6 +54,8 @@ const getCoreClasses: FormThemeExtension = (classes: FormThemeClasses) => {
       ...(classes.select || {}),
       outer: `${classes.select && classes.select.outer} field-treeselect`,
     }),
+    button: addButtonVariants(classes.button),
+    submit: addButtonVariants(classes.submit),
   }
 }
 

+ 2 - 2
app/frontend/apps/mobile/modules/login/views/Login.vue

@@ -43,7 +43,7 @@ interface LoginFormData {
 }
 
 const login = (formData: FormData<LoginFormData>) => {
-  authentication
+  return authentication
     .login(formData.login as string, formData.password as string)
     .then(() => {
       router.replace('/')
@@ -107,7 +107,7 @@ const application = useApplicationStore()
               </div>
               <FormKit
                 wrapper-class="mx-8 mt-8 flex grow justify-center items-center"
-                input-class="py-2 px-4 w-full h-14 text-xl font-semibold text-black bg-yellow rounded-xl select-none"
+                input-class="py-2 px-4 w-full h-14 text-xl font-semibold text-black formkit-variant-primary:bg-yellow rounded-xl select-none"
                 type="submit"
               >
                 {{ i18n.t('Sign in') }}

+ 1 - 1
app/frontend/shared/components/Form/Form.vue

@@ -126,7 +126,7 @@ const onSubmit = (values: FormData): Promise<void> | void => {
 
   const submitResult = props.onSubmit(emitValues)
 
-  // TODO: maybe we need to handle the disabled state on submit on our own. In clarification with FormKit.
+  // TODO: Maybe we need to handle the disabled state on submit on our own. In clarification with FormKit (https://github.com/formkit/formkit/issues/236).
   if (submitResult instanceof Promise) {
     return submitResult.catch((errors: UserError) => {
       formNode.value?.setErrors(

+ 55 - 2
app/frontend/shared/components/Form/fields/FieldButton/__tests__/FieldButton.spec.ts

@@ -33,6 +33,7 @@ describe('Form - Field - Button (Formkit-BuildIn)', () => {
     const button = view.getByText('Sign In')
 
     expect(button).toHaveAttribute('id', 'button')
+    expect(button.closest('div')).toHaveAttribute('data-variant', 'primary')
   })
 
   it('can render a button with a label instead of slot', () => {
@@ -48,6 +49,23 @@ describe('Form - Field - Button (Formkit-BuildIn)', () => {
     expect(view.getByText('Sign In')).toBeInTheDocument()
   })
 
+  it('can use different variant', () => {
+    const view = renderButton({
+      props: {
+        name: 'button',
+        type: 'button',
+        id: 'button',
+        label: 'Sign In',
+        variant: 'secondary',
+      },
+    })
+
+    expect(view.getByText('Sign In').closest('div')).toHaveAttribute(
+      'data-variant',
+      'secondary',
+    )
+  })
+
   it('can be disabled', async () => {
     const view = renderButton({
       props: {
@@ -75,6 +93,43 @@ describe('Form - Field - Button (Formkit-BuildIn)', () => {
 
     expect(button).not.toHaveAttribute('disabled')
   })
+
+  it('can use icons', async () => {
+    const view = renderButton({
+      props: {
+        name: 'button',
+        type: 'button',
+        id: 'button',
+        label: 'Sign In',
+        icon: 'arrow-right',
+      },
+    })
+
+    const icon = view.getIconByName('arrow-right')
+
+    expect(icon).toBeInTheDocument()
+  })
+
+  it('can trigger action on icon', async () => {
+    const iconClickSpy = vi.fn()
+
+    const view = renderButton({
+      props: {
+        name: 'button',
+        type: 'button',
+        id: 'button',
+        label: 'Sign In',
+        icon: 'arrow-right',
+        onIconClick: iconClickSpy,
+      },
+    })
+
+    const icon = view.getIconByName('arrow-right')
+
+    await view.events.click(icon)
+
+    expect(iconClickSpy).toHaveBeenCalledTimes(1)
+  })
 })
 
 describe('Form - Field - Submit-Button (Formkit-BuildIn)', () => {
@@ -96,5 +151,3 @@ describe('Form - Field - Submit-Button (Formkit-BuildIn)', () => {
     expect(button).toHaveAttribute('type', 'submit')
   })
 })
-
-// TODO: Add test cases for new functionality, e.g. some loading state.

+ 49 - 14
app/frontend/shared/components/Form/fields/FieldButton/index.ts

@@ -1,23 +1,58 @@
 // Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
 
-import { FormFieldType } from '@shared/types/form'
+import type { FormKitNode } from '@formkit/core'
+import { has } from '@formkit/utils'
 import {
   button as buttonDefinition,
   submit as submitDefinition,
 } from '@formkit/inputs'
+import type {
+  FormFieldsTypeDefinition,
+  FormFieldType,
+} from '@shared/types/form'
+import initializeFieldDefinition from '@shared/form/core/initializeFieldDefinition'
+import extendSchemaDefinition from '@shared/form/utils/extendSchemaDefinition'
+import addIcon from '@shared/form/features/addIcon'
+import { ButtonVariant } from './types'
 
-// TODO: Build-In loading cycle funcitonality for the buttons?
-// TODO: Build-In Variants? => Best solution with Tailwind?
-
-const buttonInputs: FormFieldType[] = [
-  {
-    fieldType: 'button',
-    definition: buttonDefinition,
-  },
-  {
-    fieldType: 'submit',
-    definition: submitDefinition,
-  },
-]
+// TODO: Build-In loading cycle funcitonality for the buttons or at least a disabled-state when loading is in progress?
+
+const addVariantDataAttribute = (node: FormKitNode) => {
+  extendSchemaDefinition(node, 'wrapper', {
+    attrs: {
+      'data-variant': '$variant',
+    },
+  })
+}
+
+const setVariantDefault = (node: FormKitNode) => {
+  const { props } = node
+
+  node.addProps(['variant'])
+
+  node.on('created', () => {
+    if (!has(props, 'variant')) {
+      props.variant = ButtonVariant.primary
+    }
+  })
+}
+
+const buttonFieldDefinitionList: FormFieldsTypeDefinition = {
+  button: buttonDefinition,
+  submit: submitDefinition,
+}
+
+const buttonInputs: FormFieldType[] = []
+
+Object.keys(buttonFieldDefinitionList).forEach((buttonType) => {
+  initializeFieldDefinition(buttonFieldDefinitionList[buttonType], {
+    features: [setVariantDefault, addVariantDataAttribute, addIcon],
+  })
+
+  buttonInputs.push({
+    fieldType: buttonType,
+    definition: buttonFieldDefinitionList[buttonType],
+  })
+})
 
 export default buttonInputs

+ 6 - 0
app/frontend/shared/components/Form/fields/FieldButton/types.ts

@@ -0,0 +1,6 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+export enum ButtonVariant {
+  'primary' = 'primary',
+  'secondary' = 'secondary',
+}

+ 19 - 34
app/frontend/shared/components/Form/fields/FieldCheckbox/index.ts

@@ -1,46 +1,32 @@
 // Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
 
-import { cloneDeep } from '@apollo/client/utilities'
-import initializeFieldDefinition from '@shared/form/core/initializeFieldDefinition'
-import { FormKitExtendableSchemaRoot, FormKitNode } from '@formkit/core'
+import { FormKitNode } from '@formkit/core'
 import { checkbox as checkboxDefinition } from '@formkit/inputs'
 import { has } from '@formkit/utils'
+import initializeFieldDefinition from '@shared/form/core/initializeFieldDefinition'
+import extendSchemaDefinition from '@shared/form/utils/extendSchemaDefinition'
 import { CheckboxVariant } from './types'
 
 const addOptionCheckedDataAttribute = (node: FormKitNode) => {
-  const { props } = node
-
-  if (!props.definition) return
-
-  const definition = cloneDeep(props.definition)
-
-  const originalSchema = definition.schema as FormKitExtendableSchemaRoot
+  node.addProps(['variant'])
 
-  definition.schema = (extensions) => {
-    const localExtensions = {
-      ...extensions,
-      wrapper: {
-        attrs: {
-          'data-is-checked': {
-            if: '$options.length',
-            then: {
-              if: '$fns.isChecked($option.value)',
-              then: 'true',
-              else: undefined,
-            },
-            else: {
-              if: '$_value',
-              then: 'true',
-              else: undefined,
-            },
-          },
+  extendSchemaDefinition(node, 'wrapper', {
+    attrs: {
+      'data-is-checked': {
+        if: '$options.length',
+        then: {
+          if: '$fns.isChecked($option.value)',
+          then: 'true',
+          else: undefined,
+        },
+        else: {
+          if: '$_value',
+          then: 'true',
+          else: undefined,
         },
       },
-    }
-    return originalSchema(localExtensions)
-  }
-
-  props.definition = definition
+    },
+  })
 }
 
 const handleVariant = (node: FormKitNode) => {
@@ -73,7 +59,6 @@ const handleVariant = (node: FormKitNode) => {
 }
 
 initializeFieldDefinition(checkboxDefinition, {
-  props: ['variant'],
   features: [addOptionCheckedDataAttribute, handleVariant],
 })
 

+ 7 - 22
app/frontend/shared/components/Form/fields/FieldEditor/index.ts

@@ -1,36 +1,21 @@
 // Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
 
+import { FormKitNode } from '@formkit/core'
 import createInput from '@shared/form/core/createInput'
-import { FormKitExtendableSchemaRoot, FormKitNode } from '@formkit/core'
-import { cloneDeep } from 'lodash-es'
-
+import extendSchemaDefinition from '@shared/form/utils/extendSchemaDefinition'
 import FieldEditorWrapper from './FieldEditorWrapper.vue'
 
 function addAriaLabel(node: FormKitNode) {
   const { props } = node
 
-  if (!props.definition) return
-
-  const definition = cloneDeep(props.definition)
-
-  const originalSchema = definition.schema as FormKitExtendableSchemaRoot
-
   // Specification doesn't allow accessing non-labeled elements, which Editor is (<div />)
   // (https://html.spec.whatwg.org/multipage/forms.html#category-label)
   // So, editor has `aria-labelledby` attribute and a label with the same ID
-  definition.schema = (definition) => {
-    const localDefinition = {
-      ...definition,
-      label: {
-        attrs: {
-          id: props.id,
-        },
-      },
-    }
-    return originalSchema(localDefinition)
-  }
-
-  props.definition = definition
+  extendSchemaDefinition(node, 'label', {
+    attrs: {
+      id: props.id,
+    },
+  })
 }
 
 const fieldDefinition = createInput(FieldEditorWrapper, [], {

+ 17 - 31
app/frontend/shared/components/Form/fields/FieldPassword/index.ts

@@ -1,9 +1,10 @@
 // Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
 
 import { cloneDeep } from 'lodash-es'
-import { FormKitExtendableSchemaRoot, FormKitNode } from '@formkit/core'
+import { FormKitNode } from '@formkit/core'
 import { password as passwordDefinition } from '@formkit/inputs'
 import initializeFieldDefinition from '@shared/form/core/initializeFieldDefinition'
+import extendSchemaDefinition from '@shared/form/utils/extendSchemaDefinition'
 
 const localPasswordDefinition = cloneDeep(passwordDefinition)
 
@@ -11,40 +12,25 @@ const switchPasswordVisibility = (node: FormKitNode) => {
   const { props } = node
 
   node.addProps(['passwordVisibilityIcon'])
-
-  if (!props.definition) return
-
-  const definition = cloneDeep(props.definition)
-
   props.passwordVisibilityIcon = 'eye'
 
-  const originalSchema = definition.schema as FormKitExtendableSchemaRoot
-
-  definition.schema = (extensions) => {
-    const localExtensions = {
-      ...extensions,
-      suffix: {
-        $el: 'span',
-        children: [
-          {
-            $cmp: 'CommonIcon',
-            props: {
-              name: '$passwordVisibilityIcon',
-              key: node.name,
-              class: 'absolute top-1/2 transform -translate-y-1/2 right-3',
-              size: 'small',
-              onClick: () => {
-                props.type = props.type === 'password' ? 'text' : 'password'
-              },
-            },
+  extendSchemaDefinition(node, 'suffix', {
+    $el: 'span',
+    children: [
+      {
+        $cmp: 'CommonIcon',
+        props: {
+          name: '$passwordVisibilityIcon',
+          key: node.name,
+          class: 'absolute top-1/2 transform -translate-y-1/2 right-3',
+          size: 'small',
+          onClick: () => {
+            props.type = props.type === 'password' ? 'text' : 'password'
           },
-        ],
+        },
       },
-    }
-    return originalSchema(localExtensions)
-  }
-
-  props.definition = definition
+    ],
+  })
 
   node.on('prop:type', ({ payload, origin }) => {
     const { props } = origin

+ 30 - 0
app/frontend/shared/form/features/__tests__/addIcon.spec.ts

@@ -0,0 +1,30 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+import { createNode } from '@formkit/core'
+import { createLibraryPlugin } from '@formkit/inputs'
+import addIcon from '../addIcon'
+
+describe('translateWrapperProps', () => {
+  it('can translate the label, placeholder (as a prop) and help text', () => {
+    const node = createNode({
+      plugins: [
+        createLibraryPlugin({
+          text: {
+            type: 'input',
+            features: [addIcon],
+            props: ['label'],
+          },
+        }),
+      ],
+      props: {
+        type: 'text',
+        label: 'example',
+        icon: 'eye',
+        onIconClick: vi.fn(),
+      },
+    })
+
+    expect(node.props).toHaveProperty('icon')
+    expect(node.props).toHaveProperty('onIconClick')
+  })
+})

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