Browse Source

Feature: Mobile - Added the possibility to use a theme for the form field styling.

Dominik Klein 3 years ago
parent
commit
976807ed55

+ 18 - 2
.storybook/preview.ts

@@ -10,7 +10,12 @@ import initializeStore from '@common/stores'
 import initializeForm, { getFormPlugins } from '@common/form'
 import type { ImportGlobEagerOutput } from '@common/types/utils'
 import type { FormKitPlugin } from '@formkit/core'
+import getMobileCoreClasses from '@mobile/form/theme/global'
 import { createRouter, createWebHashHistory, type Router } from 'vue-router'
+import type {
+  FormFieldTypeImportModules,
+  FormThemeExtension,
+} from '@common/types/form'
 
 // Adds the translations to storybook.
 app.config.globalProperties.i18n = i18n
@@ -21,13 +26,24 @@ initializeStore(app)
 
 // Initialize the FormKit plugin witht he needed fields ands internal FormKit plugins.
 const mobilePluginModules: ImportGlobEagerOutput<FormKitPlugin> =
-  import.meta.globEager('../app/frontend/apps/mobile/form/plugins/*.ts')
+  import.meta.globEager('../app/frontend/apps/mobile/form/plugins/global/*.ts')
 const mobileFieldModules: ImportGlobEagerOutput<FormFieldTypeImportModules> =
   import.meta.globEager(
     '../app/frontend/apps/mobile/components/form/field/**/*.ts',
   )
+const themeExtensionModules: ImportGlobEagerOutput<FormThemeExtension> =
+  import.meta.globEager(
+    '../app/frontend/apps/mobile/form/theme/global/extensions/*.ts',
+  )
+
 const plugins = getFormPlugins(mobilePluginModules)
-initializeForm(app, mobileFieldModules, plugins)
+
+const appTheme = {
+  coreClasses: getMobileCoreClasses,
+  extensions: themeExtensionModules,
+}
+
+initializeForm(app, undefined, mobileFieldModules, plugins, appTheme)
 
 const router: Router = createRouter({
   history: createWebHashHistory(),

+ 10 - 2
app/frontend/apps/mobile/form/index.ts

@@ -3,21 +3,29 @@
 import mainInitializeForm, { getFormPlugins } from '@common/form'
 import type {
   FormFieldTypeImportModules,
+  FormThemeExtension,
   InitializeAppForm,
 } from '@common/types/form'
 import type { ImportGlobEagerOutput } from '@common/types/utils'
 import type { FormKitPlugin } from '@formkit/core'
+import getCoreClasses from '@mobile/form/theme/global'
 import { App } from 'vue'
 
 const pluginModules: ImportGlobEagerOutput<FormKitPlugin> =
-  import.meta.globEager('./plugins/*.ts')
+  import.meta.globEager('./plugins/global/*.ts')
 const fieldModules: ImportGlobEagerOutput<FormFieldTypeImportModules> =
   import.meta.globEager('../components/form/field/**/*.ts')
+const themeExtensionModules: ImportGlobEagerOutput<FormThemeExtension> =
+  import.meta.globEager('../theme/global/extensions/*.ts')
 
 const initializeForm: InitializeAppForm = (app: App) => {
   const plugins = getFormPlugins(pluginModules)
+  const theme = {
+    coreClasses: getCoreClasses,
+    extensions: themeExtensionModules,
+  }
 
-  mainInitializeForm(app, fieldModules, plugins)
+  mainInitializeForm(app, undefined, fieldModules, plugins, theme)
 }
 
 export default initializeForm

+ 0 - 0
app/frontend/apps/mobile/form/plugins/.keep → app/frontend/apps/mobile/form/plugins/global/.keep


+ 0 - 0
app/frontend/apps/mobile/form/theme/global/extensions/.keep


+ 32 - 0
app/frontend/apps/mobile/form/theme/global/index.ts

@@ -0,0 +1,32 @@
+// Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+import type { FormThemeClasses, FormThemeExtension } from '@common/types/form'
+
+type Classes = Record<string, string>
+
+const addFloatingLabel = (classes: Classes): Classes => {
+  return {
+    outer: `${classes.outer} floating-input`,
+    wrapper: `${classes.wrapper} relative`,
+    input: `${classes.input} w-full h-14 text-sm bg-gray-500 rounded-xl border-none focus:outline-none placeholder:text-transparent focus-within:pt-8 formkit-populated:pt-8`,
+    label: `${classes.label} absolute top-0 left-0 py-5 px-3 h-full text-base transition-all duration-100 ease-in-out origin-left pointer-events-none formkit-populated:-translate-y3 formkit-populated:translate-x-1 formkit-populated:scale-75 formkit-populated:opacity-75`,
+  }
+}
+
+const getCoreClasses: FormThemeExtension = (classes: FormThemeClasses) => {
+  return {
+    global: {},
+    text: addFloatingLabel(classes.text),
+    email: addFloatingLabel(classes.email),
+    number: addFloatingLabel(classes.number),
+    search: addFloatingLabel(classes.search),
+    tel: addFloatingLabel(classes.tel),
+    time: addFloatingLabel(classes.time),
+    date: addFloatingLabel(classes.date),
+    'datetime-local': addFloatingLabel(classes['datetime-local']),
+    textarea: addFloatingLabel(classes.textarea),
+    password: addFloatingLabel(classes.password),
+  }
+}
+
+export default getCoreClasses

+ 1 - 1
app/frontend/apps/mobile/main.ts

@@ -9,7 +9,7 @@ import {
 } from '@vue/apollo-composable'
 import apolloClient from '@common/server/apollo/client'
 import useSessionIdStore from '@common/stores/session/id'
-import '@common/styles/main.css'
+import '@mobile/styles/main.css'
 import initializeStore from '@common/stores'
 import initializeStoreSubscriptions from '@common/initializer/storeSubscriptions'
 import initializeRouter from '@common/router/index'

+ 8 - 0
app/frontend/apps/mobile/styles/main.css

@@ -0,0 +1,8 @@
+@import '@common/styles/main.css';
+
+@layer components {
+  .floating-input:focus-within label,
+  .floating-input.formkit-outer[data-populated] label {
+    @apply -translate-y-3 translate-x-1 scale-75 opacity-75;
+  }
+}

+ 10 - 46
app/frontend/apps/mobile/views/Login.vue

@@ -15,7 +15,7 @@
           <template v-if="applicationConfig.value.maintenance_login">
             <!-- eslint-disable vue/no-v-html -->
             <div
-              class="my-1 flex items-center rounded bg-green py-2 px-4 text-white"
+              class="my-1 flex items-center rounded-xl bg-green py-2 px-4 text-white"
               v-html="applicationConfig.value.maintenance_login_message"
             ></div>
           </template>
@@ -35,7 +35,7 @@
               </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 select-none"
+                input-class="py-2 px-4 w-full h-14 text-xl font-semibold text-black bg-yellow rounded-xl select-none"
                 type="submit"
               >
                 {{ i18n.t('Sign in') }}
@@ -80,8 +80,8 @@ import { NotificationTypes } from '@common/types/notification'
 import CommonLogo from '@common/components/common/CommonLogo.vue'
 import useApplicationConfigStore from '@common/stores/application/config'
 import { i18n } from '@common/utils/i18n'
-import { FormKitGroupValue } from '@formkit/core'
 import Form from '@common/components/form/Form.vue'
+import { FormData } from '@common/types/form'
 
 interface Props {
   invalidatedSession?: string
@@ -104,30 +104,12 @@ const authentication = useAuthenticationStore()
 
 const router = useRouter()
 
-const forFloatingLabel = {
-  wrapper: {
-    attrs: {
-      'data-has-value': {
-        if: '$_value != "" && $fns.string($_value) !== "undefined"',
-        then: 'true',
-        else: undefined,
-      },
-    },
-  },
-}
-
 const formSchema = [
   {
     type: 'text',
     name: 'login',
     label: __('Username / Email'),
     placeholder: __('Username / Email'),
-    wrapperClass: 'relative floating-input',
-    inputClass:
-      'block mt-1 w-full h-14 text-sm bg-gray-500 rounded border-none focus:outline-none placeholder:text-transparent',
-    labelClass:
-      'absolute top-0 left-0 py-5 px-3 h-full text-base transition-all duration-100 ease-in-out origin-left pointer-events-none',
-    sectionsSchema: forFloatingLabel,
     validation: 'required',
   },
   {
@@ -135,19 +117,13 @@ const formSchema = [
     label: __('Password'),
     name: 'password',
     placeholder: __('Password'),
-    wrapperClass: 'relative floating-input',
-    inputClass:
-      'block mt-1 w-full h-14 text-sm bg-gray-500 rounded border-none focus:outline-none placeholder:text-transparent',
-    labelClass:
-      'absolute top-0 left-0 py-5 px-3 h-full text-base transition-all duration-100 ease-in-out origin-left pointer-events-none',
-    sectionsSchema: forFloatingLabel,
     validation: 'required',
   },
   {
     isLayout: true,
     element: 'div',
     attrs: {
-      class: 'mt-2 flex grow items-center justify-between text-white',
+      class: 'mt-2.5 flex grow items-center justify-between text-white',
     },
     children: [
       {
@@ -172,15 +148,15 @@ const formSchema = [
   },
 ]
 
-interface FormData {
-  login: string
-  password: string
+interface LoginFormData {
+  login?: string
+  password?: string
+  remember_me?: boolean
 }
 
-const login = (formData: FormKitGroupValue): void => {
-  const data = formData as unknown as FormData
+const login = (formData: FormData<LoginFormData>): void => {
   authentication
-    .login(data.login, data.password)
+    .login(formData.login as string, formData.password as string)
     .then(() => {
       router.replace('/')
     })
@@ -195,15 +171,3 @@ const login = (formData: FormKitGroupValue): void => {
 
 const applicationConfig = useApplicationConfigStore()
 </script>
-
-<style lang="postcss">
-.floating-input > .formkit-inner > input:focus,
-.floating-input > .formkit-inner > input:not(:placeholder-shown) {
-  @apply pt-8;
-}
-
-.floating-input:focus-within > label,
-.floating-input[data-has-value] > label {
-  @apply -translate-y-3 translate-x-1 scale-75 opacity-75;
-}
-</style>

+ 12 - 4
app/frontend/common/components/form/Form.vue

@@ -4,7 +4,6 @@
   <FormKit
     v-if="Object.keys(schemaData.fields).length > 0 || $slots.fields"
     v-bind:id="formId"
-    v-model="values"
     type="form"
     v-bind:config="formConfig"
     v-bind:form-class="localClass"
@@ -60,6 +59,7 @@ import type {
   FormKitClasses,
   FormKitSchemaDOMNode,
   FormKitSchemaComponent,
+  FormKitFrameworkContext,
 } from '@formkit/core'
 import getUuid from '@common/utils/getUuid'
 
@@ -97,7 +97,7 @@ const props = withDefaults(defineProps<Props>(), {
     return {}
   },
   staticSchema: false,
-  validationVisibility: FormValidationVisibility.blur,
+  validationVisibility: FormValidationVisibility.submit,
 })
 
 // Rename prop 'class' for usage in the template, because of reserved word
@@ -109,12 +109,22 @@ const emit = defineEmits<{
 }>()
 
 let formNode: FormKitNode
+const formNodeContext = ref<FormKitFrameworkContext | undefined>(undefined)
 const setFormNode = (node: FormKitNode) => {
   formNode = node
+  formNodeContext.value = formNode.context
 
   // TODO: maybe we should also emit the node one level above to have the node available without a own getNode-call...
 }
 
+// Use the node context value, instead of the v-model, because of performance reason.
+const values = computed<FormValues>(() => {
+  if (!formNodeContext.value) {
+    return {}
+  }
+  return formNodeContext.value.value
+})
+
 const onSubmit = (values: FormKitGroupValue) => {
   const emitValues = {
     ...values,
@@ -145,8 +155,6 @@ const additionalComponentLibrary = {
   FormLayout: markRaw(FormLayout) as ConcreteComponent,
 }
 
-const values = ref<FormValues>({})
-
 // Define the static schema, which will be filled with the real fields from the `schemaData`.
 const staticSchema: FormKitSchemaNode[] = []
 

+ 1 - 2
app/frontend/common/components/form/field/FieldPassword.ts

@@ -27,7 +27,6 @@ const switchPasswordVisibility = (node: FormKitNode) => {
           {
             $cmp: 'CommonIcon',
             props: {
-              // TODO: we need to add the new icon from figma.
               name: '$passwordVisibilityIcon',
               key: node.name,
               class: 'absolute top-1/2 transform -translate-y-1/2 right-3',
@@ -47,7 +46,7 @@ const switchPasswordVisibility = (node: FormKitNode) => {
 
   node.on('prop:type', ({ payload, origin }) => {
     const { props } = origin
-    props.passwordVisibilityIcon = payload === 'password' ? 'eye' : 'cog' // TODO: align icon name to eye-off?
+    props.passwordVisibilityIcon = payload === 'password' ? 'eye' : 'eye-off'
   })
 }
 

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