Merge branch 'develop' into feature/SSR-233

This commit is contained in:
Kulbhushan Kaushik 2023-01-25 13:04:44 -05:00
commit 4341ed913b
13 changed files with 549 additions and 452 deletions

View file

@ -1,76 +1,79 @@
<!-- Documented in confluence https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Component --> <!-- Documented in confluence https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Component -->
<template> <template>
<div <div
:class=" :class="
isOverflowScrollable ? 'button-question button-question-overflow' : 'button-question' isOverflowScrollable ? 'button-question button-question-overflow' : 'button-question'
"> ">
<div v-if="questionText && answers && answers.length > 0" class="question-text d-flex"> <div v-if="questionText && answers && answers.length > 0" class="question-text d-flex">
<span class="fw-bold w-100">{{ questionText }}</span> <span class="fw-bold w-100">{{ questionText }}</span>
</div> </div>
<div class="w-100 d-flex justify-content-center"> <div class="w-100 d-flex justify-content-center">
<fieldset <fieldset
class="w-100" class="w-100"
:aria-required="isRequired" :aria-required="isRequired"
:class="getFieldSetClasses" :class="getFieldSetClasses"
:role="isMultiSelect ? 'group' : 'radiogroup'" :role="isMultiSelect ? 'group' : 'radiogroup'"
:aria-labelledby="formatString(groupName)"> :aria-labelledby="formatString(groupName)">
<legend <legend
class="sr-only" class="sr-only"
:data-focus-target="formatString(groupName)" :data-focus-target="formatString(groupName)"
tabindex="-1" tabindex="-1"
:id="formatString(groupName)"> :id="formatString(groupName)">
{{ questionText }} {{ questionText }}
{{ {{
isMultiSelect && answers && answers.length > 1 isMultiSelect && answers && answers.length > 1
? "Select one or more options below." ? "Select one or more options below."
: "Select an option below." : "Select an option below."
}} }}
</legend> </legend>
<div :class="getComponentLoopWrapperClasses"> <div :class="getComponentLoopWrapperClasses">
<div <div
:class="getComponentWrapperClasses" :class="getComponentWrapperClasses"
v-for="answer in buttonsInfo" v-for="answer in buttonsInfo"
:key="answer.value ? answer.value : answer"> :key="answer.value ? answer.value : answer">
<component <component
:is="buttonTypeString" :is="buttonTypeString"
:buttonLabel="answer.buttonLabel" :buttonLabel="answer.buttonLabel"
:buttonLabelSubCopy="answer.buttonLabelSubCopy" :buttonLabelSubCopy="answer.buttonLabelSubCopy"
:buttonBodyCopy="answer.buttonBodyCopy" :buttonBodyCopy="answer.buttonBodyCopy"
:buttonAuxillaryCopy="answer.buttonAuxillaryCopy" :buttonAuxillaryCopy="answer.buttonAuxillaryCopy"
:buttonFooterCopy="answer.buttonFooterCopy" :buttonFooterCopy="answer.buttonFooterCopy"
:buttonImage="answer.buttonImage" :buttonImage="answer.buttonImage"
:buttonImageId="answer.buttonImageId" :buttonImageId="answer.buttonImageId"
:groupName="answer.groupName" :groupName="answer.groupName"
:isMultiSelect="isMultiSelect" :isMultiSelect="isMultiSelect"
:value="answer.value" :value="answer.value"
:selectingInitiatesLoad="selectingInitiatesLoad" :selectingInitiatesLoad="selectingInitiatesLoad"
:isWide="isWide" :isWide="isWide"
:validationRules="validationRules" :validationRules="validationRules"
:textPosition="textPosition" :textPosition="textPosition"
:additionalButtonStyling="additionalButtonStyling" :additionalButtonStyling="additionalButtonStyling"
:lastValuePushedToGa="lastValuePushedToGa" :lastValuePushedToGa="lastValuePushedToGa"
:setLastValuePushedToGa="setLastValuePushedToGa" :setLastValuePushedToGa="setLastValuePushedToGa"
:suppressError="suppressError" :suppressError="suppressError"
v-model="selectedValues" /> v-model="selectedValues" />
<!-- For nested questions --> <!-- For nested questions -->
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<div <div
v-if=" v-if="
typeof selectedValues == 'string' && typeof selectedValues == 'string' &&
selectedValues == answer.value selectedValues == answer.value
"> ">
<slot></slot> <slot></slot>
</div> </div>
</transition> </transition>
</div> </div>
</div> </div>
</fieldset> </fieldset>
</div> </div>
<div class="row form-test-error mt-1"> <div class="row form-test-error mt-1">
<error-message :name="formatString(groupName)" v-if="!suppressError"></error-message> <error-message
</div> class="small"
</div> :name="formatString(groupName)"
v-if="!suppressError"></error-message>
</div>
</div>
</template> </template>
<script> <script>
@ -81,195 +84,193 @@ import radio from "@/ux-components/radio/radio";
import { ErrorMessage } from "vee-validate"; import { ErrorMessage } from "vee-validate";
export default { export default {
name: "buttonQuestion", name: "buttonQuestion",
props: { props: {
buttonTypeString: { buttonTypeString: {
type: String, type: String,
default: "listButton", default: "listButton",
}, },
buttonTypeObject: { buttonTypeObject: {
type: Object, type: Object,
default: null, default: null,
}, },
isMultiSelect: Boolean, isMultiSelect: Boolean,
groupName: String, groupName: String,
questionText: String, questionText: String,
answers: Array, answers: Array,
textPosition: { textPosition: {
type: String, type: String,
default: "text-center", default: "text-center",
}, },
selectingInitiatesLoad: Boolean, selectingInitiatesLoad: Boolean,
loaderColor: { loaderColor: {
type: String, type: String,
default: "blue", default: "blue",
}, },
loaderPosition: { loaderPosition: {
type: String, type: String,
default: "right", default: "right",
}, },
isRequired: Boolean, isRequired: Boolean,
isOverflowScrollable: Boolean, isOverflowScrollable: Boolean,
isWide: Boolean, isWide: Boolean,
isCashOrInsurance: Boolean, isCashOrInsurance: Boolean,
modelValue: [Array, Number, String], modelValue: [Array, Number, String],
value: [Number, String], value: [Number, String],
validationRules: String, validationRules: String,
suppressError: Boolean, suppressError: Boolean,
useTextForValue: Boolean, useTextForValue: Boolean,
valueToLogType: String, valueToLogType: String,
additionalButtonStyling: String, additionalButtonStyling: String,
}, },
beforeMount() { beforeMount() {
if (this.buttonTypeObject) { if (this.buttonTypeObject) {
this.$options.components[this.buttonTypeString] = this.buttonTypeObject; this.$options.components[this.buttonTypeString] = this.buttonTypeObject;
}
},
data() {
return {
lastValuePushedToGa: null,
};
},
computed: {
getFieldSetClasses() {
if (this.isOverflowScrollable) {
return "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0";
} }
else if (this.buttonTypeString === "listCard") { },
return "w-100"; data() {
} return {
else { lastValuePushedToGa: null,
return ""; };
} },
}, computed: {
getComponentLoopWrapperClasses() { getFieldSetClasses() {
let classes; if (this.isOverflowScrollable) {
switch (this.buttonTypeString) { return "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0";
case "listButton": } else if (this.buttonTypeString == "listCard") {
classes = "w-100"; return "w-100";
break; } else {
case "listButtonHorizontal": return "";
classes = "d-flex flex-row p-0"; }
break; },
case "listCard": getComponentLoopWrapperClasses() {
classes = "row g-2 justify-content-center"; let classes;
if (this.isWide) { switch (this.buttonTypeString) {
classes += " flex-column"; case "listButton":
} classes = "w-100";
break; break;
case "radio": case "listButtonHorizontal":
classes = "ui-radio d-flex"; classes = "d-flex flex-row p-0";
break; break;
case "servicePackageRadio": case "listCard":
classes = "package-main"; classes = "row g-2 justify-content-center";
break; if (this.isWide) {
} classes += " flex-column";
return classes; }
}, break;
getComponentWrapperClasses() { case "radio":
let classes = ""; classes = "ui-radio d-flex";
break;
case "servicePackageRadio":
classes = "package-main";
break;
}
return classes;
},
getComponentWrapperClasses() {
let classes = "";
classes += this.isWide ? "col-12" : "col"; classes += this.isWide ? "col-12" : "col";
if (this.buttonTypeString == "radio") { if (this.buttonTypeString == "radio") {
classes += " radio-button-container"; classes += " radio-button-container";
} else if (this.buttonTypeString == "servicePackageRadio") { } else if (this.buttonTypeString == "servicePackageRadio") {
classes = "package-wrapper"; classes = "package-wrapper";
} }
return classes; return classes;
}, },
buttonsInfo() { buttonsInfo() {
return (Array.isArray(this.answers) ? this.answers : [])?.map((answer) => ({ return (Array.isArray(this.answers) ? this.answers : [])?.map((answer) => ({
buttonLabel: answer.buttonLabel ?? answer.Text ?? answer, buttonLabel: answer.buttonLabel ?? answer.Text ?? answer,
altText: answer.altText ?? (answer.Name ? answer.Name : answer), altText: answer.altText ?? (answer.Name ? answer.Name : answer),
buttonLabelSubCopy: answer.buttonLabelSubCopy ?? answer.SubText, buttonLabelSubCopy: answer.buttonLabelSubCopy ?? answer.SubText,
buttonBodyCopy: answer.buttonBodyCopy ?? answer.buttonBodyCopy, buttonBodyCopy: answer.buttonBodyCopy ?? answer.buttonBodyCopy,
buttonAuxillaryCopy: answer.buttonAuxillaryCopy ?? answer.buttonAuxillaryCopy, buttonAuxillaryCopy: answer.buttonAuxillaryCopy ?? answer.buttonAuxillaryCopy,
buttonFooterCopy: answer.buttonFooterCopy ?? answer.buttonFooterCopy, buttonFooterCopy: answer.buttonFooterCopy ?? answer.buttonFooterCopy,
buttonImage: answer.buttonImage ?? answer.AnswerImageUrl, buttonImage: answer.buttonImage ?? answer.AnswerImageUrl,
buttonImageId: answer.buttonImageId ?? answer.ImageId, buttonImageId: answer.buttonImageId ?? answer.ImageId,
groupName: this.formatString(this.groupName), groupName: this.formatString(this.groupName),
value: value:
answer.value ?? answer.value ??
(this.useTextForValue && answer.Text ? answer.Text : answer.Name) ?? (this.useTextForValue && answer.Text ? answer.Text : answer.Name) ??
(typeof answer !== "object" ? answer : null), (typeof answer !== "object" ? answer : null),
})); }));
}, },
selectedValues: { selectedValues: {
get() { get() {
return this.modelValue; return this.modelValue;
}, },
set(selectedAnswers) { set(selectedAnswers) {
this.$emit("update:modelValue", selectedAnswers); this.$emit("update:modelValue", selectedAnswers);
}, },
}, },
}, },
methods: { methods: {
formatString(str) { formatString(str) {
return String(str).replaceAll(" ", "-"); return String(str).replaceAll(" ", "-");
}, },
setLastValuePushedToGa(lastValuePushedToGa) { setLastValuePushedToGa(lastValuePushedToGa) {
this.lastValuePushedToGa = lastValuePushedToGa; this.lastValuePushedToGa = lastValuePushedToGa;
}, },
}, },
components: { components: {
listButton, listButton,
listButtonHorizontal, listButtonHorizontal,
listCard, listCard,
ErrorMessage, ErrorMessage,
radio, radio,
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.button-question-overflow { .button-question-overflow {
height: calc(100vh - 274px); height: calc(100vh - 274px);
.overflow-scroll { .overflow-scroll {
// Height will be determined by overall height of content above list // Height will be determined by overall height of content above list
height: calc(100% - 314px); height: calc(100% - 314px);
overflow-x: hidden !important; overflow-x: hidden !important;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
} }
.button-question { .button-question {
color: $black; color: $black;
.radio-button-container { .radio-button-container {
&:not(:last-child) { &:not(:last-child) {
padding-bottom: map-get($spacers, 2); padding-bottom: map-get($spacers, 2);
} }
} }
} }
.question-text { .question-text {
margin-top: 1.5rem; margin-top: 1.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
font-size: 1rem; font-size: 1rem;
line-height: 1.625rem; line-height: 1.625rem;
& > span { & > span {
text-align: center; text-align: center;
} }
} }
.vehicle-parts { .vehicle-parts {
.question-text { .question-text {
span { span {
font-size: 0.875rem; font-size: 0.875rem;
text-align: left; text-align: left;
margin: 0 0 0.5rem 0; margin: 0 0 0.5rem 0;
} }
} }
.question-text { .question-text {
margin: 0; margin: 0;
} }
fieldset { fieldset {
.ui-radio { .ui-radio {
margin: 0; margin: 0;
} }
} }
} }
.welcome { .welcome {

View file

@ -1,159 +1,164 @@
<template> <template>
<div class="dropdown-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''"> <div class="dropdown-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
<label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label> <label
<select v-model="selectedOption" :for="inputId"
class="form-select" :aria-label="questionText"
:id="inputId" class="form-label"
:name="inputId" v-html="labelText"></label>
:aria-disabled="isDisabled" <select
:disabled="isDisabled" v-model="selectedOption"
:aria-required="isRequired" class="form-select"
:validationRules="validationRules" :id="inputId"
:placeHolderText="placeHolderText" :name="inputId"
> :aria-disabled="isDisabled"
<option v-if="placeHolderText" value="" selected>{{ placeHolderText }}</option> :disabled="isDisabled"
<option v-for="(value, name, index) in options" :value="name" :key="index"> :aria-required="isRequired"
{{ value }} :validationRules="validationRules"
</option> :placeHolderText="placeHolderText">
</select> <option v-if="placeHolderText" value="" selected>{{ placeHolderText }}</option>
<div v-show="errorMessage" class="row mt-2 form-test-error"> <option v-for="(value, name, index) in options" :value="name" :key="index">
<span role="alert">{{ errorMessage }}</span> {{ value }}
</div> </option>
</div> </select>
<div v-show="errorMessage" class="row mt-2 form-test-error">
<span role="alert">{{ errorMessage }}</span>
</div>
</div>
</template> </template>
<script> <script>
import { useField } from "vee-validate"; import { useField } from "vee-validate";
export default { export default {
name: "dropdown-question", name: "dropdown-question",
props: { props: {
modelValue: String, modelValue: String,
inputId: String, inputId: String,
options: { options: {
type: Object, type: Object,
required: true required: true,
}, },
isDisabled: Boolean, isDisabled: Boolean,
isRequired: Boolean, isRequired: Boolean,
disableAutoFill: Boolean, disableAutoFill: Boolean,
validationRules: String, validationRules: String,
cmsWidgetName: String, cmsWidgetName: String,
hasError: Boolean, hasError: Boolean,
errors: Array, errors: Array,
placeHolderText: String placeHolderText: String
}, },
setup(props) { setup(props) {
const propsClone = Object.assign({}, props); const propsClone = Object.assign({}, props);
const modelValue = propsClone.modelValue; const modelValue = propsClone.modelValue;
let initialValue; let initialValue;
switch (typeof modelValue) { switch (typeof modelValue) {
case "number": case "number":
initialValue = modelValue; initialValue = modelValue;
break; break;
default: default:
initialValue = (modelValue && modelValue.length > 0) ? modelValue : ""; initialValue = (modelValue && modelValue.length > 0) ? modelValue : "";
break; break;
} }
const fieldOptions = { const fieldOptions = {
type: "select", type: "select",
value: props.modelValue, value: props.modelValue,
initialValue: initialValue, initialValue: initialValue,
}; };
const { const {errorMessage, handleBlur, handleChange, meta, errors } = useField(
errorMessage, props.inputId,
handleBlur, props.validationRules,
handleChange, fieldOptions
meta, );
errors
} = useField(props.inputId, props.validationRules, fieldOptions);
return { return {
errorMessage, errorMessage,
handleBlur, handleBlur,
handleChange, handleChange,
meta, meta,
errors errors
}; };
}, },
computed: { computed: {
questionText(){ questionText() {
return this.getCmsContent(this.cmsWidgetName, 'QuestionText'); return this.getCmsContent(this.cmsWidgetName, "QuestionText");
}, },
selectedOption: { selectedOption: {
get: function() { get: function () {
return !this.modelValue ? "" : this.modelValue; return !this.modelValue ? "" : this.modelValue;
}, },
set: function(newValue) { set: function (newValue) {
this.$emit("update:modelValue", newValue); this.$emit("update:modelValue", newValue);
} },
}, },
labelText: { labelText: {
get: function () { get: function () {
const noBreakChar = "&NoBreak;"; const noBreakChar = "&NoBreak;";
var questionText = ""; var questionText = "";
if (this.disableAutoFill) { if (this.disableAutoFill) {
var words = this.questionText.toString().split(/[ ]+/); var words = this.questionText.toString().split(/[ ]+/);
words.forEach(function (word) { words.forEach(function (word) {
const position = 1; const position = 1;
word = [word.toString().slice(0, position), noBreakChar, word.toString().slice(position)].join(''); word = [
questionText += `${word} `; word.toString().slice(0, position),
}); noBreakChar,
word.toString().slice(position),
questionText = questionText.trimEnd(); ].join("");
} else { questionText += `${word} `;
questionText = this.questionText.toString(); });
}
return questionText; questionText = questionText.trimEnd();
} } else {
} questionText = this.questionText.toString();
}, }
watch: {
selectedOption(newValue) { return questionText;
this.handleChange(newValue); },
} },
} },
watch: {
selectedOption(newValue) {
this.handleChange(newValue);
},
},
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.dropdown-question { .dropdown-question {
label { label {
color: $black; color: $black;
font-weight: 500; font-weight: 500;
} }
.form-label { .form-label {
margin-bottom: .25rem; margin-bottom: 0.25rem;
} }
.form-select { .form-select {
color: $gray-600; color: $gray-600;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%231474a2'/%3e%3c/svg%3e"); background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%231474a2'/%3e%3c/svg%3e");
border: 1px solid $gray-500; border: 1px solid $gray-500;
border-radius: .5rem; border-radius: 0.5rem;
min-height: 3rem; min-height: 3rem;
&:focus, &:focus,
&:focus-visible { &:focus-visible {
box-shadow: 0 0 0 2.5px $blue; box-shadow: 0 0 0 2.5px $blue;
} }
&:disabled, &:disabled,
&.disabled { &.disabled {
background-color: $gray-100; background-color: $gray-100;
filter: grayscale(100%); filter: grayscale(100%);
&:hover { &:hover {
box-shadow: 0 0 0 4px transparent; box-shadow: 0 0 0 4px transparent;
border: 1px solid $gray-500; border: 1px solid $gray-500;
} }
} }
&:hover { &:hover {
border: 1px solid $gray-500; border: 1px solid $gray-500;
box-shadow: 0 0 0 4px $blue-300; box-shadow: 0 0 0 4px $blue-300;
} }
} }
} }
</style> </style>

View file

@ -3,7 +3,7 @@
<div <div
class="modal fade modal-component" class="modal fade modal-component"
v-on="{ 'hidden.bs.modal': resetButtonStyle }" v-on="{ 'hidden.bs.modal': resetButtonStyle }"
:id="this.cmsWidgetName" :id="cmsWidgetName"
tabindex="-1" tabindex="-1"
aria-labelledby="ModalComponentLabel" aria-labelledby="ModalComponentLabel"
aria-hidden="true"> aria-hidden="true">
@ -17,13 +17,18 @@
aria-label="Close"></button> aria-label="Close"></button>
</div> </div>
<div class="modal-body ps-4 pe-4 pt-5 pb-4"> <div class="modal-body ps-4 pe-4 pt-5 pb-4">
<img :src="this.ModalImage" class="mw-100 d-flex mx-auto mb-4" alt="" /> <img :src="ModalImage" class="mw-100 d-flex mx-auto mb-4" alt="" />
<h5 class="mb-4" v-html="this.ModalHeadline"></h5> <h5 class="mb-4" v-html="ModalHeadline"></h5>
<p class="fw-bold mb-2 subheader-text" v-html="this.ModalSubheadertext"></p> <p class="fw-bold mb-2 subheader-text" v-html="ModalSubheadertext"></p>
<p class="mb-0" v-html="this.ModalBodyText"></p> <p class="mb-0" v-html="ModalBodyText"></p>
<p
class="my-4 caption modal-sub-body"
v-if="ModalSubBodyText"
v-html="ModalSubBodyText"></p>
</div> </div>
<div class="modal-footer px-5 py-4"> <div class="modal-footer px-5 py-4">
<buttonMain <buttonMain
isPrimary
class="w-100" class="w-100"
ref="buttonMain" ref="buttonMain"
suppressLoader suppressLoader
@ -54,6 +59,9 @@ export default {
ModalBodyText() { ModalBodyText() {
return this.getCmsContent(this.cmsWidgetName, "BodyText"); return this.getCmsContent(this.cmsWidgetName, "BodyText");
}, },
ModalSubBodyText() {
return this.getCmsContent(this.cmsWidgetName, "BodyText2");
},
ModalImage() { ModalImage() {
return this.getCmsContent(this.cmsWidgetName, "Image"); return this.getCmsContent(this.cmsWidgetName, "Image");
}, },
@ -97,6 +105,9 @@ export default {
box-shadow: 0px 16px 48px -16px rgba(0, 0, 0, 0.25); box-shadow: 0px 16px 48px -16px rgba(0, 0, 0, 0.25);
border-radius: 1.5rem 1.5rem 0 0; border-radius: 1.5rem 1.5rem 0 0;
.modal-body { .modal-body {
.modal-sub-body {
color: $gray-600;
}
ul { ul {
margin-bottom: 0; margin-bottom: 0;
} }
@ -130,4 +141,4 @@ body {
} }
} }
} }
</style> </style>

View file

@ -1,6 +1,6 @@
<template> <template>
<div <div
class="text-block d-flex w-100 mt-2" class="text-block w-100 mt-2"
:class="[justifyText, typeStyle, fontWeight]" :class="[justifyText, typeStyle, fontWeight]"
v-html="this.TextBlockCopy"></div> v-html="this.TextBlockCopy"></div>
</template> </template>

View file

@ -44,8 +44,8 @@
<button v-if="includeSelectIcon" type="submit" data-bs-toggle="modal" <button v-if="includeSelectIcon" type="submit" data-bs-toggle="modal"
:data-bs-target="'#' + this.cmsWidgetName" aria-label="Select button" /> :data-bs-target="'#' + this.cmsWidgetName" aria-label="Select button" />
</div> </div>
<div v-show="errorMessage" class="row my-2 form-test-error mb-0"> <div v-show="errorMessage" class="row my-1 form-test-error">
<span class="d-inline-flex mt-0" role="alert">{{ errorMessage }}</span> <span class="d-inline-flex small mt-0" role="alert">{{ errorMessage }}</span>
</div> </div>
</div> </div>
</template> </template>

View file

@ -15,7 +15,7 @@ describe("address-questions.vue", () => {
}); });
describe("initial state", () => { describe("initial state", () => {
test("Should render addressQuestions sub-components (textbox-questions and dropdown-questions)", async () => { test("Should hide addressQuestions sub-components (textbox-questions and dropdown-questions)", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
@ -24,17 +24,16 @@ describe("address-questions.vue", () => {
const city = wrapper.findComponent({ ref: "city" }); const city = wrapper.findComponent({ ref: "city" });
const state = wrapper.findComponent({ ref: "state" }); const state = wrapper.findComponent({ ref: "state" });
const zipCode = wrapper.findComponent({ ref: "zipCode" }); const zipCode = wrapper.findComponent({ ref: "zipCode" });
const streetAddress2 = wrapper.findComponent({ ref: "streetAddress2"});
// Assert // Assert
expect(streetAddress.exists()).toBe(true); expect(streetAddress.exists()).toBe(true);
expect(city.exists()).toBe(true); expect(city.exists()).toBe(false);
expect(state.exists()).toBe(true); expect(state.exists()).toBe(false);
expect(zipCode.exists()).toBe(true); expect(zipCode.exists()).toBe(false);
expect(streetAddress2.exists()).toBe(false);
}); });
});
describe("happy paths", () => {
test("full street address is passed in => address fields are displayed", async () => { test("full street address is passed in => address fields are displayed", async () => {
// Arrange/Act // Arrange/Act
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
@ -45,6 +44,7 @@ describe("address-questions.vue", () => {
state: "OH", state: "OH",
zipCode: "12312", zipCode: "12312",
}, },
includeStreetAddress2: true,
}, },
}); });
@ -54,14 +54,40 @@ describe("address-questions.vue", () => {
const cityField = wrapper.findComponent({ ref: "city" }); const cityField = wrapper.findComponent({ ref: "city" });
const stateField = wrapper.findComponent({ ref: "state" }); const stateField = wrapper.findComponent({ ref: "state" });
const zipField = wrapper.findComponent({ ref: "zipCode" }); const zipField = wrapper.findComponent({ ref: "zipCode" });
const streetAddress2 = wrapper.findComponent({ ref: "streetAddress2"});
expect(cityField.exists()).toBeTruthy(); expect(cityField.exists()).toBeTruthy();
expect(cityField.isVisible()).toBeTruthy(); expect(cityField.isVisible()).toBeTruthy();
expect(stateField.exists()).toBeTruthy(); expect(stateField.exists()).toBeTruthy();
expect(cityField.isVisible()).toBeTruthy(); expect(cityField.isVisible()).toBeTruthy();
expect(zipField.exists()).toBeTruthy(); expect(zipField.exists()).toBeTruthy();
expect(cityField.isVisible()).toBeTruthy(); expect(cityField.isVisible()).toBeTruthy();
expect(streetAddress2.isVisible()).toBeTruthy();
}); });
test("address2 is hidden if includeStreetAddress2 is false", async () => {
// Arrange/Act
const { wrapper } = setupMocks({
props: {
modelValue: {
streetAddress: "12345 Test Road",
city: "Tests",
state: "OH",
zipCode: "12312",
},
includeStreetAddress2: false,
},
});
await wrapper.vm.$nextTick();
// Assert
const streetAddress2 = wrapper.findComponent({ ref: "streetAddress2"});
expect(streetAddress2.exists()).toBe(false);
});
});
describe("happy paths", () => {
test("street address is entered, user chooses good result from autocomplete results => other fields are filled in", async () => { test("street address is entered, user chooses good result from autocomplete results => other fields are filled in", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});

View file

@ -14,7 +14,6 @@
cmsWidgetName="AlertNoMatchWarningWidget" cmsWidgetName="AlertNoMatchWarningWidget"
alertClass="alert-warning" alertClass="alert-warning"
v-bind:isDismissible="false" /> v-bind:isDismissible="false" />
<div class="row mt-2 mb-4"> <div class="row mt-2 mb-4">
<div class="col"> <div class="col">
<textboxQuestion <textboxQuestion
@ -31,55 +30,55 @@
@keydown.enter.prevent /> @keydown.enter.prevent />
</div> </div>
</div> </div>
<div class="row mt-2 mb-4" v-if="includeStreetAddress2">
<div class="col">
<textboxQuestion
cmsWidgetName="StreetAddress2QuestionWidget"
v-model="addressModel.streetAddress2"
aria-haspopup=""
inputId="streetAddress2Field"
disableAutoFill
/>
</div>
</div>
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<div class="row mb-4" aria-live="polite"> <div v-if="showAllFields">
<div class="col"> <div class="row mt-2 mb-4" v-if="includeStreetAddress2">
<textboxQuestion <div class="col">
cmsWidgetName="CityQuestionWidget" <textboxQuestion
v-model="addressModel.city" ref="streetAddress2"
ref="city" cmsWidgetName="StreetAddress2QuestionWidget"
inputId="cbf28188fdf2436688fd735915f7ee56" v-model="addressModel.streetAddress2"
disableAutoFill aria-haspopup=""
validationRules="city-required" /> inputId="streetAddress2Field"
disableAutoFill
/>
</div>
</div>
<div class="row mb-4" aria-live="polite">
<div class="col">
<textboxQuestion
cmsWidgetName="CityQuestionWidget"
v-model="addressModel.city"
ref="city"
inputId="cbf28188fdf2436688fd735915f7ee56"
disableAutoFill
validationRules="city-required" />
</div>
</div>
<div class="row mb-4" aria-live="polite">
<div class="col">
<dropdownQuestion
cmsWidgetName="StateQuestionWidget"
v-model="addressModel.state"
ref="state"
inputId="8fdf9dc2e13e430eb57529499dceb3eb"
:options="stateOptions"
disableAutoFill
validationRules="state-required" />
</div>
<div class="col">
<textboxQuestion
cmsWidgetName="ZipQuestionWidget"
v-model="addressModel.zipCode"
ref="zipCode"
inputId="01a9a1c2de0b4c9da8e023c9ae3be498"
mask="#####"
disableAutoFill
validationRules="zip-code-required|zip-code-format" />
</div>
</div> </div>
</div> </div>
</transition> </transition>
<transition name="fade" mode="out-in">
<div class="row mb-4" aria-live="polite">
<div class="col">
<dropdownQuestion
cmsWidgetName="StateQuestionWidget"
v-model="addressModel.state"
ref="state"
inputId="8fdf9dc2e13e430eb57529499dceb3eb"
:options="stateOptions"
disableAutoFill
validationRules="state-required" />
</div>
<div class="col">
<textboxQuestion
cmsWidgetName="ZipQuestionWidget"
v-model="addressModel.zipCode"
ref="zipCode"
inputId="01a9a1c2de0b4c9da8e023c9ae3be498"
mask="#####"
disableAutoFill
validationRules="zip-code-required|zip-code-format" />
</div>
</div>
</transition>
</div> </div>
</template> </template>
@ -130,6 +129,7 @@ export default {
matchFound: null, // null = no attempted match, true = match was found, false = match was not found matchFound: null, // null = no attempted match, true = match was found, false = match was not found
enterPressed: false, enterPressed: false,
isAddressWatchActive: false, // Only deep watch the address model when a match was not found isAddressWatchActive: false, // Only deep watch the address model when a match was not found
showAllFields: false,
}; };
}, },
computed: { computed: {
@ -210,6 +210,8 @@ export default {
// If a match has been previously found then do nothing // If a match has been previously found then do nothing
// OR // OR
// If the user pressed "Enter" then do nothing // If the user pressed "Enter" then do nothing
this.showAllFields = true;
if (self.matchFound || self.enterPressed) { if (self.matchFound || self.enterPressed) {
return; return;
} }
@ -307,6 +309,7 @@ export default {
}, },
mounted() { mounted() {
this.setupAddressLookup(); this.setupAddressLookup();
this.showAllFields = !!this.addressModel.streetAddress;
}, },
watch: { watch: {
matchFound: { matchFound: {

View file

@ -31,6 +31,7 @@
v-model="licensePlate" v-model="licensePlate"
isRequired isRequired
disableAutoFill disableAutoFill
id="license-plate-question-wrapper"
inputId="license-plate-question" inputId="license-plate-question"
validationRules="license-plate-required" /> validationRules="license-plate-required" />
<dropdownQuestion <dropdownQuestion
@ -272,3 +273,12 @@ export default {
}, },
}; };
</script> </script>
<style>
#license-plate-question-wrapper .form-test-error {
/**
Override extra margin-bottom in the error message in TextboxQuestion
*/
margin-bottom: 0 !important;
}
</style>

View file

@ -1,6 +1,9 @@
/* eslint-env jest */ /* eslint-env jest */
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { navigationScenarios } from '@/router/router-constants/navigation-scenarios'; import { navigationScenarios } from '@/router/router-constants/navigation-scenarios';
import { issPageValues } from '@/router/router-constants/issPage-values';
import { queryStrings } from '@/constants/query-strings';
import { GaActions } from "@/constants/analytics";
import VehicleLookup from './vehicle-lookup.vue'; import VehicleLookup from './vehicle-lookup.vue';
const vinLookupMethodsMockData = { const vinLookupMethodsMockData = {
@ -55,6 +58,12 @@ function setupMocks() {
mixins: [ mixins: [
{ {
computed: { computed: {
GaActions() {
return GaActions;
},
queryStrings() {
return queryStrings;
},
navigationScenarios() { navigationScenarios() {
return navigationScenarios; return navigationScenarios;
}, },
@ -80,6 +89,7 @@ function setupMocks() {
}), }),
getFooterInfoBoxHeight: jest.fn(() => 80), getFooterInfoBoxHeight: jest.fn(() => 80),
getPageNameByQueryString: jest.fn(() => "vehicle-lookup"), getPageNameByQueryString: jest.fn(() => "vehicle-lookup"),
pushEventToGA: jest.fn(),
}, },
}, },
], ],

View file

@ -2,12 +2,23 @@
import { render } from '@testing-library/vue'; import { render } from '@testing-library/vue';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { issPageValues } from '@/router/router-constants/issPage-values';
import { queryStrings } from '@/constants/query-strings';
import { GaActions } from "@/constants/analytics";
import VinLocationInformationComponent from './vin-location-information.vue'; import VinLocationInformationComponent from './vin-location-information.vue';
const mockText = Object.freeze({ const mockText = Object.freeze({
HEADER: 'Mock Header', HEADER: 'Mock Header',
BODY: 'Mock Body', BODY: 'Mock Body',
}); });
const mockRoute = {
query: {
issPage: issPageValues.VIN_LOOKUP,
},
};
const mockRouter = {
navigate: jest.fn(),
};
const mountOptions = { const mountOptions = {
global: { global: {
@ -27,9 +38,22 @@ const mountOptions = {
return ''; return '';
}), }),
pushEventToGA: jest.fn()
}, },
computed: {
GaActions() {
return GaActions;
},
queryStrings() {
return queryStrings;
},
}
}, },
], ],
mocks: {
$route: mockRoute,
$router: mockRouter,
},
}, },
}; };

View file

@ -6,6 +6,8 @@ import { createTestingPinia } from '@pinia/testing';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { errorMessages } from '@/constants/error-messages'; import { errorMessages } from '@/constants/error-messages';
import { issPageValues } from '@/router/router-constants/issPage-values'; import { issPageValues } from '@/router/router-constants/issPage-values';
import { queryStrings } from '@/constants/query-strings';
import { GaActions } from "@/constants/analytics";
import { navigationScenarios } from '@/router/router-constants/navigation-scenarios'; import { navigationScenarios } from '@/router/router-constants/navigation-scenarios';
import { routerParams } from '@/router/router-params'; import { routerParams } from '@/router/router-params';
import { useMainStore } from '@/store'; import { useMainStore } from '@/store';
@ -134,11 +136,18 @@ const mountOptions = {
getFooterInfoBoxHeight: jest.fn(() => 80), getFooterInfoBoxHeight: jest.fn(() => 80),
cssClassNameForCmsWidget: jest.fn(() => 'widget-name-mock-class'), cssClassNameForCmsWidget: jest.fn(() => 'widget-name-mock-class'),
getPageNameByQueryString: jest.fn(() => ''), getPageNameByQueryString: jest.fn(() => ''),
pushEventToGA: jest.fn(),
}, },
computed: { computed: {
GaActions() {
return GaActions;
},
navigationScenarios() { navigationScenarios() {
return navigationScenarios; return navigationScenarios;
}, },
queryStrings() {
return queryStrings;
},
}, },
}, },
], ],

View file

@ -7,7 +7,7 @@
&.list-card { &.list-card {
&:not(.selected) { &:not(.selected) {
position: relative; position: relative;
z-index: 4; z-index: 5;
@include box-shadow-hover($blue-300); @include box-shadow-hover($blue-300);
} }
} }

View file

@ -11,10 +11,6 @@
{{text}} {{text}}
<slot name="after-text"></slot> <slot name="after-text"></slot>
</a> </a>
<a v-else-if="linkType === 'dashedUnderline'" @click="handleClick" class="dashed-underline" :href="href">
{{text}}
<slot name="after-text"></slot>
</a>
<a v-else-if="linkType === 'text'" @click="handleClick" :href="href"> <a v-else-if="linkType === 'text'" @click="handleClick" :href="href">
{{text}} {{text}}
<slot name="after-text"></slot> <slot name="after-text"></slot>
@ -33,6 +29,12 @@
}, },
methods: { methods: {
handleClick(event) { handleClick(event) {
this.pushEventToGA(
this.$route.query[this.queryStrings.ISS_PAGE],
this.GaActions.CLICKED,
this.text,
true
);
this.$emit("click-event"); this.$emit("click-event");
}, },
}, },
@ -41,10 +43,9 @@
<style lang="scss"> <style lang="scss">
a { a {
display: inline-flex !important;
color: $blue; color: $blue;
text-underline-offset: 0.5em; text-underline-offset: 5px;
line-height: 26px; line-height: 2;
padding: 0 0 4px 0; padding: 0 0 4px 0;
font-weight: 500; font-weight: 500;
max-width: fit-content; max-width: fit-content;
@ -75,9 +76,6 @@
text-decoration: underline; text-decoration: underline;
} }
} }
&.dashed-underline {
text-decoration: underline dashed 1px;
}
} }
</style> </style>