CSR-254: Implement final stages of form test

This commit is contained in:
Adam Caouette 2022-01-13 15:58:19 -05:00
parent 3c10f75b67
commit 349c59b7dd
5 changed files with 229 additions and 85 deletions

View file

@ -20,7 +20,8 @@
"vue": "^3.0.0", "vue": "^3.0.0",
"vue-router": "^4.0.11", "vue-router": "^4.0.11",
"vuex": "^4.0.2", "vuex": "^4.0.2",
"vuex-persistedstate": "^4.1.0" "vuex-persistedstate": "^4.1.0",
"yup": "^0.32.11"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-babel": "~4.5.0",

View file

@ -1,6 +1,6 @@
<!-- Simple implementation of an input field --> <!-- Simple implementation of an input field -->
<template> <template>
<div class="d-flex w-50 mb-2"> <div class="d-flex w-50 mb-2" :class="{ 'has-error': !!errorMessage }">
<input <input
type="text" type="text"
:name="name" :name="name"
@ -9,6 +9,7 @@
:aria-required="isRequired" :aria-required="isRequired"
@input="handleChange" @input="handleChange"
@blur="handleBlur" @blur="handleBlur"
:data-focus-target="name"
/> />
<label <label
:for="name" :for="name"

View file

@ -1,16 +1,26 @@
<template> <template>
<Form :validation-schema="schema" @submit="onSubmit" v-slot="{ values, meta }"> <Form
<div class="row px-3 mt-4"> :validation-schema="schema"
<!-- The role="radiogroup" and aria-labelledby must be included in the parent component for the group --> @submit="onSubmit"
<h3 class="mx-2 mt-4 mb-0">How many chips are we repairing?</h3> @invalid-submit="onInvalidSubmit"
<div role="radiogroup" aria-labelledby="demo-group" class="d-flex flex-row p-0"> v-slot="{ values, meta }"
<!-- The h3 and id must be included. The id must match the aria-labelledby of the parent div. --> >
<h3 class="sr-only" id="demo-group"> <div class="row g-2 mt-6 mx-4">
<h2 class="mx-2 mt-4 mb-0">FORM VALIDATION TEST</h2>
</div>
<div class="row g-2 mt-6 mx-4">
<h4 class="mx-2 mt-4 mb-0">How many chips are we repairing? (Required)</h4>
<div
role="radiogroup"
aria-labelledby="demo-group"
class="d-flex flex-row p-0"
>
<h4 class="sr-only" id="demo-group">
How many chips are we repairing? How many chips are we repairing?
</h3> </h4>
<listButtonHorizontal <listButtonHorizontal
groupName="demo" groupName="demo"
ariaLabelBy="vehicle-model"
buttonID="1" buttonID="1"
value="1" value="1"
buttonLabelSubCopy="" buttonLabelSubCopy=""
@ -18,13 +28,12 @@
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
v-bind:totalInGroup="3" :totalInGroup="3"
v-bind:positionInGroup="1" :positionInGroup="1"
screenReaderOnlyText=" opens new window" screenReaderOnlyText=" opens new window"
/> />
<listButtonHorizontal <listButtonHorizontal
groupName="demo" groupName="demo"
ariaLabelBy="vehicle-model"
buttonID="2" buttonID="2"
value="2" value="2"
buttonLabelSubCopy="" buttonLabelSubCopy=""
@ -32,13 +41,12 @@
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
v-bind:totalInGroup="3" :totalInGroup="3"
v-bind:positionInGroup="2" :positionInGroup="2"
screenReaderOnlyText=" opens new window" screenReaderOnlyText=" opens new window"
/> />
<listButtonHorizontal <listButtonHorizontal
groupName="demo" groupName="demo"
ariaLabelBy="vehicle-model"
buttonID="3" buttonID="3"
value="3" value="3"
buttonLabelSubCopy="" buttonLabelSubCopy=""
@ -46,8 +54,8 @@
loaderColor="blue" loaderColor="blue"
loaderPosition="right" loaderPosition="right"
sizeInRem="1" sizeInRem="1"
v-bind:totalInGroup="3" :totalInGroup="3"
v-bind:positionInGroup="3" :positionInGroup="3"
screenReaderOnlyText=" opens new window" screenReaderOnlyText=" opens new window"
/> />
</div> </div>
@ -56,9 +64,9 @@
</div> </div>
</div> </div>
<div class="row g-2 mt-4"> <div class="row g-2 mt-6 mx-4">
<div class="col"> <div class="col">
<h3 class="mx-2 mt-4 mb-0">Which windshield part needs fixed?</h3> <h4 class="mx-2 mt-4 mb-0">Which windshield part needs fixed? (Required)</h4>
<fieldset> <fieldset>
<legend class="sr-only">Which windshield part needs fixed?</legend> <legend class="sr-only">Which windshield part needs fixed?</legend>
<listCard <listCard
@ -95,9 +103,9 @@
</div> </div>
</div> </div>
<div class="row g-2 mt-4"> <div class="row g-2 mt-6 mx-4">
<div class="col"> <div class="col">
<h3 class="mx-2 mt-4 mb-0">Enter your VIN</h3> <h4 class="mx-2 mt-4 mb-0">demo3: Enter your VIN (Required)</h4>
<textInput <textInput
name="demo3" name="demo3"
type="text" type="text"
@ -105,26 +113,134 @@
:isRequired="true" :isRequired="true"
/> />
</div> </div>
<!-- Here, the error block is included in the component level textInput -->
</div> </div>
<div class="row g-2 mt-4"> <div class="row g-2 mt-6 mx-4">
<div class="col"> <div class="col">
<h3 class="mx-2 mt-4 mb-0">Optional field</h3> <h4 class="mx-2 mt-4 mb-0">demo4: Enter some numbers (optional)</h4>
<textInput <textInput
name="demo4" name="demo4"
type="text" type="text"
label="Optional field (Not Required)" label="Numbers Only"
:isRequired="true"
/> />
</div> </div>
<!-- Here, the error block is included in the component level textInput -->
</div> </div>
<div class="g-2 mx-2 mt-6"> <div class="row g-2 mt-6 mx-4">
<button :class="!meta.valid && 'form-test-invalid'">Get your estimate</button> <h4 class="mx-2 mt-4 mb-0">demo5: Where's your damage? (Required)</h4>
<p>Values:</p> <div
role="radiogroup"
aria-labelledby="demo-group"
class="d-flex flex-row p-0"
>
<h4 class="sr-only" id="demo-group">
Where's your damage?
</h4>
<listCard
isMultiSelect
buttonImage="windshield-damage.svg"
buttonLabel="Windshield"
value="Windshield"
altText=""
buttonID="List Card Checkbox a"
groupName="demo5"
/>
<listCard
isMultiSelect
buttonImage="side-window-damage.svg"
buttonLabel="Side Door"
value="Side Door"
altText=""
buttonID="List Card Checkbox b"
groupName="demo5"
/>
<listCard
isMultiSelect
buttonImage="back-glass-damage.svg"
buttonLabel="Rear Window"
value="Rear Window"
altText=""
buttonID="List Card Checkbox c"
groupName="demo5"
/>
</div>
</div>
<div class="row px-3 form-test-error">
<error-message name="demo5"></error-message>
</div>
<div class="row g-2 mt-6 mx-4">
<h4 class="mx-2 mt-4 mb-0">demo6: What's your windshield damage? (Required if demo5 "Windshield" is selected)</h4>
<div
role="radiogroup"
aria-labelledby="demo-group"
class="d-flex flex-row p-0"
>
<h4 class="sr-only" id="demo-group">
What's your windshield damage?
</h4>
<listCard
isRadio
buttonImage="windshield-damage.svg"
buttonLabel="Crack"
value="Crack"
buttonLabelSubCopy="My damage is larger than six inches."
altText=""
buttonID="Crack Checkbox a"
groupName="demo6"
/>
<listCard
isRadio
buttonImage="windshield-damage.svg"
buttonLabel="Chip(s)"
value="Chip"
buttonLabelSubCopy="I have three or fewer chips smaller than six inches."
altText=""
buttonID="Chip Checkbox b"
groupName="demo6"
/>
</div>
</div>
<div class="row px-3 form-test-error">
<error-message name="demo6"></error-message>
</div>
<div class="row g-2 mt-6 mx-4">
<div class="col">
<h4 class="mx-2 mt-4 mb-0">Password (Required, enter any string)</h4>
<textInput
name="password"
type="text"
label="Password (Required)"
:isRequired="true"
/>
</div>
</div>
<div class="row g-2 mt-6 mx-4">
<div class="col">
<h4 class="mx-2 mt-4 mb-0">Confirm Password (Required, must match above Password field)</h4>
<textInput
name="passwordConfirmation"
type="text"
label="Confirm Password (Required)"
:isRequired="true"
/>
</div>
</div>
<div class="row px-3 form-test-error" data-focus-target="demoX">
<error-message name="demoX"></error-message>
</div>
<div class="g-2 mt-6 mx-4">
<button class="mb-6" :class="!meta.valid && 'form-test-invalid'">Get your estimate</button>
<p>Current Form Values:</p>
<pre>{{ values }}</pre> <pre>{{ values }}</pre>
<p>Meta.valid:</p> <p>Is Form Valid? (Meta.valid):</p>
<pre>{{ meta.valid }}</pre> <pre>{{ meta.valid }}</pre>
</div> </div>
@ -134,71 +250,87 @@
<script> <script>
import { Form, ErrorMessage } from 'vee-validate'; import { Form, ErrorMessage } from 'vee-validate';
import * as Yup from "yup";
import buttonPrimary from "@/ux-components/button-primary/button-primary";
import buttonSecondary from "@/ux-components/button-secondary/button-secondary";
import buttonBack from "@/common-components/button-back/button-back";
import listCard from "@/ux-components/list-card/list-card"; import listCard from "@/ux-components/list-card/list-card";
import textInput from "@/common-components/text-input/text-input"; import textInput from "@/common-components/text-input/text-input";
import listButton from "@/ux-components/list-button/list-button";
import alert from "@/ux-components/alert/alert";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelHeader from "@/common-components/funnel-header/funnel-header";
import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-button-horizontal"; import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-button-horizontal";
import checkbox from "@/ux-components/checkbox/checkbox";
import { useField } from 'vee-validate';
export default { export default {
name: "App", name: "App",
data() { data() {
return { return {
schema: { // VALIDATION SETUP schema: Yup.object().shape({
demo: (value) => { demo: Yup.string().required('Please select chip(s)'),
if (value && value.length > 0) { demo2: Yup.array().required('Please select a windshield part'),
demo3: Yup.string().required('Invalid VIN. Please enter X, Y, Z...'),
demo4: Yup.string().matches('^[0-9]*$', 'Invalid Entry. Must enter only numbers, no letters.'),
demo5: Yup.array().required('Please select damage location'),
demo6: Yup.string()
.when('demo5', function (demo5, schema, value) {
if (demo5) {
const demo5String = demo5.toString();
if (demo5String.includes("Windshield")) {
// make demo6 required
return schema.required('You must pick an option for Windshield Damage.')
}
return schema;
}
return schema;
}),
demoX: Yup.string().notRequired(),
// // PASSWORD EXAMPLE, NOT BEING USED NOW
//password: Yup.string().min(6).required(),
//passwordConfirmation: Yup.string()
// .required()
// .oneOf([Yup.ref("password")], "Passwords do not match"),
}).test('repair-replace-conflict',
null, // need to pass null as error message so it won't get added to list of errors
function(value) {
// whole form test
console.log('ALL FORM VALUES: ', value);
// test problem scenario... return new error if conditions are true, allow submit if not
if (value.demo5 && value.demo6) {
const demo5String = value.demo5.toString();
console.log('demo5String: ', demo5String);
console.log('demo6: ', value.demo6);
if (demo5String.includes("Windshield") && value.demo5.length > 1 && value.demo6 == "Chip") {
return this.createError({ name: 'demoX', message: "Sorry, at this time we can't schedule a repair and replace at the same time. Please schedule these appointments separately as they are different technicians. Thanks.", path: 'demoX' });
}
return true; return true;
} }
return 'Please select chip(s)'; return true;
}, }),
demo2: (value) => {
if (value && value.length > 0) {
return true;
}
return 'Please select a windshield part';
},
demo3: (value) => {
if (value && value.length > 0) {
return true;
}
return 'Invalid VIN. Please enter X, Y, Z...';
},
} }
}
}, },
components: { components: {
// buttonPrimary,
// buttonSecondary,
// buttonBack,
listCard, listCard,
// listButton,
// alert,
// vehicleBanner,
listButtonHorizontal, listButtonHorizontal,
textInput, textInput,
Form, Form,
ErrorMessage, ErrorMessage,
// checkbox,
// funnelHeader,
}, },
methods: { methods: {
onSubmit(values) { onSubmit(values) {
window.alert('Success! Submitted values: ' + JSON.stringify(values, null ,2))
console.log('submitted: ', JSON.stringify(values, null ,2)); console.log('submitted: ', JSON.stringify(values, null ,2));
}, },
// onInvalidSubmit({ values, errors, results }) { onInvalidSubmit({ values, errors, results }) {
// console.log("values: ", values); // identify the first error field and put focus on it
// console.log("errors: ", errors); console.log("values: ", values);
// console.log("results: ", results); console.log("errors: ", errors);
// } console.log("results: ", results);
// get errorEls and order by alpha
const errorEls = Object.keys(errors).sort();
const firstErrorEl = errorEls[0];
console.log('firstErrorEl: ', firstErrorEl);
if (firstErrorEl) {
const qsString = "[data-focus-target='" + firstErrorEl + "']";
document.querySelector(qsString).focus();
}
}
}, },
}; };
</script> </script>
@ -211,4 +343,7 @@ export default {
.form-test-invalid { .form-test-invalid {
opacity: 0.5; opacity: 0.5;
} }
.has-error {
border: 1px solid red;
}
</style> </style>

View file

@ -1,14 +1,16 @@
<!-- See the component-test.vue page for example implementation --> <!-- See the component-test.vue page for example implementation -->
<template> <template>
<!-- IMPORTANT: Refrain from using more than 4 horizontal buttons on desktop, 3 on mobile. --> <!-- IMPORTANT: Refrain from using more than 4 horizontal buttons on desktop, 3 on mobile. -->
<div v-if="isMultiSelect" class="list-group list-button-horizontal d-flex flex-column w-100 mb-2"> <div
v-if="isMultiSelect"
class="list-group list-button-horizontal d-flex flex-column w-100 mb-2"
>
<input <input
type="checkbox" type="checkbox"
:id="buttonID" :id="buttonID"
:name="groupName" :name="groupName"
:value="buttonID" :value="buttonID"
:aria-required="isRequired" :aria-required="isRequired"
@keyup.space="handleClick(value)"
@click="handleClick(value)" @click="handleClick(value)"
/> />
<label <label
@ -18,21 +20,25 @@
:aria-labelledby="buttonID" :aria-labelledby="buttonID"
class="d-flex flex-column justify-content-center py-3 px-4" class="d-flex flex-column justify-content-center py-3 px-4"
:class="isFirstOrLastButton" :class="isFirstOrLastButton"
:data-focus-target="groupName"
> >
<span class="m-0" :class="[this.textPosition]">{{buttonID}}</span> <span class="m-0" :class="[this.textPosition]">{{buttonID}}</span>
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="[this.textPosition]">{{buttonLabelSubCopy}}</span> <span v-if="buttonLabelSubCopy" class="m-0 small" :class="[this.textPosition]">{{buttonLabelSubCopy}}</span>
<span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span> <span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span>
</label> </label>
</div> </div>
<div v-else class="col list-group list-button-horizontal d-flex flex-column mb-2"> <div
v-else
class="col list-group list-button-horizontal d-flex flex-column mb-2"
>
<input <input
type="radio" type="radio"
:id="buttonID" :id="buttonID"
:name="groupName" :name="groupName"
:value="buttonID" :value="buttonID"
aria-required="true" aria-required="true"
@keyup.space="handleClick(value)"
@click="handleClick(value)" @click="handleClick(value)"
:data-focus-target="groupName"
/> />
<label <label
tabindex="-1" tabindex="-1"
@ -108,13 +114,14 @@ export default {
setup(props) { setup(props) {
const { groupName, value } = toRefs(props); const { groupName, value } = toRefs(props);
const inputType = props.isMultiSelect ? "checkbox" : "radio"; const inputType = props.isMultiSelect ? "checkbox" : "radio";
const { checked, handleChange } = useField(groupName, undefined, { const { checked, handleChange, errorMessage } = useField(groupName, undefined, {
type: inputType, type: inputType,
checkedValue: value checkedValue: value
}); });
return { return {
checked, checked,
handleChange, handleChange,
errorMessage,
}; };
}, },
}; };
@ -125,8 +132,8 @@ export default {
input[type="radio"], input[type="radio"],
input[type="checkbox"] { input[type="checkbox"] {
opacity: 0; opacity: 0;
position: fixed;
width: 0; width: 0;
height: 0;
&:focus-visible + label { &:focus-visible + label {
box-shadow: 0 0 0 2px $blue; box-shadow: 0 0 0 2px $blue;
} }

View file

@ -7,8 +7,8 @@
:name="groupName" :name="groupName"
:value="buttonLabel" :value="buttonLabel"
:aria-required="isRequired" :aria-required="isRequired"
@keyup.space="handleChange(value)"
@click="handleChange(value)" @click="handleChange(value)"
:data-focus-target="groupName"
/> />
<label <label
:for="buttonID" :for="buttonID"
@ -27,8 +27,8 @@
:name="groupName" :name="groupName"
:value="buttonID" :value="buttonID"
:aria-required="isRequired" :aria-required="isRequired"
@keyup.space="handleChange(value)"
@click="handleChange(value)" @click="handleChange(value)"
:data-focus-target="groupName"
/> />
<label <label
:for="buttonID" :for="buttonID"
@ -47,8 +47,8 @@
:name="groupName" :name="groupName"
:value="buttonLabel" :value="buttonLabel"
:aria-required="isRequired" :aria-required="isRequired"
@keyup.space="handleChange(value)"
@click="handleChange(value)" @click="handleChange(value)"
:data-focus-target="groupName"
/> />
<label <label
:for="buttonID" :for="buttonID"
@ -70,8 +70,8 @@
:name="groupName" :name="groupName"
:value="buttonLabel" :value="buttonLabel"
:aria-required="isRequired" :aria-required="isRequired"
@keyup.space="handleChange(value)"
@click="handleChange(value)" @click="handleChange(value)"
:data-focus-target="groupName"
/> />
<label <label
:for="buttonID" :for="buttonID"
@ -159,8 +159,8 @@ export default {
input[type='checkbox'], input[type='checkbox'],
input[type="radio"] { input[type="radio"] {
opacity: 0; opacity: 0;
position: fixed;
width: 0; width: 0;
height: 0;
+ label { + label {
display: block; display: block;
position: relative; position: relative;