Browse Source

Feature: Mobile - Added improvements for the form field checkbox.

Dominik Klein 2 years ago
parent
commit
cc963eb526

+ 6 - 1
.overcommit.yml

@@ -15,7 +15,12 @@ PreCommit:
     on_warn: fail
   CoffeeLint:
     # .coffeelint/rules/* not supported in YAML, specify all rules separately.
-    flags: ['--reporter=csv', '--rules', './.coffeelint/rules/detect_translatable_string.coffee']
+    flags:
+      [
+        '--reporter=csv',
+        '--rules',
+        './.coffeelint/rules/detect_translatable_string.coffee',
+      ]
     enabled: true
     on_warn: fail
     exclude: public/assets/chat/**/*

+ 0 - 4
app/frontend/apps/mobile/views/Login.vue

@@ -130,10 +130,6 @@ const formSchema = [
         type: 'checkbox',
         label: __('Remember me'),
         name: 'remember_me',
-        wrapperClass: 'inline-flex items-center',
-        inputClass:
-          'appearance-none h-4 w-4 border-[1.5px] border-white rounded-sm bg-transparent',
-        innerClass: 'mr-2',
       },
       {
         isLayout: true,

+ 17 - 13
app/frontend/common/components/form/Form.vue

@@ -268,22 +268,26 @@ const localChangeFields = computed(() => {
 })
 
 // If something changed in the change fields, we need to update the current schemaData
-watch(localChangeFields, (newChangeFields) => {
-  Object.keys(newChangeFields).forEach((fieldName) => {
-    const field = {
-      ...newChangeFields[fieldName],
-      name: fieldName,
-    }
+watch(
+  localChangeFields,
+  (newChangeFields) => {
+    Object.keys(newChangeFields).forEach((fieldName) => {
+      const field = {
+        ...newChangeFields[fieldName],
+        name: fieldName,
+      }
 
-    updateSchemaDataField(field)
+      updateSchemaDataField(field)
 
-    nextTick(() => {
-      if (field.value !== values.value[fieldName]) {
-        formNode.at(fieldName)?.input(field.value)
-      }
+      nextTick(() => {
+        if (field.value !== values.value[fieldName]) {
+          formNode.at(fieldName)?.input(field.value)
+        }
+      })
     })
-  })
-})
+  },
+  { deep: true },
+)
 
 // TODO: maybe we should react on schema changes and rebuild the static schema with a new form-id and re-rendering of
 // the complete form (= use the formId as the key for the whole form to trigger the re-rendering of the component...)

+ 73 - 1
app/frontend/common/components/form/field/FieldCheckbox.ts

@@ -1,9 +1,81 @@
 // Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
 
+import { cloneDeep } from '@apollo/client/utilities'
 import initializeFieldDefinition from '@common/form/core/initializeFieldDefinition'
+import CheckboxVariant from '@common/types/form/fields'
+import { FormKitExtendableSchemaRoot, FormKitNode } from '@formkit/core'
 import { checkbox as checkboxDefinition } from '@formkit/inputs'
+import { has } from '@formkit/utils'
 
-initializeFieldDefinition(checkboxDefinition)
+const addOptionCheckedDataAttribute = (node: FormKitNode) => {
+  const { props } = node
+
+  if (!props.definition) return
+
+  const definition = cloneDeep(props.definition)
+
+  const originalSchema = definition.schema as FormKitExtendableSchemaRoot
+
+  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,
+            },
+          },
+        },
+      },
+    }
+    return originalSchema(localExtensions)
+  }
+
+  props.definition = definition
+}
+
+const handleVariant = (node: FormKitNode) => {
+  const { props } = node
+
+  const setVariantClasses = (variant: CheckboxVariant) => {
+    if (CheckboxVariant.switch === variant) {
+      props.innerClass =
+        'bg-gray-300 relative inline-flex flex-shrink-0 h-6 w-10 border border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus-within:ring-1 focus-within:ring-white focus-within:ring-opacity-75 formkit-is-checked:bg-blue formkit-invalid:border-red formkit-invalid:border-solid'
+      props.decoratorClass =
+        'translate-x-0 pointer-events-none inline-block h-[22px] w-[22px] rounded-full bg-white shadow-lg transform ring-0 transition ease-in-out duration-200 peer-checked:translate-x-4'
+      props.inputClass = '$reset peer sr-only'
+    } else {
+      props.inputClass =
+        'h-4 w-4 border-[1.5px] border-white rounded-sm bg-transparent checked:focus:color-blue checked:bg-blue checked:border-blue checked:focus:bg-blue checked:hover:bg-blue'
+    }
+  }
+
+  node.on('created', () => {
+    if (!has(props, 'variant')) {
+      props.variant = CheckboxVariant.default
+    }
+
+    setVariantClasses(props.variant)
+
+    node.on('prop:variant', ({ payload }) => {
+      setVariantClasses(payload)
+    })
+  })
+}
+
+initializeFieldDefinition(checkboxDefinition, {
+  props: ['variant'],
+  features: [addOptionCheckedDataAttribute, handleVariant],
+})
 
 export default {
   fieldType: 'checkbox',

+ 6 - 1
app/frontend/common/form/plugins/global/addValuePopulatedDataAttribute.ts

@@ -2,6 +2,7 @@
 
 import { cloneDeep } from '@apollo/client/utilities'
 import { FormKitNode, FormKitExtendableSchemaRoot } from '@formkit/core'
+import { isEmpty } from 'lodash-es'
 
 const addValuePopulatedDataAttribute = (node: FormKitNode) => {
   const { props, context } = node
@@ -9,7 +10,11 @@ const addValuePopulatedDataAttribute = (node: FormKitNode) => {
   if (!props.definition || !context || node.type !== 'input') return
 
   // Adds a helper function to check the existing value inside of the context.
-  context.fns.hasValue = (value) => !!value
+  context.fns.hasValue = (value) => {
+    if (typeof value === 'object') return !isEmpty(value)
+
+    return !!value
+  }
 
   const definition = cloneDeep(props.definition)
 

+ 7 - 1
app/frontend/common/form/theme/global/index.ts

@@ -3,7 +3,7 @@
 import type { FormThemeClasses } from '@common/types/form'
 
 const defaultTextInput: Record<string, string> = {
-  input: 'block',
+  input: 'block focus:outline-none focus:ring-0',
 }
 
 const classes: FormThemeClasses = {
@@ -24,6 +24,12 @@ const classes: FormThemeClasses = {
   'datetime-local': defaultTextInput,
   textarea: defaultTextInput,
   password: defaultTextInput,
+  checkbox: {
+    wrapper: 'inline-flex items-center cursor-pointer',
+    inner: 'mr-2',
+    input:
+      'appearance-none focus:outline-none focus:ring-0 focus:ring-offset-0',
+  },
 }
 
 export default classes

+ 8 - 0
app/frontend/common/types/form/fields.ts

@@ -0,0 +1,8 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+enum CheckboxVariant {
+  'default' = 'default',
+  'switch' = 'switch',
+}
+
+export default CheckboxVariant

+ 98 - 0
app/frontend/stories/form/field/FieldCheckbox.stories.ts

@@ -0,0 +1,98 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+import { Story } from '@storybook/vue3'
+import { FormKit } from '@formkit/vue'
+import defaultArgTypes from '@/stories/support/form/field/defaultArgTypes'
+import { FieldArgs } from '@/stories/types/form'
+import CheckboxVariant from '@common/types/form/fields'
+
+export default {
+  title: 'Form/Field/Checkbox',
+  component: FormKit,
+  argTypes: {
+    ...defaultArgTypes,
+    variant: {
+      control: { type: 'select' },
+      table: {
+        defaultValue: {
+          summary: CheckboxVariant.default,
+        },
+      },
+      options: [CheckboxVariant.default, CheckboxVariant.switch],
+    },
+    options: {
+      name: 'options',
+      type: { name: 'array', required: false },
+      description:
+        'An object of value/label pairs or an array of strings, or an array of objects that must contain a label and value property.',
+      table: {
+        type: { summary: 'Array/Object' },
+        defaultValue: {
+          summary: '[]',
+        },
+      },
+      control: {
+        type: 'object',
+      },
+    },
+    onValue: {
+      name: 'onValue',
+      type: { name: 'string', required: false },
+      description:
+        'The value when the checkbox is checked (single checkboxes only).',
+      table: {
+        type: { summary: 'Boolean/String' },
+        defaultValue: {
+          summary: 'true',
+        },
+      },
+      control: {
+        type: 'text',
+      },
+    },
+    offValue: {
+      name: 'offValue',
+      type: { name: 'string', required: false },
+      description:
+        'The value when the checkbox is unchecked (single checkboxes only).',
+      table: {
+        type: { summary: 'Boolean/String' },
+        defaultValue: {
+          summary: 'false',
+        },
+      },
+      control: {
+        type: 'text',
+      },
+    },
+  },
+  parameters: {
+    docs: {
+      description: {
+        component:
+          '[FormKit Built-In - Password](https://formkit.com/inputs/checkbox) + Variant for switch',
+      },
+    },
+  },
+}
+
+const Template: Story<FieldArgs> = (args: FieldArgs) => ({
+  components: { FormKit },
+  setup() {
+    return { args }
+  },
+  template: '<FormKit type="checkbox" v-bind="args"/>',
+})
+
+export const Default = Template.bind({})
+Default.args = {
+  label: 'Checkbox',
+  name: 'checkbox',
+}
+
+export const VariantSwitch = Template.bind({})
+VariantSwitch.args = {
+  label: 'Checkbox',
+  name: 'checkbox',
+  variant: CheckboxVariant.switch,
+}

+ 1 - 1
app/frontend/stories/form/field/FieldPassword.stories.ts

@@ -48,7 +48,7 @@ export default {
     docs: {
       description: {
         component:
-          '[FormKit Built-In - Password](https://formkit.com/inputs/password)',
+          'Base: [FormKit Built-In - Password](https://formkit.com/inputs/password) + Show/Hide current value',
       },
     },
   },

+ 11 - 0
app/frontend/stories/support/form/field/defaultArgTypes.ts

@@ -56,6 +56,17 @@ const argTypes = {
       type: 'text',
     },
   },
+  disabled: {
+    name: 'disabled',
+    type: { name: 'boolean', required: false },
+    desciption: '',
+    table: {
+      type: { summary: 'false' },
+    },
+    control: {
+      type: 'boolean',
+    },
+  },
   config: {
     name: 'config',
     type: { name: 'object', required: false },

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