Merge branch 'develop' into feature/Digital/SSR-123
# Conflicts: # src/common-components/button-question/button-question.vue
This commit is contained in:
commit
207cd61380
35 changed files with 4134 additions and 2195 deletions
|
|
@ -13,4 +13,5 @@
|
||||||
@import "@/styles/common-typography-styles.scss";
|
@import "@/styles/common-typography-styles.scss";
|
||||||
@import "@/styles/common-error-styles.scss";
|
@import "@/styles/common-error-styles.scss";
|
||||||
@import "@/styles/common-animations.scss";
|
@import "@/styles/common-animations.scss";
|
||||||
|
@import "@/styles/shared-input-button-styles.scss";
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
170
src/common-components/base-input-button/base-input-button.vue
Normal file
170
src/common-components/base-input-button/base-input-button.vue
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
<template>
|
||||||
|
<label
|
||||||
|
:class="[buttonWrapperClasses, { 'has-error': errors.length > 0 }]"
|
||||||
|
:for="buttonId"
|
||||||
|
@focusin="handleFocus"
|
||||||
|
@focusout="handleBlur"
|
||||||
|
@mousedown.left="handleEventAction(eventTypes.CLICK, $event)">
|
||||||
|
<input
|
||||||
|
:type="inputType"
|
||||||
|
:id="buttonId"
|
||||||
|
:key="buttonId"
|
||||||
|
:name="groupName"
|
||||||
|
:class="inputClasses"
|
||||||
|
:aria-required="isRequired"
|
||||||
|
:value="value"
|
||||||
|
:checked="isChecked"
|
||||||
|
@keypress.space="handleEventAction(eventTypes.SPACE, $event)"
|
||||||
|
@keypress.enter="handleEventAction(eventTypes.ENTER, $event)"
|
||||||
|
@change="handleEventAction(eventTypes.CHANGE, $event)" />
|
||||||
|
|
||||||
|
<slot></slot>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useField } from "vee-validate";
|
||||||
|
import { toRef } from "vue";
|
||||||
|
import {
|
||||||
|
handleButtonComponentFocus,
|
||||||
|
handleInputComponentBlur,
|
||||||
|
} from "@/helpers/button-question-focus-helper";
|
||||||
|
import { inputButtonProps } from "@/common-components/base-input-button/button-functionality-props";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "base-input-button",
|
||||||
|
props: {
|
||||||
|
...inputButtonProps,
|
||||||
|
buttonWrapperClasses: [String, Array, Object],
|
||||||
|
inputClasses: [String, Array, Object],
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
valueToEmit: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.isChecked) {
|
||||||
|
this.handleChange(this.modelValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleEventAction(eventType, e) {
|
||||||
|
if (this.isMultiSelect) {
|
||||||
|
switch (eventType) {
|
||||||
|
case this.eventTypes.CHANGE:
|
||||||
|
this.handleClick(e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (eventType) {
|
||||||
|
case this.eventTypes.CLICK:
|
||||||
|
case this.eventTypes.ENTER:
|
||||||
|
case this.eventTypes.SPACE:
|
||||||
|
this.handleClick(e);
|
||||||
|
break;
|
||||||
|
case this.eventTypes.CHANGE:
|
||||||
|
this.selectingInitiatesLoad
|
||||||
|
? this.handleSelectionChange(e)
|
||||||
|
: this.handleClick(e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleSelectionChange(e) {
|
||||||
|
if (
|
||||||
|
this.isMultiSelect &&
|
||||||
|
(this.modelValue instanceof Array || this.modelValue == null)
|
||||||
|
) {
|
||||||
|
let newValue = this.modelValue ? [...this.modelValue] : [];
|
||||||
|
if (!newValue.includes(this.value)) {
|
||||||
|
newValue.push(this.value);
|
||||||
|
} else {
|
||||||
|
newValue.splice(newValue.indexOf(this.value), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.valueToEmit = newValue;
|
||||||
|
} else if (!this.isMultiSelect) {
|
||||||
|
this.valueToEmit = this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleChange(this.valueToEmit);
|
||||||
|
},
|
||||||
|
handleClick(e) {
|
||||||
|
this.handleSelectionChange(e);
|
||||||
|
this.$emit("update:modelValue", this.valueToEmit);
|
||||||
|
},
|
||||||
|
handleFocus() {
|
||||||
|
handleButtonComponentFocus({
|
||||||
|
groupName: this.groupName,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleBlur() {
|
||||||
|
handleInputComponentBlur({
|
||||||
|
groupName: this.groupName,
|
||||||
|
onButtonQuestionLostFocusCallback: this.handlePushClickEventToGACheck,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isChecked() {
|
||||||
|
if (this.isMultiSelect && this.modelValue instanceof Array) {
|
||||||
|
return this.modelValue.includes(this.value);
|
||||||
|
} else if (!this.isMultiSelect) {
|
||||||
|
return this.modelValue == this.value;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inputType() {
|
||||||
|
return this.isMultiSelect ? "checkbox" : "radio";
|
||||||
|
},
|
||||||
|
buttonId() {
|
||||||
|
return `${this.groupName?.replace(" ", "-")}-${this.value
|
||||||
|
?.toString()
|
||||||
|
?.replace(" ", "-")}`;
|
||||||
|
},
|
||||||
|
isValueSelectedOnClick() {
|
||||||
|
return this.isMultiSelect || this.selectingInitiatesLoad;
|
||||||
|
},
|
||||||
|
eventTypes() {
|
||||||
|
return {
|
||||||
|
CHANGE: "change",
|
||||||
|
ENTER: "enter",
|
||||||
|
SPACE: "space",
|
||||||
|
CLICK: "click",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const inputType = props.isMultiSelect ? "checkbox" : "radio";
|
||||||
|
|
||||||
|
const fieldOptions = {
|
||||||
|
type: inputType,
|
||||||
|
validateOnValueUpdate: false,
|
||||||
|
validateOnMount: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { handleChange, meta, errors } = useField(
|
||||||
|
toRef(props, "groupName"),
|
||||||
|
toRef(props, "validationRules"),
|
||||||
|
fieldOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleChange,
|
||||||
|
errors,
|
||||||
|
meta,
|
||||||
|
fieldOptions, // only need to expose this for unit test purposes
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
input {
|
||||||
|
opacity: 0;
|
||||||
|
height: 0.1px; // NOTE: cannot be zero or Safari can't put focus on it
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
export const inputButtonProps = {
|
||||||
|
value: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: [Array, String, Number],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isMultiSelect: Boolean,
|
||||||
|
groupName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
validationRules: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
valueToLogType: String,
|
||||||
|
isRequired: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
lastValuePushedToGa: [String, Number],
|
||||||
|
setLastValuePushedToGa: Function,
|
||||||
|
selectingInitiatesLoad: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,101 +1,135 @@
|
||||||
|
<!-- Documented in confluence https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Component -->
|
||||||
<template>
|
<template>
|
||||||
<div :class="isOverflowScrollable ? 'button-question button-question-overflow' : 'button-question'">
|
<div
|
||||||
|
:class="
|
||||||
|
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 class="w-100" :aria-required=isRequired :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="formatString(groupName)">
|
<fieldset
|
||||||
<legend class="sr-only" :data-focus-target="formatString(groupName)" :id="formatString(groupName)" tabindex="-1">
|
class="w-100"
|
||||||
{{ questionText }}
|
:aria-required="isRequired"
|
||||||
{{(isMultiSelect && answers && answers.length > 1) ? 'Select one or more options below.' : 'Select an option below.' }}
|
:class="getFieldSetClasses"
|
||||||
</legend>
|
:role="isMultiSelect ? 'group' : 'radiogroup'"
|
||||||
<div :class="getComponentLoopWrapperClasses">
|
:aria-labelledby="formatString(groupName)">
|
||||||
<div :class="getComponentWrapperClasses" v-for="answer in answers" :key="answer.Name ? answer.Name : answer">
|
<legend
|
||||||
<component
|
class="sr-only"
|
||||||
:is="buttonType"
|
:data-focus-target="formatString(groupName)"
|
||||||
@isCheckedChanged="handleCheckedChanged"
|
tabindex="-1"
|
||||||
:buttonID="answer.Name ? formatString(groupName) + '-' + answer.Name : formatString(groupName) + '-' + getAnswerString(answer, 'Text')"
|
:id="formatString(groupName)">
|
||||||
:value="getValue(answer)"
|
{{ questionText }}
|
||||||
:buttonLabel="answer.Text ? answer.Text : getAnswerString(answer, 'Name')"
|
{{
|
||||||
:buttonLabelSubCopy="answer.SubText"
|
isMultiSelect && answers && answers.length > 1
|
||||||
:textPosition="textPosition"
|
? "Select one or more options below."
|
||||||
:isMultiSelect="isMultiSelect"
|
: "Select an option below."
|
||||||
:groupName="formatString(groupName)"
|
}}
|
||||||
:selectingInitiatesLoad="selectingInitiatesLoad"
|
</legend>
|
||||||
:loaderColor="loaderColor"
|
<div :class="getComponentLoopWrapperClasses">
|
||||||
:loaderPosition="loaderPosition"
|
<div
|
||||||
:isWide=isWide
|
:class="getComponentWrapperClasses"
|
||||||
:isCashOrInsurance=isCashOrInsurance
|
v-for="answer in buttonsInfo"
|
||||||
:isRequired=isRequired
|
:key="answer.value ? answer.value : answer">
|
||||||
:buttonImage="answer.AnswerImageUrl"
|
<component
|
||||||
:buttonImageId="answer.ImageId"
|
:is="buttonTypeString"
|
||||||
:altText="answer.Name ? answer.Name : answer"
|
:buttonLabel="answer.buttonLabel"
|
||||||
screenReaderOnlyText="(opens new window)"
|
:buttonLabelSubCopy="answer.buttonLabelSubCopy"
|
||||||
:colLength="getColLength"
|
:buttonBodyCopy="answer.buttonBodyCopy"
|
||||||
:selectedValues="selectedValues"
|
:buttonAuxillaryCopy="answer.buttonAuxillaryCopy"
|
||||||
data-test="button"
|
:buttonFooterCopy="answer.buttonFooterCopy"
|
||||||
:validationRules="validationRules"
|
:buttonImage="answer.buttonImage"
|
||||||
:class="[suppressError ? 'alertError' : '' , isCashOrInsurance ? 'radio-fancy' : '']"
|
:buttonImageId="answer.buttonImageId"
|
||||||
:valueToLogType="valueToLogType"
|
:groupName="answer.groupName"
|
||||||
/>
|
:isMultiSelect="isMultiSelect"
|
||||||
<!-- For nested questions -->
|
:value="answer.value"
|
||||||
<transition name="fade" mode="out-in">
|
:selectingInitiatesLoad="selectingInitiatesLoad"
|
||||||
<div v-if="typeof selectedValues == 'string' && selectedValues == answer.Name">
|
:isWide="isWide"
|
||||||
<slot></slot>
|
:validationRules="validationRules"
|
||||||
</div>
|
:textPosition="textPosition"
|
||||||
</transition>
|
:additionalButtonStyling="additionalButtonStyling"
|
||||||
</div>
|
:lastValuePushedToGa="lastValuePushedToGa"
|
||||||
</div>
|
:setLastValuePushedToGa="setLastValuePushedToGa"
|
||||||
</fieldset>
|
v-model="selectedValues" />
|
||||||
|
<!-- For nested questions -->
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
typeof selectedValues == 'string' &&
|
||||||
|
selectedValues == answer.value
|
||||||
|
">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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 :name="formatString(groupName)" v-if="!suppressError"></error-message>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import listButton from "@/ux-components/list-button/list-button";
|
import listButton from "@/ux-components/list-button/list-button";
|
||||||
import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-button-horizontal";
|
import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-button-horizontal";
|
||||||
import listCard from "@/ux-components/list-card/list-card";
|
import listCard from "@/ux-components/list-card/list-card";
|
||||||
import { ErrorMessage } from 'vee-validate';
|
import radio from "@/ux-components/radio/radio";
|
||||||
import radio from "@/ux-components/radio/radio";
|
import { ErrorMessage } from "vee-validate";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "buttonQuestion",
|
name: "buttonQuestion",
|
||||||
props: {
|
props: {
|
||||||
buttonType: {
|
buttonTypeString: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "listButton",
|
default: "listButton",
|
||||||
|
},
|
||||||
|
buttonTypeObject: {
|
||||||
|
type: Object,
|
||||||
|
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, String],
|
modelValue: [Array, Number, String],
|
||||||
|
value: [Number, String],
|
||||||
validationRules: String,
|
validationRules: String,
|
||||||
suppressError: Boolean,
|
suppressError: Boolean,
|
||||||
useTextForValue: Boolean,
|
useTextForValue: Boolean,
|
||||||
valueToLogType: String,
|
valueToLogType: String,
|
||||||
},
|
additionalButtonStyling: String,
|
||||||
computed: {
|
},
|
||||||
|
beforeMount() {
|
||||||
|
if (this.buttonTypeObject) {
|
||||||
|
this.$options.components[this.buttonTypeString] = this.buttonTypeObject;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lastValuePushedToGa: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
getFieldSetClasses() {
|
getFieldSetClasses() {
|
||||||
if (this.isOverflowScrollable) {
|
if (this.isOverflowScrollable) {
|
||||||
return "container-fluid overflow-scroll position-absolute px-5 pb-2";
|
return "container-fluid overflow-scroll position-absolute px-5 pb-2";
|
||||||
|
|
@ -108,146 +142,132 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getComponentLoopWrapperClasses() {
|
getComponentLoopWrapperClasses() {
|
||||||
let classes;
|
let classes;
|
||||||
switch (this.buttonType) {
|
switch (this.buttonTypeString) {
|
||||||
case "listButton":
|
case "listButton":
|
||||||
classes = "w-100";
|
classes = "w-100";
|
||||||
break;
|
break;
|
||||||
case "listButtonHorizontal":
|
case "listButtonHorizontal":
|
||||||
classes = "d-flex flex-row p-0";
|
classes = "d-flex flex-row p-0";
|
||||||
break;
|
break;
|
||||||
case 'listCard':
|
case "listCard":
|
||||||
classes = "row g-2 justify-content-center";
|
classes = "row g-2 justify-content-center";
|
||||||
break;
|
if (this.isWide) {
|
||||||
case 'radio':
|
classes += " flex-column";
|
||||||
classes = 'ui-radio d-flex'
|
}
|
||||||
break;
|
break;
|
||||||
}
|
case "radio":
|
||||||
return classes;
|
classes = "ui-radio d-flex";
|
||||||
|
break;
|
||||||
|
case "servicePackageRadio":
|
||||||
|
classes = "package-main";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return classes;
|
||||||
},
|
},
|
||||||
getComponentWrapperClasses() {
|
getComponentWrapperClasses() {
|
||||||
let classes = "";
|
let classes = "";
|
||||||
|
|
||||||
classes += this.isWide ? "col-12" : "col";
|
classes += this.isWide ? "col-12" : "col";
|
||||||
|
|
||||||
if (this.buttonType == "radio") {
|
if (this.buttonTypeString == "radio") {
|
||||||
classes += " radio-button-container";
|
classes += " radio-button-container";
|
||||||
}
|
} else if (this.buttonTypeString == "servicePackageRadio") {
|
||||||
|
classes = "package-wrapper";
|
||||||
|
}
|
||||||
|
|
||||||
return classes;
|
return classes;
|
||||||
},
|
},
|
||||||
getColLength(){
|
buttonsInfo() {
|
||||||
if(this.isWide) {
|
return (Array.isArray(this.answers) ? this.answers : [])?.map((answer) => ({
|
||||||
return "12"
|
buttonLabel: answer.buttonLabel ?? answer.Text ?? answer,
|
||||||
} else {
|
altText: answer.altText ?? (answer.Name ? answer.Name : answer),
|
||||||
return "";
|
buttonLabelSubCopy: answer.buttonLabelSubCopy ?? answer.SubText,
|
||||||
}
|
buttonBodyCopy: answer.buttonBodyCopy ?? answer.buttonBodyCopy,
|
||||||
|
buttonAuxillaryCopy: answer.buttonAuxillaryCopy ?? answer.buttonAuxillaryCopy,
|
||||||
|
buttonFooterCopy: answer.buttonFooterCopy ?? answer.buttonFooterCopy,
|
||||||
|
buttonImage: answer.buttonImage ?? answer.AnswerImageUrl,
|
||||||
|
buttonImageId: answer.buttonImageId ?? answer.ImageId,
|
||||||
|
groupName: this.formatString(this.groupName),
|
||||||
|
value:
|
||||||
|
answer.value ??
|
||||||
|
(this.useTextForValue && answer.Text ? answer.Text : answer.Name) ??
|
||||||
|
(typeof answer !== "object" ? answer : null),
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
selectedValues: {
|
selectedValues: {
|
||||||
get: function() {
|
get() {
|
||||||
return this.modelValue;
|
return this.modelValue;
|
||||||
},
|
},
|
||||||
set: function(newValue) {
|
set(selectedAnswers) {
|
||||||
this.$emit("update:modelValue", newValue);
|
this.$emit("update:modelValue", selectedAnswers);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
formatString(str) {
|
formatString(str) {
|
||||||
return str.replace(" ", "-");
|
return str?.replaceAll(" ", "-");
|
||||||
},
|
},
|
||||||
getValue(answer){
|
setLastValuePushedToGa(lastValuePushedToGa) {
|
||||||
if (this.useTextForValue) { return answer.Text }
|
this.lastValuePushedToGa = lastValuePushedToGa;
|
||||||
return answer.Name ? answer.Name : answer;
|
|
||||||
},
|
},
|
||||||
getAnswerString(answer, prop = "Name") {
|
},
|
||||||
switch (typeof answer) {
|
components: {
|
||||||
case "string":
|
|
||||||
case "number":
|
|
||||||
case "boolean":
|
|
||||||
return this.formatString(answer.toString());
|
|
||||||
default:
|
|
||||||
return answer[prop] ? this.formatString(answer[prop]) : this.formatString(answer.toString());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleCheckedChanged(val) {
|
|
||||||
if(this.selectingInitiatesLoad) {
|
|
||||||
this.selectedValues = val.value;
|
|
||||||
} else {
|
|
||||||
if(this.isMultiSelect) {
|
|
||||||
const newSelectedValues = this.selectedValues;
|
|
||||||
val.checkValue ? newSelectedValues.push(val.value) : newSelectedValues.splice(newSelectedValues.indexOf(val.value), 1);
|
|
||||||
this.selectedValues = newSelectedValues;
|
|
||||||
}
|
|
||||||
else if (Array.isArray(this.selectedValues)) {
|
|
||||||
this.selectedValues[0] = val.value;
|
|
||||||
const temp = this.selectedValues;
|
|
||||||
this.selectedValues = temp;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.selectedValues = val.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.$emit("isCheckedChanged", val);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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 {
|
}
|
||||||
color: $black;
|
.button-question {
|
||||||
|
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 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.625rem;
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle-parts {
|
||||||
|
.question-text {
|
||||||
|
span {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-align: left;
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.question-text {
|
.question-text {
|
||||||
margin-top: 1.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
|
|
||||||
& > span {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.vehicle-parts {
|
|
||||||
.question-text {
|
|
||||||
span {
|
|
||||||
font-size: .875rem;
|
|
||||||
text-align: left;
|
|
||||||
margin: 0 0 .5rem 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.question-text {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
|
||||||
fieldset {
|
|
||||||
.ui-radio {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
fieldset {
|
||||||
|
.ui-radio {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ import textLink from "@/ux-components/text-link/text-link";
|
||||||
import buttonMain from "@/ux-components/button-main/button-main";
|
import buttonMain from "@/ux-components/button-main/button-main";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "funnelFooter",
|
name: "siteFooter",
|
||||||
props: {
|
props: {
|
||||||
isForwardActionDisabled: Boolean,
|
isForwardActionDisabled: Boolean,
|
||||||
isBackButtonHidden: { type: Boolean, default: false },
|
isBackButtonHidden: { type: Boolean, default: false },
|
||||||
|
|
|
||||||
9
src/constants/damage-locations-cms.js
Normal file
9
src/constants/damage-locations-cms.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
const damageLocationsCms = {
|
||||||
|
WINDSHIELD: "WINDSHIELD",
|
||||||
|
SIDEDOOR: "SIDEDOOR",
|
||||||
|
REARWINDOW: "REARWINDOW",
|
||||||
|
DRIVERSIDE: "DRIVERSIDE",
|
||||||
|
PASSENGERSIDE: "PASSENGERSIDE",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { damageLocationsCms };
|
||||||
21
src/constants/damage-locations-selected.js
Normal file
21
src/constants/damage-locations-selected.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
const damageLocationsSelected = {
|
||||||
|
WINDSHIELD: "Windshield",
|
||||||
|
SIDEDOOR: "SideDoor",
|
||||||
|
REARWINDOW: "RearWindow",
|
||||||
|
REPAIR: "Repair",
|
||||||
|
REPLACE: "Replace",
|
||||||
|
DRIVER: "Driver",
|
||||||
|
PASSENGER: "Passenger",
|
||||||
|
FRONT: "Front",
|
||||||
|
REAR: "Rear",
|
||||||
|
BACK: "Back",
|
||||||
|
QUARTER: "Quarter",
|
||||||
|
VENT: "Vent",
|
||||||
|
SINGLE: "Single",
|
||||||
|
DRIVERSIDE: "DriverSide",
|
||||||
|
PASSENGERSIDE: "PassengerSide",
|
||||||
|
STATIONARY: "Stationary",
|
||||||
|
SLIDER: "Slider",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { damageLocationsSelected };
|
||||||
|
|
@ -27,6 +27,10 @@ const endpoints = {
|
||||||
url: "/vehicle/api/v1/vehicle/styles",
|
url: "/vehicle/api/v1/vehicle/styles",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
},
|
},
|
||||||
|
GetDamageOptions: {
|
||||||
|
url: "/parts/api/v1/parts/damage-options",
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
GetVehicle: {
|
GetVehicle: {
|
||||||
url: "/vehicle/api/v1/vehicle/lookup",
|
url: "/vehicle/api/v1/vehicle/lookup",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ export default {
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolve(response);
|
return resolve(response);
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
|
|
|
||||||
38
src/helpers/button-question-focus-helper.js
Normal file
38
src/helpers/button-question-focus-helper.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
/**
|
||||||
|
* Helper for GA click event. When the user mouse clicks on a `base-input-button`, we
|
||||||
|
* push the click event. When the user tabs through a list of radio buttons via a
|
||||||
|
* keyboard, we only want to push the GA click event if the selection was deliberate
|
||||||
|
* (space/enter key) or if there is a selection and the user tabs off of the radio group.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let lastFocusedInputGroupName = "";
|
||||||
|
let onButtonQuestionLostFocusCallback = null;
|
||||||
|
|
||||||
|
const handleAnyComponentFocus = (e) => {
|
||||||
|
const targetType = e.target.type;
|
||||||
|
if (targetType !== "radio" && targetType !== "checkbox") {
|
||||||
|
invokeButtonQuestionLostFocusCallback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleButtonComponentFocus = (e) => {
|
||||||
|
if (e && lastFocusedInputGroupName !== e.groupName) {
|
||||||
|
invokeButtonQuestionLostFocusCallback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputComponentBlur = (e) => {
|
||||||
|
if (e) {
|
||||||
|
lastFocusedInputGroupName = e.groupName;
|
||||||
|
onButtonQuestionLostFocusCallback = e.onButtonQuestionLostFocusCallback;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const invokeButtonQuestionLostFocusCallback = () => {
|
||||||
|
if (onButtonQuestionLostFocusCallback) {
|
||||||
|
onButtonQuestionLostFocusCallback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { handleAnyComponentFocus, handleButtonComponentFocus, handleInputComponentBlur };
|
||||||
|
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
<template>
|
||||||
|
<div class="damage-location-question">
|
||||||
|
<buttonQuestion
|
||||||
|
:questionText="questionText"
|
||||||
|
isMultiSelect
|
||||||
|
:answers="answersToDisplay"
|
||||||
|
:groupName="groupName"
|
||||||
|
buttonTypeString="listCard"
|
||||||
|
isRequired
|
||||||
|
v-model="selectedValues"
|
||||||
|
validationRules="damage-location-required" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import buttonQuestion from "@/common-components/button-question/button-question";
|
||||||
|
import { defineRule } from "vee-validate";
|
||||||
|
import { required } from "@/helpers/validation-rules";
|
||||||
|
import { errorMessages } from "@/constants/error-messages";
|
||||||
|
|
||||||
|
// DEFINE VALIDATION RULES
|
||||||
|
defineRule("damage-location-required", required(errorMessages.DAMAGE_LOCATION_REQUIRED));
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "damageLocationQuestion",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
damageOptions: Object,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: Array,
|
||||||
|
groupName: String,
|
||||||
|
cmsWidgetName: String,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initializeComponent(damageOptions) {
|
||||||
|
this.damageOptions = damageOptions;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
questionText() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
|
||||||
|
},
|
||||||
|
answersFromCms() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "Answers");
|
||||||
|
},
|
||||||
|
selectedValues: {
|
||||||
|
get: function () {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.$emit("update:modelValue", newValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
damageOptionsMap() {
|
||||||
|
return {
|
||||||
|
Windshield: true,
|
||||||
|
SideDoor:
|
||||||
|
this.damageOptions.driverSideOptions.availableReplacementOptions.length ||
|
||||||
|
this.damageOptions.passengerSideOptions.availableReplacementOptions.length,
|
||||||
|
RearWindow: this.damageOptions.backGlassOptions.availableReplacementOptions.length,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
answersToDisplay() {
|
||||||
|
const filteredAnswers = Array.isArray(this.answersFromCms)
|
||||||
|
? this.answersFromCms.filter((ans) => {
|
||||||
|
const name = ans.Name.split("-");
|
||||||
|
return (
|
||||||
|
name[0].toUpperCase() === this.mainStore.vehicle.category &&
|
||||||
|
this.damageOptionsMap[name[1]]
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
return filteredAnswers.map((ans) => {
|
||||||
|
const newName = ans.Name.includes("-") ? ans.Name.split("-")[1] : ans.Name;
|
||||||
|
ans.Name = newName;
|
||||||
|
return ans;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
buttonQuestion,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
<template>
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<div
|
||||||
|
v-if="shouldDisplayReplaceOptionsQuestion"
|
||||||
|
class="replace-options-question"
|
||||||
|
:class="this.answersToDisplay.length < 2 ? 'd-none' : ''"
|
||||||
|
aria-live="polite">
|
||||||
|
<buttonQuestion
|
||||||
|
isWide
|
||||||
|
:questionText="questionText"
|
||||||
|
:isMultiSelect="isMultiSelect"
|
||||||
|
:answers="answersToDisplay"
|
||||||
|
:groupName="groupName"
|
||||||
|
buttonTypeString="listCard"
|
||||||
|
v-model="selectedValues"
|
||||||
|
:validationRules="validationRules"
|
||||||
|
:suppressError="suppressError"
|
||||||
|
:isRequired="isRequired" />
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import buttonQuestion from "@/common-components/button-question/button-question";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "replaceOptionsQuestion",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
replaceOptions: this.isMultiSelect ? [] : "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
isAvailable: Boolean,
|
||||||
|
filterByVehicleCategory: Boolean,
|
||||||
|
groupName: String,
|
||||||
|
modelValue: [Array, String, Number],
|
||||||
|
isMultiSelect: Boolean,
|
||||||
|
validationRules: String,
|
||||||
|
suppressError: Boolean,
|
||||||
|
cmsWidgetName: String,
|
||||||
|
isRequired: Boolean,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initializeComponent(replaceOptions) {
|
||||||
|
this.replaceOptions = replaceOptions;
|
||||||
|
},
|
||||||
|
updateSelectedValues() {
|
||||||
|
// UPDATE SELECTEDVALUES IF ONLY ONE ANSWER
|
||||||
|
if (Array.isArray(this.answersToDisplay) && this.answersToDisplay.length === 1) {
|
||||||
|
this.selectedValues = this.isMultiSelect
|
||||||
|
? [this.answersToDisplay[0].Name]
|
||||||
|
: this.answersToDisplay[0].Name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
questionText() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
|
||||||
|
},
|
||||||
|
answersFromCms() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "Answers");
|
||||||
|
},
|
||||||
|
selectedValues: {
|
||||||
|
get: function () {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.$emit("update:modelValue", newValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
answersToDisplay() {
|
||||||
|
const filteredAnswers = Array.isArray(this.answersFromCms)
|
||||||
|
? this.answersFromCms.filter((ans) => {
|
||||||
|
const name = ans.Name.split("-");
|
||||||
|
return this.filterByVehicleCategory
|
||||||
|
? name[0].toUpperCase() === this.mainStore.vehicle.category &&
|
||||||
|
this.replaceOptions.includes(name[1])
|
||||||
|
: this.replaceOptions.includes(ans.Name);
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return filteredAnswers.map((ans) => {
|
||||||
|
const newName = ans.Name.includes("-") ? ans.Name.split("-")[1] : ans.Name;
|
||||||
|
ans.Name = newName;
|
||||||
|
return ans;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
shouldDisplayReplaceOptionsQuestion() {
|
||||||
|
return this.isAvailable && this.answersToDisplay.length > 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
isAvailable(val) {
|
||||||
|
// CHECK TO UPDATE SELECTED VALUES WHEN ISAVAILABLE IS TRUE
|
||||||
|
val && this.updateSelectedValues();
|
||||||
|
},
|
||||||
|
shouldDisplayReplaceOptionsQuestion(shouldDisplayReplaceOptionsQuestion) {
|
||||||
|
if (!shouldDisplayReplaceOptionsQuestion) {
|
||||||
|
this.selectedValues = this.isMultiSelect ? [] : "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
buttonQuestion,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
<template>
|
||||||
|
<div class="side-door-options">
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<div
|
||||||
|
class="side-doors"
|
||||||
|
v-if="selectedDamageLocations.includes('SideDoor')"
|
||||||
|
aria-live="polite">
|
||||||
|
<buttonQuestion
|
||||||
|
:questionText="questionText"
|
||||||
|
isMultiSelect
|
||||||
|
:answers="answersToDisplay"
|
||||||
|
:groupName="groupName"
|
||||||
|
buttonTypeString="listCard"
|
||||||
|
v-model="selectedDoorSidesValues"
|
||||||
|
validationRules="damage-side-required"
|
||||||
|
isRequired />
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
<replaceOptionsQuestion
|
||||||
|
ref="driverSideOptions"
|
||||||
|
cmsWidgetName="DriverSideReplaceOptionsQuestion"
|
||||||
|
:isAvailable="isDriverSideReplaceOptionsQuestionAvailable"
|
||||||
|
groupName="driverSideOptions"
|
||||||
|
isMultiSelect
|
||||||
|
filterByVehicleCategory
|
||||||
|
v-model="selectedDriverSideReplaceOptionsValues"
|
||||||
|
validationRules="driver-side-options-required"
|
||||||
|
isRequired />
|
||||||
|
<replaceOptionsQuestion
|
||||||
|
ref="passengerSideOptions"
|
||||||
|
cmsWidgetName="PassengerSideReplaceOptionsQuestion"
|
||||||
|
:isAvailable="isPassengerSideReplaceOptionsQuestionAvailable"
|
||||||
|
groupName="passengerSideOptions"
|
||||||
|
isMultiSelect
|
||||||
|
filterByVehicleCategory
|
||||||
|
v-model="selectedPassengerSideReplaceOptionsValues"
|
||||||
|
validationRules="passenger-side-options-required"
|
||||||
|
isRequired />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import buttonQuestion from "@/common-components/button-question/button-question";
|
||||||
|
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question";
|
||||||
|
import { defineRule } from "vee-validate";
|
||||||
|
import { required } from "@/helpers/validation-rules";
|
||||||
|
import { errorMessages } from "@/constants/error-messages";
|
||||||
|
import { damageLocationsSelected } from "@/constants/damage-locations-selected";
|
||||||
|
|
||||||
|
// DEFINE VALIDATION RULES
|
||||||
|
defineRule("damage-side-required", required(errorMessages.DAMAGE_SIDE_REQUIRED));
|
||||||
|
defineRule("driver-side-options-required", required(errorMessages.DRIVER_SIDE_OPTIONS_REQUIRED));
|
||||||
|
defineRule("passenger-side-options-required",required(errorMessages.PASSENGER_SIDE_OPTIONS_REQUIRED));
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "sideDoorOptions",
|
||||||
|
props: {
|
||||||
|
groupName: String,
|
||||||
|
modelValue: Object,
|
||||||
|
selectedDamageLocations: Array,
|
||||||
|
cmsWidgetName: String,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initializeComponent(driverSideOptions, passengerSideOptions) {
|
||||||
|
this.$refs.driverSideOptions.initializeComponent(driverSideOptions);
|
||||||
|
this.$refs.passengerSideOptions.initializeComponent(passengerSideOptions);
|
||||||
|
},
|
||||||
|
getSideDoorReplacementOptions(
|
||||||
|
selectedDoorSides,
|
||||||
|
selectedDriverSideReplaceOptions,
|
||||||
|
selectedPassengerSideReplaceOptions
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
selectedDoorSides: selectedDoorSides,
|
||||||
|
selectedDriverSideReplaceOptions: selectedDriverSideReplaceOptions,
|
||||||
|
selectedPassengerSideReplaceOptions: selectedPassengerSideReplaceOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
questionText() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
|
||||||
|
},
|
||||||
|
answersFromCms() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "Answers");
|
||||||
|
},
|
||||||
|
selectedValues: {
|
||||||
|
get: function () {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.$emit("update:modelValue", newValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectedDoorSidesValues: {
|
||||||
|
get: function () {
|
||||||
|
return this.selectedValues.selectedDoorSides;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.selectedValues = this.getSideDoorReplacementOptions(
|
||||||
|
newValue,
|
||||||
|
this.selectedValues.selectedDriverSideReplaceOptions,
|
||||||
|
this.selectedValues.selectedPassengerSideReplaceOptions
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectedDriverSideReplaceOptionsValues: {
|
||||||
|
get: function () {
|
||||||
|
return this.selectedValues.selectedDriverSideReplaceOptions;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.selectedValues = this.getSideDoorReplacementOptions(
|
||||||
|
this.selectedValues.selectedDoorSides,
|
||||||
|
newValue,
|
||||||
|
this.selectedValues.selectedPassengerSideReplaceOptions
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectedPassengerSideReplaceOptionsValues: {
|
||||||
|
get: function () {
|
||||||
|
return this.selectedValues.selectedPassengerSideReplaceOptions;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.selectedValues = this.getSideDoorReplacementOptions(
|
||||||
|
this.selectedValues.selectedDoorSides,
|
||||||
|
this.selectedValues.selectedDriverSideReplaceOptions,
|
||||||
|
newValue
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
answersToDisplay() {
|
||||||
|
const filteredAnswers = Array.isArray(this.answersFromCms)
|
||||||
|
? this.answersFromCms.filter((ans) => {
|
||||||
|
const name = ans.Name.split("-");
|
||||||
|
return name[0].toUpperCase() === this.mainStore.vehicle.category;
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return filteredAnswers.map((ans) => {
|
||||||
|
const newName = ans.Name.includes("-") ? ans.Name.split("-")[1] : ans.Name;
|
||||||
|
ans.Name = newName;
|
||||||
|
return ans;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isDriverSideReplaceOptionsQuestionAvailable() {
|
||||||
|
return (
|
||||||
|
Array.isArray(this.selectedDoorSidesValues) &&
|
||||||
|
this.selectedDoorSidesValues.includes(damageLocationsSelected.DRIVERSIDE) &&
|
||||||
|
Array.isArray(this.selectedDamageLocations) &&
|
||||||
|
this.selectedDamageLocations.includes(damageLocationsSelected.SIDEDOOR)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isPassengerSideReplaceOptionsQuestionAvailable() {
|
||||||
|
return (
|
||||||
|
Array.isArray(this.selectedDoorSidesValues) &&
|
||||||
|
this.selectedDoorSidesValues.includes(damageLocationsSelected.PASSENGERSIDE) &&
|
||||||
|
Array.isArray(this.selectedDamageLocations) &&
|
||||||
|
this.selectedDamageLocations.includes(damageLocationsSelected.SIDEDOOR)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
buttonQuestion,
|
||||||
|
replaceOptionsQuestion,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
464
src/layouts/vehicle-damage/vehicle-damage.vue
Normal file
464
src/layouts/vehicle-damage/vehicle-damage.vue
Normal file
|
|
@ -0,0 +1,464 @@
|
||||||
|
<template>
|
||||||
|
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
|
||||||
|
<div class="page-container-grouped-styles">
|
||||||
|
<siteHeader cmsWidgetName="SiteHeaderWidget" />
|
||||||
|
<vehicleBanner
|
||||||
|
cmsWidgetName="VehicleBannerWidget"
|
||||||
|
:displayGenericVehicleImage="false" />
|
||||||
|
<siteSubHeader cmsWidgetName="SiteSubHeaderWidget" />
|
||||||
|
<div class="fade-on-route-transition sub-container make-tall">
|
||||||
|
<alert
|
||||||
|
ref="vehicleChangeAlert"
|
||||||
|
v-if="shouldDisplayVehicleChangeAlert"
|
||||||
|
class="mt-5 mb-0"
|
||||||
|
cmsWidgetName="VehicleChangeAlert"
|
||||||
|
alertClass="alert-warning"
|
||||||
|
:isDismissible="false" />
|
||||||
|
<damageLocationQuestion
|
||||||
|
ref="damageLocation"
|
||||||
|
cmsWidgetName="DamageLocationQuestion"
|
||||||
|
v-model="selectedDamageLocations"
|
||||||
|
groupName="DamageLocationQuestion" />
|
||||||
|
<windshieldOptions
|
||||||
|
ref="windshieldOptions"
|
||||||
|
v-model="selectedWindshieldOptions"
|
||||||
|
:hasRepairReplaceConflict="hasRepairReplaceConflict"
|
||||||
|
:hasSplitSingleConflict="hasSplitSingleConflict"
|
||||||
|
:selectedDamageLocations="selectedDamageLocations" />
|
||||||
|
<alert
|
||||||
|
v-if="hasRepairReplaceConflict"
|
||||||
|
class="my-5"
|
||||||
|
cmsWidgetName="HasReplacementConflict"
|
||||||
|
alertClass="alert-danger"
|
||||||
|
:isDismissible="false" />
|
||||||
|
<sideDoorOptions
|
||||||
|
ref="sideDoorOptions"
|
||||||
|
cmsWidgetName="SideDoorSideQuestion"
|
||||||
|
groupName="SideDoorSideQuestion"
|
||||||
|
v-model="sideDoorOptionsData"
|
||||||
|
v-show="!hasRepairReplaceConflict"
|
||||||
|
:selectedDamageLocations="selectedDamageLocations" />
|
||||||
|
<replaceOptionsQuestion
|
||||||
|
ref="backGlassOptions"
|
||||||
|
cmsWidgetName="RearReplaceOptionsQuestion"
|
||||||
|
:isAvailable="isRearWindowDamageLocation && !hasRepairReplaceConflict"
|
||||||
|
v-model="selectedRearReplaceOptions"
|
||||||
|
groupName="BackGlassReplaceOptionsQuestion"
|
||||||
|
validationRules="replace-options-required" />
|
||||||
|
<site-footer
|
||||||
|
cmsWidgetName="SiteFooterWidget"
|
||||||
|
:isForwardActionDisabled="!meta.valid"
|
||||||
|
@back-clicked="backButtonAction"
|
||||||
|
@ForwardClicked="forwardButtonAction" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Components
|
||||||
|
import siteHeader from "@/common-components/site-header/site-header";
|
||||||
|
import siteFooter from "@/common-components/site-footer/site-footer";
|
||||||
|
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
|
||||||
|
import siteSubHeader from "@/common-components/site-sub-header/site-sub-header";
|
||||||
|
import sideDoorOptions from "@/layouts/vehicle-damage/side-door-options/side-door-options";
|
||||||
|
import damageLocationQuestion from "@/layouts/vehicle-damage/damage-location-question/damage-location-question";
|
||||||
|
import windshieldOptions from "@/layouts/vehicle-damage/windshield-options/windshield-options";
|
||||||
|
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question";
|
||||||
|
import alert from "@/ux-components/alert/alert";
|
||||||
|
|
||||||
|
// Supporting files
|
||||||
|
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||||
|
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||||
|
import { Form, defineRule } from "vee-validate";
|
||||||
|
import { required } from "@/helpers/validation-rules";
|
||||||
|
import { errorMessages } from "@/constants/error-messages";
|
||||||
|
import { damageLocationsCms } from "@/constants/damage-locations-cms.js";
|
||||||
|
import { damageLocationsSelected } from "@/constants/damage-locations-selected.js";
|
||||||
|
import { useMainStore } from '@/store';
|
||||||
|
|
||||||
|
// DEFINE VALIDATION RULES
|
||||||
|
defineRule("replace-options-required", required(errorMessages.REPLACE_OPTIONS_REQUIRED));
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "vehicle-damage",
|
||||||
|
async beforeRouteEnter(to, from, next) {
|
||||||
|
// Call APIs
|
||||||
|
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
|
||||||
|
const damageOptionsPromise = useMainStore().getDamageOptions(useMainStore().order.vehicle.carId);
|
||||||
|
|
||||||
|
// Settle promises and get results
|
||||||
|
const promiseResultMap = [
|
||||||
|
{
|
||||||
|
resultKey: "cmsContent",
|
||||||
|
promise: cmsContentPromise,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resultKey: "damageOptions",
|
||||||
|
promise: damageOptionsPromise,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const resultMap = await settleAllPromises(promiseResultMap);
|
||||||
|
|
||||||
|
// Call the "next" function to complete the transition to this page.
|
||||||
|
next((vm) => {
|
||||||
|
vm.setCmsContent(resultMap.cmsContent);
|
||||||
|
vm.$refs.damageLocation.initializeComponent(resultMap.damageOptions);
|
||||||
|
vm.$refs.sideDoorOptions.initializeComponent(
|
||||||
|
resultMap.damageOptions.driverSideOptions.availableReplacementOptions,
|
||||||
|
resultMap.damageOptions.passengerSideOptions.availableReplacementOptions
|
||||||
|
);
|
||||||
|
vm.$refs.windshieldOptions.initializeComponent(
|
||||||
|
resultMap.damageOptions.windshieldOptions.availableReplacementOptions
|
||||||
|
);
|
||||||
|
vm.$refs.backGlassOptions.initializeComponent(
|
||||||
|
resultMap.damageOptions.backGlassOptions.availableReplacementOptions
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedDamageLocations: this.getDamageLocationsFromStore(),
|
||||||
|
sideDoorOptionsData: {
|
||||||
|
selectedDoorSides: this.getDoorSidesFromStore(),
|
||||||
|
selectedDriverSideReplaceOptions: this.getDriverSideReplaceOptionsFromStore(),
|
||||||
|
selectedPassengerSideReplaceOptions: this.getPassengerSideReplaceOptionsFromStore(),
|
||||||
|
},
|
||||||
|
selectedWindshieldOptions: this.getWindshieldOptionsFromStore(),
|
||||||
|
selectedRearReplaceOptions: this.getRearReplaceOptionsFromStore(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
arePagePrerequisitesValid() {
|
||||||
|
if (this.mainStore.order.vehicle.carId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
backButtonAction() {
|
||||||
|
// route to move backwards
|
||||||
|
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||||
|
},
|
||||||
|
|
||||||
|
getDamageLocationsFromStore() {
|
||||||
|
var glassSelections = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
useMainStore().order.damage.glassToReplace?.some((glass) => {
|
||||||
|
return glass.glassLocation === damageLocationsSelected.WINDSHIELD;
|
||||||
|
}) ||
|
||||||
|
useMainStore().order.damage.isRepair
|
||||||
|
) {
|
||||||
|
glassSelections.push(damageLocationsSelected.WINDSHIELD);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
useMainStore().order.damage.glassToReplace?.some((glass) => {
|
||||||
|
return (
|
||||||
|
glass.glassLocation === damageLocationsSelected.DRIVER ||
|
||||||
|
glass.glassLocation === damageLocationsSelected.PASSENGER
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
glassSelections.push(damageLocationsSelected.SIDEDOOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
useMainStore().order.damage.glassToReplace?.some((glass) => {
|
||||||
|
return glass.glassLocation === damageLocationsSelected.REAR;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
glassSelections.push(damageLocationsSelected.REARWINDOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
return glassSelections;
|
||||||
|
},
|
||||||
|
|
||||||
|
getWindshieldOptionsFromStore() {
|
||||||
|
var windShieldOptions = {
|
||||||
|
selectedWindshieldDamageType: "",
|
||||||
|
selectedWindshieldChipCount: null,
|
||||||
|
selectedWindshieldReplaceOptions: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (useMainStore().order.damage.isRepair === undefined) return windshieldOptions;
|
||||||
|
|
||||||
|
if (useMainStore().order.damage.isRepair) {
|
||||||
|
windShieldOptions.selectedWindshieldDamageType = damageLocationsSelected.REPAIR;
|
||||||
|
windShieldOptions.selectedWindshieldChipCount = useMainStore().order.damage.numberOfChips;
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
useMainStore().order.damage.glassToReplace?.some((glass) => {
|
||||||
|
return (
|
||||||
|
glass.glassLocation === damageLocationsSelected.WINDSHIELD &&
|
||||||
|
glass.glassName === damageLocationsSelected.SINGLE
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
windShieldOptions.selectedWindshieldDamageType =
|
||||||
|
damageLocationsSelected.REPLACE;
|
||||||
|
windShieldOptions.selectedWindshieldReplaceOptions.push(
|
||||||
|
damageLocationsSelected.SINGLE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
useMainStore().order.damage.glassToReplace?.some((glass) => {
|
||||||
|
return (
|
||||||
|
glass.glassLocation === damageLocationsSelected.WINDSHIELD &&
|
||||||
|
glass.glassName === damageLocationsSelected.DRIVER
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
windShieldOptions.selectedWindshieldDamageType =
|
||||||
|
damageLocationsSelected.REPLACE;
|
||||||
|
windShieldOptions.selectedWindshieldReplaceOptions.push(
|
||||||
|
damageLocationsSelected.DRIVER
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
useMainStore().order.damage.glassToReplace?.some((glass) => {
|
||||||
|
return (
|
||||||
|
glass.glassLocation === damageLocationsSelected.WINDSHIELD &&
|
||||||
|
glass.glassName === damageLocationsSelected.PASSENGER
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
windShieldOptions.selectedWindshieldDamageType =
|
||||||
|
damageLocationsSelected.REPLACE;
|
||||||
|
windShieldOptions.selectedWindshieldReplaceOptions.push(
|
||||||
|
damageLocationsSelected.PASSENGER
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return windShieldOptions;
|
||||||
|
},
|
||||||
|
|
||||||
|
getDoorSidesFromStore() {
|
||||||
|
var doorSides = [];
|
||||||
|
if (
|
||||||
|
useMainStore().order.damage.glassToReplace?.some((glass) => {
|
||||||
|
return glass.glassLocation === damageLocationsSelected.DRIVER;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
doorSides.push(damageLocationsSelected.DRIVERSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
useMainStore().order.damage.glassToReplace?.some((glass) => {
|
||||||
|
return glass.glassLocation === damageLocationsSelected.PASSENGER;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
doorSides.push(damageLocationsSelected.PASSENGERSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return doorSides;
|
||||||
|
},
|
||||||
|
|
||||||
|
getDriverSideReplaceOptionsFromStore() {
|
||||||
|
var driverSideReplaceOptions = [];
|
||||||
|
|
||||||
|
useMainStore().order.damage.glassToReplace?.forEach((glass) => {
|
||||||
|
if (glass.glassLocation === damageLocationsSelected.DRIVER) {
|
||||||
|
driverSideReplaceOptions.push(glass.glassName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return driverSideReplaceOptions;
|
||||||
|
},
|
||||||
|
|
||||||
|
getPassengerSideReplaceOptionsFromStore() {
|
||||||
|
var passengerSideReplaceOptions = [];
|
||||||
|
|
||||||
|
useMainStore().order.damage.glassToReplace?.forEach((glass) => {
|
||||||
|
if (glass.glassLocation === damageLocationsSelected.PASSENGER) {
|
||||||
|
passengerSideReplaceOptions.push(glass.glassName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return passengerSideReplaceOptions;
|
||||||
|
},
|
||||||
|
|
||||||
|
getRearReplaceOptionsFromStore() {
|
||||||
|
var rearReplaceOptions = useMainStore().order.damage.glassToReplace?.filter(
|
||||||
|
(glass) => glass.glassLocation === damageLocationsSelected.REAR
|
||||||
|
)[0]?.glassName;
|
||||||
|
|
||||||
|
return rearReplaceOptions;
|
||||||
|
},
|
||||||
|
|
||||||
|
async forwardButtonAction() {
|
||||||
|
await this.dispatchStoreAction(
|
||||||
|
this.storeActions.SAVE_VEHICLE_DAMAGE,
|
||||||
|
{
|
||||||
|
isWindshieldRepair: this.isWindshieldRepair,
|
||||||
|
selectedGlassToReplace: this.selectedGlassToReplace(),
|
||||||
|
selectedWindshieldChipCount:
|
||||||
|
this.selectedWindshieldOptions.selectedWindshieldChipCount,
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.navigateForward();
|
||||||
|
},
|
||||||
|
|
||||||
|
navigateForward() {
|
||||||
|
// If vin already exists, navigate directly to vin-lookup
|
||||||
|
if (useMainStore().order.vehicle.vin) {
|
||||||
|
this.$router.navigate(
|
||||||
|
this.navigationScenarios.CLICKED_FORWARD_WITH_VIN,
|
||||||
|
this.$route
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.$router.navigate(
|
||||||
|
this.navigationScenarios.CLICKED_FORWARD_WITHOUT_VIN,
|
||||||
|
this.$route
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
selectedGlassToReplace() {
|
||||||
|
const selectedGlassToReplace = [];
|
||||||
|
if (this.isWindshieldDamageLocation && !this.isWindshieldRepair) {
|
||||||
|
this.selectedWindshieldOptions.selectedWindshieldReplaceOptions.forEach(
|
||||||
|
(wsItem) => {
|
||||||
|
selectedGlassToReplace.push({
|
||||||
|
glassLocation: damageLocationsSelected.WINDSHIELD,
|
||||||
|
glassName: wsItem,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isDriverSideReplace) {
|
||||||
|
this.sideDoorOptionsData.selectedDriverSideReplaceOptions.forEach((driverItem) => {
|
||||||
|
selectedGlassToReplace.push({
|
||||||
|
glassLocation: damageLocationsSelected.DRIVER,
|
||||||
|
glassName: driverItem,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isPassengerSideReplace) {
|
||||||
|
this.sideDoorOptionsData.selectedPassengerSideReplaceOptions.forEach(
|
||||||
|
(passengerItem) => {
|
||||||
|
selectedGlassToReplace.push({
|
||||||
|
glassLocation: damageLocationsSelected.PASSENGER,
|
||||||
|
glassName: passengerItem,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isRearWindowDamageLocation) {
|
||||||
|
selectedGlassToReplace.push({
|
||||||
|
glassLocation: damageLocationsSelected.REAR,
|
||||||
|
glassName: this.selectedRearReplaceOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedGlassToReplace;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isWindshieldDamageLocation() {
|
||||||
|
return this.selectedDamageLocations.some((selectedDamages) => {
|
||||||
|
return selectedDamages.toUpperCase() === damageLocationsCms.WINDSHIELD;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isSideDoorDamageLocation() {
|
||||||
|
return this.selectedDamageLocations.some((selectedDamages) => {
|
||||||
|
return selectedDamages.toUpperCase() === damageLocationsCms.SIDEDOOR;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isRearWindowDamageLocation() {
|
||||||
|
return this.selectedDamageLocations.some((selectedDamages) => {
|
||||||
|
return selectedDamages.toUpperCase() === damageLocationsCms.REARWINDOW;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isWindshieldRepair() {
|
||||||
|
return (
|
||||||
|
this.isWindshieldDamageLocation &&
|
||||||
|
this.selectedWindshieldOptions.selectedWindshieldDamageType ===
|
||||||
|
damageLocationsSelected.REPAIR
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isDriverSideReplace() {
|
||||||
|
if (!this.isSideDoorDamageLocation) return false;
|
||||||
|
|
||||||
|
return this.sideDoorOptionsData.selectedDoorSides.some((selectedDriverSide) => {
|
||||||
|
return selectedDriverSide.toUpperCase() === damageLocationsCms.DRIVERSIDE;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isPassengerSideReplace() {
|
||||||
|
if (!this.isSideDoorDamageLocation) return false;
|
||||||
|
|
||||||
|
return this.sideDoorOptionsData.selectedDoorSides.some((selectedPassengerSide) => {
|
||||||
|
return selectedPassengerSide.toUpperCase() === damageLocationsCms.PASSENGERSIDE;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
hasRepairReplaceConflict() {
|
||||||
|
return (
|
||||||
|
this.isWindshieldDamageLocation &&
|
||||||
|
this.selectedDamageLocations.length > 1 &&
|
||||||
|
this.isWindshieldRepair
|
||||||
|
);
|
||||||
|
},
|
||||||
|
hasSplitSingleConflict() {
|
||||||
|
if (
|
||||||
|
!this.selectedDamageLocations?.includes("Windshield") ||
|
||||||
|
this.selectedWindshieldOptions.selectedWindshieldDamageType ===
|
||||||
|
damageLocationsSelected.REPAIR ||
|
||||||
|
!this.selectedWindshieldOptions.selectedWindshieldReplaceOptions
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.selectedWindshieldOptions.selectedWindshieldReplaceOptions?.some(
|
||||||
|
(selectedSingleWindshield) => {
|
||||||
|
return (
|
||||||
|
selectedSingleWindshield.toUpperCase() ===
|
||||||
|
damageLocationsSelected.SINGLE.toUpperCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
) &&
|
||||||
|
(this.selectedWindshieldOptions.selectedWindshieldReplaceOptions?.some(
|
||||||
|
(selectedDriverWindshield) => {
|
||||||
|
return (
|
||||||
|
selectedDriverWindshield.toUpperCase() ===
|
||||||
|
damageLocationsSelected.DRIVER.toUpperCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
) ||
|
||||||
|
this.selectedWindshieldOptions.selectedWindshieldReplaceOptions?.some(
|
||||||
|
(selectedPassengerWindshield) => {
|
||||||
|
return (
|
||||||
|
selectedPassengerWindshield.toUpperCase() ===
|
||||||
|
damageLocationsSelected.PASSENGER.toUpperCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
shouldDisplayVehicleChangeAlert() {
|
||||||
|
return this.$route.params[this.routerParams.DISPLAY_VEHICLE_CHANGE_ALERT];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
siteHeader,
|
||||||
|
siteFooter,
|
||||||
|
vehicleBanner,
|
||||||
|
siteSubHeader,
|
||||||
|
sideDoorOptions,
|
||||||
|
damageLocationQuestion,
|
||||||
|
windshieldOptions,
|
||||||
|
replaceOptionsQuestion,
|
||||||
|
Form,
|
||||||
|
alert,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
<template>
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<div class="windshield-chip-count-question" v-if="isAvailable" aria-live="polite">
|
||||||
|
<buttonQuestion
|
||||||
|
:questionText="questionText"
|
||||||
|
:answers="answersFromCms"
|
||||||
|
:groupName="groupName"
|
||||||
|
buttonTypeString="listButtonHorizontal"
|
||||||
|
useTextForValue
|
||||||
|
v-model="selectedValue"
|
||||||
|
:validationRules="validationRules"
|
||||||
|
isRequired />
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import buttonQuestion from "@/common-components/button-question/button-question";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "windshieldOptions",
|
||||||
|
props: {
|
||||||
|
modelValue: [String, Number],
|
||||||
|
groupName: String,
|
||||||
|
isAvailable: Boolean,
|
||||||
|
validationRules: String,
|
||||||
|
cmsWidgetName: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
questionText() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
|
||||||
|
},
|
||||||
|
answersFromCms() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "Answers");
|
||||||
|
},
|
||||||
|
selectedValue: {
|
||||||
|
get: function () {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
const numberValue = Number(newValue);
|
||||||
|
this.$emit("update:modelValue", numberValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
buttonQuestion,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
<template>
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<div class="windshield-damage-type-question" v-if="isAvailable" aria-live="polite">
|
||||||
|
<buttonQuestion
|
||||||
|
:questionText="questionText"
|
||||||
|
:answers="answersFromCms"
|
||||||
|
:groupName="groupName"
|
||||||
|
buttonTypeString="listCard"
|
||||||
|
v-model="selectedValues"
|
||||||
|
:suppressError="suppressError"
|
||||||
|
:validationRules="validationRules"
|
||||||
|
isRequired />
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import buttonQuestion from "@/common-components/button-question/button-question";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "windshieldDamageTypeQuestion",
|
||||||
|
props: {
|
||||||
|
modelValue: String,
|
||||||
|
groupName: String,
|
||||||
|
isAvailable: Boolean,
|
||||||
|
suppressError: Boolean,
|
||||||
|
validationRules: String,
|
||||||
|
cmsWidgetName: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
questionText() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
|
||||||
|
},
|
||||||
|
answersFromCms() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "Answers");
|
||||||
|
},
|
||||||
|
selectedValues: {
|
||||||
|
get: function () {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.$emit("update:modelValue", newValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
buttonQuestion,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
<template>
|
||||||
|
<div class="windshield-options">
|
||||||
|
<windshieldDamageTypeQuestion
|
||||||
|
cmsWidgetName="WindshieldDamageTypeQuestion"
|
||||||
|
:isAvailable="isWindshieldDamageLocation"
|
||||||
|
:suppressError="hasRepairReplaceConflict || showNoReplacementAvailableError"
|
||||||
|
groupName="WindshieldDamageTypeQuestion"
|
||||||
|
v-model="selectedWindshieldDamageTypeValue"
|
||||||
|
:validationRules="windshieldDamageTypeQuestionValidationRules" />
|
||||||
|
<alert
|
||||||
|
v-if="showNoReplacementAvailableError"
|
||||||
|
class="my-3"
|
||||||
|
cmsWidgetName="NoReplacementAvailableError"
|
||||||
|
alertClass="alert-danger"
|
||||||
|
:isDismissible="false" />
|
||||||
|
<windshieldChipCountQuestion
|
||||||
|
cmsWidgetName="WindshieldChipCountQuestion"
|
||||||
|
:isAvailable="isRepairOptionSelected && !hasRepairReplaceConflict"
|
||||||
|
groupName="WindshieldChipCountQuestion"
|
||||||
|
v-model="selectedWindshieldChipCountValues"
|
||||||
|
validationRules="windshield-chip-count-required" />
|
||||||
|
<replaceOptionsQuestion
|
||||||
|
ref="replaceOptionsQuestion"
|
||||||
|
cmsWidgetName="WindshieldReplaceOptionsQuestion"
|
||||||
|
:isAvailable="isReplaceOptionSelected"
|
||||||
|
isMultiSelect
|
||||||
|
groupName="WindshieldReplaceOptions"
|
||||||
|
v-model="selectedWindshieldReplaceOptionsValues"
|
||||||
|
validationRules="windshield-replace-options-required|prevent-split-and-single-together"
|
||||||
|
:suppressError="hasSplitSingleConflict"
|
||||||
|
isRequired />
|
||||||
|
<alert
|
||||||
|
v-if="hasSplitSingleConflict"
|
||||||
|
class="mt-5"
|
||||||
|
cmsWidgetName="SplitSingleConflict"
|
||||||
|
alertClass="alert-danger"
|
||||||
|
:isDismissible="false" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import windshieldDamageTypeQuestion from "@/layouts/vehicle-damage/windshield-options/windshield-damage-type-question/windshield-damage-type-question";
|
||||||
|
import windshieldChipCountQuestion from "@/layouts/vehicle-damage/windshield-options/windshield-chip-count-question/windshield-chip-count-question";
|
||||||
|
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question";
|
||||||
|
import alert from "@/ux-components/alert/alert";
|
||||||
|
|
||||||
|
import { defineRule } from "vee-validate";
|
||||||
|
import { required } from "@/helpers/validation-rules";
|
||||||
|
import { errorMessages } from "@/constants/error-messages";
|
||||||
|
import { damageLocationsSelected } from "@/constants/damage-locations-selected.js";
|
||||||
|
|
||||||
|
// DEFINE VALIDATION RULES
|
||||||
|
defineRule(
|
||||||
|
"windshield-damage-type-required",
|
||||||
|
required(errorMessages.WINDSHIELD_DAMAGE_TYPE_REQUIRED)
|
||||||
|
);
|
||||||
|
defineRule(
|
||||||
|
"windshield-chip-count-required",
|
||||||
|
required(errorMessages.WINDSHIELD_CHIP_COUNT_REQUIRED)
|
||||||
|
);
|
||||||
|
defineRule(
|
||||||
|
"windshield-replace-options-required",
|
||||||
|
required(errorMessages.WINSHIELD_REPLACE_OPTIONS_REQUIRED)
|
||||||
|
);
|
||||||
|
|
||||||
|
defineRule(
|
||||||
|
"check-for-repair-and-replace",
|
||||||
|
(selectedWindshieldDamageType, selectedDamageLocations) => {
|
||||||
|
return (
|
||||||
|
selectedWindshieldDamageType.toString() != damageLocationsSelected.REPAIR ||
|
||||||
|
(!selectedDamageLocations.includes(damageLocationsSelected.WINDSHIELD) &&
|
||||||
|
!selectedDamageLocations[0]?.includes(damageLocationsSelected.WINDSHIELD)) ||
|
||||||
|
selectedDamageLocations[0].length === 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
defineRule("repair-only", (value) => {
|
||||||
|
return value.toString() === damageLocationsSelected.REPAIR;
|
||||||
|
});
|
||||||
|
defineRule("prevent-split-and-single-together", (value) => {
|
||||||
|
if (
|
||||||
|
value.toString().toUpperCase().includes(damageLocationsSelected.SINGLE.toUpperCase()) &&
|
||||||
|
(value.toString().toUpperCase().includes(damageLocationsSelected.DRIVER.toUpperCase()) ||
|
||||||
|
value
|
||||||
|
.toString()
|
||||||
|
.toUpperCase()
|
||||||
|
.includes(damageLocationsSelected.PASSENGER.toUpperCase()))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "windshieldOptions",
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
windshieldAvailableReplacementOptions: Object,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
modelValue: Object,
|
||||||
|
selectedDamageLocations: Array,
|
||||||
|
hasRepairReplaceConflict: Boolean,
|
||||||
|
hasSplitSingleConflict: Boolean,
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
initializeComponent(windshieldAvailableReplacementOptions) {
|
||||||
|
this.windshieldAvailableReplacementOptions = windshieldAvailableReplacementOptions;
|
||||||
|
this.$refs.replaceOptionsQuestion.initializeComponent(
|
||||||
|
windshieldAvailableReplacementOptions
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getWindshieldOptions(
|
||||||
|
selectedWindshieldDamageType,
|
||||||
|
selectedWindshieldChipCount,
|
||||||
|
selectedWindshieldReplaceOptions
|
||||||
|
) {
|
||||||
|
// ONLY UPDATE THE NEW VALUE IF IT IS TRUTHY (NOT NULL)
|
||||||
|
return {
|
||||||
|
selectedWindshieldDamageType: selectedWindshieldDamageType
|
||||||
|
? selectedWindshieldDamageType
|
||||||
|
: this.selectedValues.selectedWindshieldDamageType,
|
||||||
|
selectedWindshieldChipCount: selectedWindshieldChipCount
|
||||||
|
? selectedWindshieldChipCount
|
||||||
|
: this.selectedValues.selectedWindshieldChipCount,
|
||||||
|
selectedWindshieldReplaceOptions: selectedWindshieldReplaceOptions
|
||||||
|
? selectedWindshieldReplaceOptions
|
||||||
|
: this.selectedValues.selectedWindshieldReplaceOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selectedValues: {
|
||||||
|
get: function () {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.$emit("update:modelValue", newValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectedWindshieldDamageTypeValue: {
|
||||||
|
get: function () {
|
||||||
|
return this.selectedDamageLocations.includes(damageLocationsSelected.WINDSHIELD)
|
||||||
|
? this.selectedValues.selectedWindshieldDamageType
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.selectedValues = this.getWindshieldOptions(newValue, null, null);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectedWindshieldChipCountValues: {
|
||||||
|
get: function () {
|
||||||
|
return this.selectedValues.selectedWindshieldChipCount;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.selectedValues = this.getWindshieldOptions(
|
||||||
|
this.selectedWindshieldDamageTypeValue,
|
||||||
|
newValue,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectedWindshieldReplaceOptionsValues: {
|
||||||
|
get: function () {
|
||||||
|
return this.selectedValues.selectedWindshieldReplaceOptions;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.selectedValues = this.getWindshieldOptions(
|
||||||
|
this.selectedWindshieldDamageTypeValue,
|
||||||
|
null,
|
||||||
|
newValue
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isWindshieldDamageLocation() {
|
||||||
|
return this.selectedDamageLocations.some(
|
||||||
|
(selectedDamageLocation) =>
|
||||||
|
selectedDamageLocation === damageLocationsSelected.WINDSHIELD
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isRepairOptionSelected() {
|
||||||
|
if (!this.selectedWindshieldDamageTypeValue) return false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.selectedWindshieldDamageTypeValue === damageLocationsSelected.REPAIR &&
|
||||||
|
this.isWindshieldDamageLocation
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isReplaceOptionSelected() {
|
||||||
|
if (!this.selectedWindshieldDamageTypeValue) return false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.selectedWindshieldDamageTypeValue === damageLocationsSelected.REPLACE &&
|
||||||
|
this.isWindshieldDamageLocation
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isWindshieldReplaceAvailable() {
|
||||||
|
return !(
|
||||||
|
Array.isArray(this.windshieldAvailableReplacementOptions) &&
|
||||||
|
this.windshieldAvailableReplacementOptions.length < 1
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isSplitWindshieldOption() {
|
||||||
|
const options = this.windshieldAvailableReplacementOptions.toString().toUpperCase();
|
||||||
|
if (options.includes("DRIVER") && options.includes("PASSENGER")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
showNoReplacementAvailableError() {
|
||||||
|
if (this.isReplaceOptionSelected && !this.isWindshieldReplaceAvailable) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
windshieldDamageTypeQuestionValidationRules() {
|
||||||
|
// Note: the validation rules string is not dynamic (it cannot be changed once component has been created)
|
||||||
|
let validationRules =
|
||||||
|
"windshield-damage-type-required|check-for-repair-and-replace:@DamageLocationQuestion";
|
||||||
|
// if vehicle has no windshield replacement option
|
||||||
|
if (!this.isWindshieldReplaceAvailable) {
|
||||||
|
validationRules = validationRules.concat("|repair-only");
|
||||||
|
}
|
||||||
|
return validationRules;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
windshieldDamageTypeQuestion,
|
||||||
|
windshieldChipCountQuestion,
|
||||||
|
replaceOptionsQuestion,
|
||||||
|
alert,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
<siteSubHeader
|
<siteSubHeader
|
||||||
cmsWidgetName="SiteSubHeaderWidget"
|
cmsWidgetName="SiteSubHeaderWidget"
|
||||||
:hasBackButton="true"
|
:hasBackButton="true"
|
||||||
|
:backButtonAccessibleText="backButtonAccessibleText"
|
||||||
@click-event="backButtonAction" />
|
@click-event="backButtonAction" />
|
||||||
<div class="fade-on-route-transition">
|
<div class="fade-on-route-transition">
|
||||||
<styleQuestion
|
<styleQuestion
|
||||||
|
|
@ -92,5 +93,12 @@ export default {
|
||||||
siteSubHeader,
|
siteSubHeader,
|
||||||
vehicleBanner,
|
vehicleBanner,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
backButtonAccessibleText()
|
||||||
|
{
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "BackButtonAccessibleText")
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { navigationScenarios } from "@/router/router-constants/navigation-scenar
|
||||||
import { vehicleCategories } from "@/constants/vehicle-categories.js";
|
import { vehicleCategories } from "@/constants/vehicle-categories.js";
|
||||||
import { queryStrings } from "@/constants/query-strings";
|
import { queryStrings } from "@/constants/query-strings";
|
||||||
import { dynamicStrings } from "@/constants/dynamic-strings";
|
import { dynamicStrings } from "@/constants/dynamic-strings";
|
||||||
|
import { routerParams } from "@/router/router-constants/router-params";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -43,5 +44,8 @@ export default {
|
||||||
cssClassNameForCmsWidget(){
|
cssClassNameForCmsWidget(){
|
||||||
return "widget-name-" + this.cmsWidgetName;
|
return "widget-name-" + this.cmsWidgetName;
|
||||||
},
|
},
|
||||||
|
routerParams() {
|
||||||
|
return routerParams;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
40
src/mixins/input-button-wrapper-mixin.js
Normal file
40
src/mixins/input-button-wrapper-mixin.js
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { inputButtonProps } from "@/common-components/base-input-button/button-functionality-props";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
model: {
|
||||||
|
prop: "modelValue",
|
||||||
|
event: "change",
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
...inputButtonProps,
|
||||||
|
buttonLabel: [Number, String],
|
||||||
|
buttonLabelSubCopy: String,
|
||||||
|
buttonBodyCopy: String,
|
||||||
|
buttonAuxillaryCopy: String,
|
||||||
|
buttonFooterCopy: String,
|
||||||
|
buttonImage: String,
|
||||||
|
buttonImageId: String,
|
||||||
|
altText: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
textPosition: String,
|
||||||
|
screenReaderOnlyText: String,
|
||||||
|
isWide: Boolean,
|
||||||
|
additionalButtonStyling: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selectedValue: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(e) {
|
||||||
|
if (this.preHandleAnswerChange) {
|
||||||
|
this.preHandleAnswerChange(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit("update:modelValue", e);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -52,10 +52,27 @@ const routingTable = function(store) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: navigationScenarios.SELECTED_STYLE,
|
scenario: navigationScenarios.SELECTED_STYLE,
|
||||||
destinationIssPageValue: issPageValues.WELCOME_PAGE,
|
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
issPageValue: issPageValues.VEHICLE_DAMAGE,
|
||||||
|
maps: [
|
||||||
|
{
|
||||||
|
scenario: navigationScenarios.CLICKED_BACK,
|
||||||
|
destinationIssPageValue: issPageValues.VEHICLE_STYLE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: navigationScenarios.CLICKED_FORWARD_WITH_VIN,
|
||||||
|
destinationIssPageValue: issPageValues.WELCOME_PAGE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: navigationScenarios.CLICKED_FORWARD_WITHOUT_VIN,
|
||||||
|
destinationIssPageValue: issPageValues.WELCOME_PAGE,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
issPageValue: issPageValues.WELCOME_PAGE,
|
issPageValue: issPageValues.WELCOME_PAGE,
|
||||||
maps: [
|
maps: [
|
||||||
|
|
|
||||||
5
src/router/router-params.js
Normal file
5
src/router/router-params.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
const routerParams = {
|
||||||
|
DISPLAY_VEHICLE_CHANGE_ALERT: "displayVehicleChangeAlert",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { routerParams };
|
||||||
|
|
@ -30,6 +30,14 @@ const getDefaultState = () => {
|
||||||
lastName: null,
|
lastName: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
damage: {
|
||||||
|
isRepair: null,
|
||||||
|
numberOfChips: null,
|
||||||
|
glassToReplace: null,
|
||||||
|
partQuestionAnswers: null,
|
||||||
|
moldingQuestionAnswers: null,
|
||||||
|
capabilityQuestionAnswers: null,
|
||||||
|
},
|
||||||
referralNumber: null,
|
referralNumber: null,
|
||||||
referralDate: null,
|
referralDate: null,
|
||||||
accountNumber: 0,
|
accountNumber: 0,
|
||||||
|
|
@ -56,6 +64,8 @@ export const useMainStore = defineStore({
|
||||||
id: storeId,
|
id: storeId,
|
||||||
state: () => state,
|
state: () => state,
|
||||||
getters: {
|
getters: {
|
||||||
|
vehicle: (state) => state.order.vehicle,
|
||||||
|
damage: (state) => state.order.damage,
|
||||||
eventBusItem: (state) => ( eventCategory, eventSubCategory) => {
|
eventBusItem: (state) => ( eventCategory, eventSubCategory) => {
|
||||||
|
|
||||||
const matchedEvent = state.applicationUser.eventBus.find(
|
const matchedEvent = state.applicationUser.eventBus.find(
|
||||||
|
|
@ -190,6 +200,13 @@ export const useMainStore = defineStore({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getDamageOptions(carId) {
|
||||||
|
return globalMethods.callHttpClient({
|
||||||
|
methods: endpoints.GetDamageOptions.method,
|
||||||
|
endpoint: `${endpoints.GetDamageOptions.url}/${carId}`,
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
},
|
||||||
setVehicle() {
|
setVehicle() {
|
||||||
return globalMethods
|
return globalMethods
|
||||||
.callHttpClient({
|
.callHttpClient({
|
||||||
|
|
@ -204,7 +221,11 @@ export const useMainStore = defineStore({
|
||||||
},
|
},
|
||||||
|
|
||||||
updateVehicle(vehicle) {
|
updateVehicle(vehicle) {
|
||||||
this.order.vehicle = { ...this.order.vehicle, ...vehicle };
|
this.order.vehicle.carId = vehicle.carId;
|
||||||
|
this.order.vehicle.category = vehicle.category;
|
||||||
|
this.order.vehicle.imageUrl = vehicle.imageUrl;
|
||||||
|
this.order.vehicle.imageVifNumber = vehicle.imageVifNumber;
|
||||||
|
this.order.vehicle.imageColor = vehicle.imageColor;
|
||||||
},
|
},
|
||||||
|
|
||||||
resetVehicleState()
|
resetVehicleState()
|
||||||
|
|
@ -342,6 +363,9 @@ export const useMainStore = defineStore({
|
||||||
},
|
},
|
||||||
logCustomEvent({ userId, sessionKey, pageName, sessionId, category, action, label, value, shouldUseSessionId, experimentsForUser})
|
logCustomEvent({ userId, sessionKey, pageName, sessionId, category, action, label, value, shouldUseSessionId, experimentsForUser})
|
||||||
{
|
{
|
||||||
|
if ( pageName == null || pageName.length == 0 )
|
||||||
|
pageName = "none";
|
||||||
|
|
||||||
var payload = {
|
var payload = {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
sessionKey: sessionKey,
|
sessionKey: sessionKey,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ describe("Store", () => {
|
||||||
store = useMainStore();
|
store = useMainStore();
|
||||||
store.applicationUser.eventBus = [];
|
store.applicationUser.eventBus = [];
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
||||||
it("Should Store Vehicle Year", () => {
|
it("Should Store Vehicle Year", () => {
|
||||||
|
|
@ -114,7 +114,7 @@ describe("Store", () => {
|
||||||
|
|
||||||
expect(store.order.vehicle.imageUrl).toEqual(response.data.imageUrl);
|
expect(store.order.vehicle.imageUrl).toEqual(response.data.imageUrl);
|
||||||
expect(store.order.vehicle.imageVifNumber).toEqual(response.data.imageVifNumber);
|
expect(store.order.vehicle.imageVifNumber).toEqual(response.data.imageVifNumber);
|
||||||
expect(store.order.vehicle.imageVifColor).toEqual(response.data.imageVifColor);
|
expect(store.order.vehicle.style).toEqual(response.data.style);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("setVehicle should call globalMethods.callHttpClient", () => {
|
it("setVehicle should call globalMethods.callHttpClient", () => {
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,7 @@
|
||||||
@mixin blue-gradient {
|
@mixin blue-gradient {
|
||||||
background: linear-gradient(270deg, $blue 0%, $blue-800 100%);
|
background: linear-gradient(270deg, $blue 0%, $blue-800 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin box-shadow-hover($color) {
|
||||||
|
box-shadow: 0 0 0 4px $color;
|
||||||
|
}
|
||||||
|
|
|
||||||
15
src/styles/shared-input-button-styles.scss
Normal file
15
src/styles/shared-input-button-styles.scss
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
.base-input-button {
|
||||||
|
&:not(.has-error):hover {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.list-button,
|
||||||
|
&.list-button-horizontal,
|
||||||
|
&.list-card {
|
||||||
|
&:not(.selected) {
|
||||||
|
position: relative;
|
||||||
|
z-index: 4;
|
||||||
|
@include box-shadow-hover($blue-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,186 +1,189 @@
|
||||||
//Colors
|
//Colors
|
||||||
|
|
||||||
// White/black
|
// White/black
|
||||||
$white: #FFFFFF;
|
$white: #ffffff;
|
||||||
$black: #000000;
|
$black: #000000;
|
||||||
|
|
||||||
// Blues
|
// Blues
|
||||||
$blue-100: #E4F1F7;// Used in theme
|
$blue-100: #e4f1f7; // Used in theme
|
||||||
$blue-200: #C1DFEE;
|
$blue-200: #c1dfee;
|
||||||
$blue-300: #9FCEE6;
|
$blue-300: #9fcee6;
|
||||||
$blue-400: #69ADCF;// Used in theme
|
$blue-400: #69adcf; // Used in theme
|
||||||
$blue-500: #3B8FB8;
|
$blue-500: #3b8fb8;
|
||||||
$blue: #1574A1;// Default Blue
|
$blue: #1574a1; // Default Blue
|
||||||
$blue-700: #06577C;
|
$blue-700: #06577c;
|
||||||
$blue-800: #003D58;
|
$blue-800: #003d58;
|
||||||
$blue-900: #002433;// Used in theme
|
$blue-900: #002433; // Used in theme
|
||||||
|
|
||||||
// Reds
|
// Reds
|
||||||
$red-100: #FFE6E4;
|
$red-100: #ffe6e4;
|
||||||
$red-200: #FCBFBB;
|
$red-200: #fcbfbb;
|
||||||
$red-300: #F89892;
|
$red-300: #f89892;
|
||||||
$red-400: #E65C53;
|
$red-400: #e65c53;
|
||||||
$red: #D4281C;// Default Red
|
$red: #d4281c; // Default Red
|
||||||
$red-600: #AC160B;
|
$red-600: #ac160b;
|
||||||
$red-700: #840900;
|
$red-700: #840900;
|
||||||
$red-800: #5B0600;
|
$red-800: #5b0600;
|
||||||
$red-900: #330300;
|
$red-900: #330300;
|
||||||
|
|
||||||
// Greens
|
// Greens
|
||||||
$green-100: #E3F2EA;
|
$green-100: #e3f2ea;
|
||||||
$green-200: #BCEDD4;
|
$green-200: #bcedd4;
|
||||||
$green-300: #94D7B6;
|
$green-300: #94d7b6;
|
||||||
$green-400: #5ABC8C;// Used in theme
|
$green-400: #5abc8c; // Used in theme
|
||||||
$green-500: #2CA168;
|
$green-500: #2ca168;
|
||||||
$green: #0C7E47;// Default Green
|
$green: #0c7e47; // Default Green
|
||||||
$green-700: #006A36;
|
$green-700: #006a36;
|
||||||
$green-800: #004F28;
|
$green-800: #004f28;
|
||||||
$green-900: #00331A;
|
$green-900: #00331a;
|
||||||
|
|
||||||
// Yellows
|
// Yellows
|
||||||
$yellow-100: #FFF5EB;
|
$yellow-100: #fff5eb;
|
||||||
$yellow-200: #FFE1C6;
|
$yellow-200: #ffe1c6;
|
||||||
$yellow-300: #FFCAA0;
|
$yellow-300: #ffcaa0;
|
||||||
$yellow-400: #F5975D;
|
$yellow-400: #f5975d;
|
||||||
$yellow: #E86421;// Default Yellow
|
$yellow: #e86421; // Default Yellow
|
||||||
$yellow-600: #BB4B06;
|
$yellow-600: #bb4b06;
|
||||||
$yellow-700: #8E3C00;
|
$yellow-700: #8e3c00;
|
||||||
$yellow-800: #602D00;
|
$yellow-800: #602d00;
|
||||||
$yellow-900: #331A00;
|
$yellow-900: #331a00;
|
||||||
|
|
||||||
// Grays
|
// Grays
|
||||||
$gray-100: #F5F5F5;// Used in theme
|
$gray-100: #f5f5f5; // Used in theme
|
||||||
$gray-200: #E3E4E4;// Used in theme
|
$gray-200: #e3e4e4; // Used in theme
|
||||||
$gray-300: #D2D4D4;
|
$gray-300: #d2d4d4;
|
||||||
$gray: #B0B3B3;// Default Gray
|
$gray: #b0b3b3; // Default Gray
|
||||||
$gray-500: #8E9292;// Used in theme
|
$gray-500: #8e9292; // Used in theme
|
||||||
$gray-550: #727676;// Used in theme
|
$gray-550: #727676; // Used in theme
|
||||||
$gray-600: #4D5151;// Used in theme
|
$gray-600: #4d5151; // Used in theme
|
||||||
$gray-700: #303333;// Used in theme
|
$gray-700: #303333; // Used in theme
|
||||||
$gray-800: #222424;// Used in theme
|
$gray-800: #222424; // Used in theme
|
||||||
$gray-900: #181A1A;// Used in theme
|
$gray-900: #181a1a; // Used in theme
|
||||||
|
|
||||||
// Miscellaneous Colors
|
// Miscellaneous Colors
|
||||||
$indigo: #6610f2;
|
$indigo: #6610f2;
|
||||||
$purple: #6f42c1;
|
$purple: #6f42c1;
|
||||||
$pink: #d63384;
|
$pink: #d63384;
|
||||||
$orange: #fd7e14;
|
$orange: #fd7e14;
|
||||||
$teal: #20c997;
|
$teal: #20c997;
|
||||||
$cyan: #0dcaf0;
|
$cyan: #0dcaf0;
|
||||||
|
|
||||||
// scss-docs-start colors-map
|
// scss-docs-start colors-map
|
||||||
$colors: (
|
$colors: (
|
||||||
"blue": $blue,
|
"blue": $blue,
|
||||||
"indigo": $indigo,
|
"indigo": $indigo,
|
||||||
"purple": $purple,
|
"purple": $purple,
|
||||||
"pink": $pink,
|
"pink": $pink,
|
||||||
"red": $red,
|
"red": $red,
|
||||||
"orange": $orange,
|
"orange": $orange,
|
||||||
"yellow": $yellow,
|
"yellow": $yellow,
|
||||||
"green": $green,
|
"green": $green,
|
||||||
"teal": $teal,
|
"teal": $teal,
|
||||||
"cyan": $cyan,
|
"cyan": $cyan,
|
||||||
"white": $white,
|
"white": $white,
|
||||||
"gray": $gray,
|
"gray": $gray,
|
||||||
"gray-dark": $gray-500
|
"gray-dark": $gray-500,
|
||||||
);
|
);
|
||||||
|
|
||||||
// scss-docs-start theme-color-variables
|
// scss-docs-start theme-color-variables
|
||||||
$primary: $blue;
|
$primary: $blue;
|
||||||
$secondary: $red;
|
$secondary: $red;
|
||||||
$success: $green;
|
$success: $green;
|
||||||
$info: $cyan;
|
$info: $cyan;
|
||||||
$warning: $yellow;
|
$warning: $yellow;
|
||||||
$danger: $red;
|
$danger: $red;
|
||||||
$light: $gray-100;
|
$light: $gray-100;
|
||||||
$dark: $gray-500;
|
$dark: $gray-500;
|
||||||
|
|
||||||
// scss-docs-start theme-colors-map
|
// scss-docs-start theme-colors-map
|
||||||
$theme-colors: (
|
$theme-colors: (
|
||||||
"primary": $primary,
|
"primary": $primary,
|
||||||
"secondary": $secondary,
|
"secondary": $secondary,
|
||||||
"success": $success,
|
"success": $success,
|
||||||
"info": $info,
|
"info": $info,
|
||||||
"warning": $warning,
|
"warning": $warning,
|
||||||
"danger": $danger,
|
"danger": $danger,
|
||||||
"light": $light,
|
"light": $light,
|
||||||
"dark": $dark
|
"dark": $dark,
|
||||||
);
|
);
|
||||||
|
|
||||||
//Default font color
|
//Default font color
|
||||||
$body-color: $gray-600;
|
$body-color: $gray-600;
|
||||||
|
|
||||||
//Fonts
|
//Fonts
|
||||||
$font-family-sans-serif: Roboto, Arial, Helvetica, sans-serif;
|
$font-family-sans-serif: Roboto, Arial, Helvetica, sans-serif;
|
||||||
$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
|
||||||
|
monospace;
|
||||||
// stylelint-enable value-keyword-case
|
// stylelint-enable value-keyword-case
|
||||||
$font-family-base: $font-family-sans-serif;
|
$font-family-base: $font-family-sans-serif;
|
||||||
$font-family-code: $font-family-monospace;
|
$font-family-code: $font-family-monospace;
|
||||||
$font-size-base: 1rem; // Assumes the browser default, typically `16px`
|
$font-size-base: 1rem; // Assumes the browser default, typically `16px`
|
||||||
|
|
||||||
//Custom Font size (extra small)
|
//Custom Font size (extra small)
|
||||||
$font-size-xsm: $font-size-base * .75;
|
$font-size-xsm: $font-size-base * 0.75;
|
||||||
$font-sizes: (
|
$font-sizes: (
|
||||||
7: $font-size-xsm
|
7: $font-size-xsm,
|
||||||
);
|
);
|
||||||
|
|
||||||
//Font weight
|
//Font weight
|
||||||
$font-weight-lighter: lighter;
|
$font-weight-lighter: lighter;
|
||||||
$font-weight-light: 300;
|
$font-weight-light: 300;
|
||||||
$font-weight-normal: 400;
|
$font-weight-normal: 400;
|
||||||
$font-weight-bold: 500;
|
$font-weight-bold: 500;
|
||||||
$font-weight-bolder: bolder;
|
$font-weight-bolder: bolder;
|
||||||
|
|
||||||
//Headings
|
//Headings
|
||||||
$h1-font-size: $font-size-base * 3;
|
$h1-font-size: $font-size-base * 3;
|
||||||
$h2-font-size: $font-size-base * 2.625;
|
$h2-font-size: $font-size-base * 2.625;
|
||||||
$h3-font-size: $font-size-base * 2;
|
$h3-font-size: $font-size-base * 2;
|
||||||
$h4-font-size: $font-size-base * 1.625;
|
$h4-font-size: $font-size-base * 1.625;
|
||||||
$h5-font-size: $font-size-base * 1.25;
|
$h5-font-size: $font-size-base * 1.25;
|
||||||
$h6-font-size: $font-size-base * .875;
|
$h6-font-size: $font-size-base * 0.875;
|
||||||
|
|
||||||
//Border Radius
|
//Border Radius
|
||||||
// Helper classes are rounded, rounded-1, rounded-2, rounded-3
|
// Helper classes are rounded, rounded-1, rounded-2, rounded-3
|
||||||
$border-radius: .25rem;
|
$border-radius: 0.25rem;
|
||||||
$border-radius-sm: .2rem;
|
$border-radius-sm: 0.2rem;
|
||||||
$border-radius-lg: .5rem;//Used for buttons. Can be used for other things, of course.
|
$border-radius-lg: 0.5rem; //Used for buttons. Can be used for other things, of course.
|
||||||
$border-radius-pill: 50rem;
|
$border-radius-pill: 50rem;
|
||||||
|
|
||||||
//Spacing
|
//Spacing
|
||||||
// 8 spacers available instead of the usual 5
|
// 8 spacers available instead of the usual 5
|
||||||
$spacer: 1rem;
|
$spacer: 1rem;
|
||||||
$spacers: (
|
$spacers: (
|
||||||
0: 0,
|
0: 0,
|
||||||
1: $spacer * .25, /* 4px */
|
1: $spacer * 0.25,
|
||||||
2: $spacer * .5, /* 8px */
|
/* 4px */ 2: $spacer * 0.5,
|
||||||
3: $spacer * .75, /* 12px */
|
/* 8px */ 3: $spacer * 0.75,
|
||||||
4: $spacer * 1, /* 16px */
|
/* 12px */ 4: $spacer * 1,
|
||||||
5: $spacer * 1.5, /* 24px */
|
/* 16px */ 5: $spacer * 1.5,
|
||||||
6: $spacer * 2, /* 32px */
|
/* 24px */ 6: $spacer * 2,
|
||||||
7: $spacer * 2.5, /* 40px */
|
/* 32px */ 7: $spacer * 2.5,
|
||||||
8: $spacer * 3, /* 48px */
|
/* 40px */ 8: $spacer * 3,
|
||||||
|
/* 48px */
|
||||||
);
|
);
|
||||||
|
|
||||||
//Grid breakpoints
|
//Grid breakpoints
|
||||||
$grid-breakpoints: (
|
$grid-breakpoints: (
|
||||||
xs: 0,
|
xs: 0,
|
||||||
sm: 576px,
|
sm: 576px,
|
||||||
md: 838px,
|
md: 838px,
|
||||||
lg: 1074px,
|
lg: 1074px,
|
||||||
xl: 1416px
|
xl: 1416px,
|
||||||
);
|
);
|
||||||
|
|
||||||
//Shadow
|
//Shadow
|
||||||
$box-shadow: 0 .5rem 1rem rgba($black, .15);
|
$box-shadow: 0 0.5rem 1rem rgba($black, 0.15);
|
||||||
$box-shadow-sm: 0 .125rem .25rem rgba($black, .075);
|
$box-shadow-sm: 0 0.125rem 0.25rem rgba($black, 0.075);
|
||||||
$box-shadow-lg: 0 1rem 3rem rgba($black, .25);//Safelite default
|
$box-shadow-lg: 0 1rem 3rem rgba($black, 0.25); //Safelite default
|
||||||
$box-shadow-inset: inset 0 1px 2px rgba($black, .075);
|
$box-shadow-inset: inset 0 1px 2px rgba($black, 0.075);
|
||||||
|
|
||||||
//Alerts
|
//Alerts
|
||||||
$alert-bg-scale: -90%;
|
$alert-bg-scale: -90%;
|
||||||
$alert-border-scale: -100%;
|
$alert-border-scale: -100%;
|
||||||
$alert-color-scale: 40%;
|
$alert-color-scale: 40%;
|
||||||
|
|
||||||
//Modal animation
|
//Modal animation
|
||||||
$modal-fade-transform: translate(0, 0);
|
// This affects all [Bootstrap] modals
|
||||||
$modal-backdrop-opacity: 0;
|
$modal-fade-transform: translate(0, 100%);
|
||||||
|
$modal-backdrop-opacity: 0;
|
||||||
|
|
|
||||||
|
|
@ -1,264 +1,139 @@
|
||||||
import { shallowMount } from "@vue/test-utils";
|
import { mount } from "@vue/test-utils";
|
||||||
import listButtonHorizontal from "./list-button-horizontal";
|
import listButtonHorizontal from "./list-button-horizontal";
|
||||||
import { nextTick } from "vue";
|
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||||
|
|
||||||
describe("list-button-horizontal.vue", () => {
|
describe("list-button-horizontal.vue", () => {
|
||||||
it("Should return input type checkbox if isMultiSelect is true", async () => {
|
describe("styling/UI", () => {
|
||||||
// Act
|
it("Should return screen reader text", async () => {
|
||||||
const wrapper = shallowMount(listButtonHorizontal, {
|
// Act
|
||||||
propsData: {
|
const { wrapper } = setupMocks({
|
||||||
isMultiSelect: true,
|
mockData: {
|
||||||
},
|
propsData: {
|
||||||
|
screenReaderOnlyText: "Screen Reader Only Text",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const paragraph = wrapper.find("span.sr-only");
|
||||||
|
|
||||||
|
expect(paragraph.text()).toEqual("Screen Reader Only Text");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return text alignment class", async () => {
|
||||||
|
// Act
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
mockData: {
|
||||||
|
propsData: {
|
||||||
|
textPosition: "text-center",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const paragraph = wrapper.find("span.m-0");
|
||||||
|
|
||||||
|
expect(paragraph.attributes("class")).toContain("text-center");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return aria-required state", async () => {
|
||||||
|
// Act
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
mockData: {
|
||||||
|
propsData: {
|
||||||
|
isRequired: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const input = wrapper.find("input");
|
||||||
|
|
||||||
|
expect(input.attributes()["aria-required"]).toEqual("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is cash or insurance button => has 'strong' class", () => {
|
||||||
|
// Arrange/Act
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
mockData: {
|
||||||
|
propsData: {
|
||||||
|
additionalButtonStyling: "listButtonHorizontalStrong",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const label = wrapper.find("label");
|
||||||
|
|
||||||
|
expect(label.classes()).toContain("strong");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("has buttonLabel => displays buttonLabel", () => {
|
||||||
|
// Arrange/Act
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
mockData: {
|
||||||
|
propsData: {
|
||||||
|
buttonLabel: "Surprise!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const content = wrapper.find(".list-button-horizontal-content");
|
||||||
|
expect(content.isVisible()).toBe(true);
|
||||||
|
expect(content.text()).toContain("Surprise!");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("has buttonLabelSubCopy => displays buttonLabelSubCopy", () => {
|
||||||
|
// Arrange/Act
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
mockData: {
|
||||||
|
propsData: {
|
||||||
|
buttonLabel: "Surprise!",
|
||||||
|
buttonLabelSubCopy: "Super duper surprise :)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const content = wrapper.find(".list-button-horizontal-content");
|
||||||
|
expect(content.isVisible()).toBe(true);
|
||||||
|
expect(content.text()).toContain("Super duper surprise :)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("has screenReaderOnlyText => displays screenReaderOnlyText", () => {
|
||||||
|
// Arrange/Act
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
mockData: {
|
||||||
|
propsData: {
|
||||||
|
buttonLabel: "Surprise!",
|
||||||
|
buttonLabelSubCopy: "Super duper surprise :)",
|
||||||
|
screenReaderOnlyText: "Tests are fun!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const content = wrapper.find(".list-button-horizontal-content");
|
||||||
|
const screenReaderOnlyText = wrapper.find(".sr-only");
|
||||||
|
expect(content.isVisible()).toBe(true);
|
||||||
|
expect(screenReaderOnlyText.exists()).toBe(true);
|
||||||
|
expect(screenReaderOnlyText.text()).toContain("Tests are fun!");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
|
||||||
const input = wrapper.find("input");
|
|
||||||
|
|
||||||
expect(input.attributes().type).toEqual("checkbox");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return input type radio if isMultiSelect is false or not specified", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButtonHorizontal, {
|
|
||||||
propsData: {
|
|
||||||
isMultiSelect: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const input = wrapper.find("input");
|
|
||||||
|
|
||||||
expect(input.attributes().type).toEqual("radio");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return primary label text (buttonID)", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButtonHorizontal, {
|
|
||||||
propsData: {
|
|
||||||
buttonID: "List Card Checkbox",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const label = wrapper.find("label");
|
|
||||||
|
|
||||||
expect(label.attributes().for).toEqual("List Card Checkbox");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return screen reader text", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButtonHorizontal, {
|
|
||||||
propsData: {
|
|
||||||
screenReaderOnlyText: "Screen Reader Only Text",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const paragraph = wrapper.find("span.sr-only");
|
|
||||||
|
|
||||||
expect(paragraph.text()).toEqual("Screen Reader Only Text");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return text alignment class", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButtonHorizontal, {
|
|
||||||
propsData: {
|
|
||||||
textPosition: "text-center",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const paragraph = wrapper.find("span.m-0");
|
|
||||||
|
|
||||||
expect(paragraph.attributes("class")).toContain("text-center");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return aria-required state", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButtonHorizontal, {
|
|
||||||
propsData: {
|
|
||||||
isRequired: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const input = wrapper.find("input");
|
|
||||||
|
|
||||||
expect(input.attributes()["aria-required"]).toEqual("true");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return loader enabled true", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButtonHorizontal, {
|
|
||||||
global: {
|
|
||||||
mocks: {
|
|
||||||
'$route': { query: { issPage: 'page-name' } },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
propsData: {
|
|
||||||
selectingInitiatesLoad: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
|
|
||||||
const label = wrapper.find("label");
|
|
||||||
|
|
||||||
wrapper.vm.handleCheckChange = jest.fn();
|
|
||||||
wrapper.vm.triggerButton();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
const loader = wrapper.find("loader-stub");
|
|
||||||
|
|
||||||
expect(loader.exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return loader color", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButtonHorizontal, {
|
|
||||||
global: {
|
|
||||||
mocks: {
|
|
||||||
'$route': { query: { issPage: 'page-name' } },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
propsData: {
|
|
||||||
loaderColor: "blue",
|
|
||||||
selectingInitiatesLoad: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
|
|
||||||
const label = wrapper.find("label");
|
|
||||||
|
|
||||||
wrapper.vm.handleCheckChange = jest.fn();
|
|
||||||
wrapper.vm.triggerButton();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
const loader = wrapper.find("loader-stub");
|
|
||||||
|
|
||||||
expect(loader.attributes("class")).toContain("blue");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return loader position", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButtonHorizontal, {
|
|
||||||
global: {
|
|
||||||
mocks: {
|
|
||||||
'$route': { query: { issPage: 'page-name' } },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
propsData: {
|
|
||||||
loaderPosition: "right",
|
|
||||||
selectingInitiatesLoad: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
|
|
||||||
const label = wrapper.find("label");
|
|
||||||
|
|
||||||
wrapper.vm.handleCheckChange = jest.fn();
|
|
||||||
wrapper.vm.triggerButton();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
const loader = wrapper.find("loader-stub");
|
|
||||||
|
|
||||||
expect(loader.attributes("class")).toContain("right");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should emit button value on click", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButtonHorizontal, {
|
|
||||||
propsData: {
|
|
||||||
isRadioHorizontal: true,
|
|
||||||
buttonLabel: "Windshield",
|
|
||||||
value: "List Card Checkbox",
|
|
||||||
groupID: "radio-demo-1",
|
|
||||||
groupName: "radio 1",
|
|
||||||
buttonImage: "windshield-damage.svg",
|
|
||||||
isRequired: true,
|
|
||||||
isWide: false,
|
|
||||||
modelValue: ["List Card Checkbox"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
wrapper.vm.handleCheckChange();
|
|
||||||
// Assert
|
|
||||||
expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: Boolean, buttonId: undefined, checkValue: false}]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButtonHorizontal, {
|
|
||||||
propsData: {
|
|
||||||
isRadioHorizontal: true,
|
|
||||||
buttonLabel: "Windshield",
|
|
||||||
buttonID: "List Card Checkbox",
|
|
||||||
groupID: "radio-demo-1",
|
|
||||||
groupName: "radio 1",
|
|
||||||
buttonImage: "windshield-damage.svg",
|
|
||||||
isRequired: true,
|
|
||||||
isWide: false,
|
|
||||||
modelValue: ["List Card Checkbox"],
|
|
||||||
isMultiSelect: false,
|
|
||||||
value: "Car-Front",
|
|
||||||
selectedValues: ["Car-Front"]
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// Assert
|
|
||||||
expect(wrapper.vm.checkValue).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should run handleCheckChange if selectingInitiatesLoad is false and handleInputChange is triggered", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButtonHorizontal, {
|
|
||||||
propsData: {
|
|
||||||
selectingInitiatesLoad: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
wrapper.vm.handleInputChange();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(wrapper.vm.handleCheckChange).toBeCalled;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should do nothing if isMultiSelect is true and handleKeyupArrow is triggered", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButtonHorizontal, {
|
|
||||||
propsData: {
|
|
||||||
isMultiSelect: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
wrapper.vm.handleKeyupArrow();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(wrapper.vm.handleKeyupArrow).toHaveReturned;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should run handleCheckChange if selectingInitiatesLoad is false and handleKeyupArrow is triggered", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButtonHorizontal, {
|
|
||||||
propsData: {
|
|
||||||
selectingInitiatesLoad: false,
|
|
||||||
isMultiSelect: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
wrapper.vm.handleKeyupArrow();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(wrapper.vm.handleCheckChange).toBeCalled;
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function setupMocks({ mockData }) {
|
||||||
|
const wrapper = mount(listButtonHorizontal, {
|
||||||
|
...mockData,
|
||||||
|
propsData: {
|
||||||
|
...mockData.propsData,
|
||||||
|
groupName: "my-group",
|
||||||
|
modelValue: mockData.propsData?.isMultiSelect ? ["5"] : "5",
|
||||||
|
value: mockData.propsData?.isMultiSelect ? ["4"] : "4",
|
||||||
|
},
|
||||||
|
mixins: [inputButtonWrapperMixin],
|
||||||
|
});
|
||||||
|
|
||||||
|
return { wrapper };
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,206 +1,84 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<baseInputButton
|
||||||
class="list-group list-button-horizontal d-flex flex-column w-100"
|
v-bind="$props"
|
||||||
:class="[(errors.length > 0 || hasError) ? 'has-error' : '']"
|
:buttonWrapperClasses="[
|
||||||
@keyup.space="triggerButton()"
|
'list-group list-button-horizontal d-flex flex-column w-100 base-input-button',
|
||||||
@keyup.up="handleKeyupArrow()"
|
{ strong: isStrongStyling },
|
||||||
@keyup.down="handleKeyupArrow()"
|
]"
|
||||||
@keyup.left="handleKeyupArrow()"
|
v-model="selectedValue">
|
||||||
@keyup.right="handleKeyupArrow()"
|
<div
|
||||||
>
|
class="button-content list-button-horizontal-content d-flex flex-column justify-content-center p-3">
|
||||||
<input
|
<span class="m-0" :class="textPosition">
|
||||||
:type="isMultiSelect ? 'checkbox' : 'radio'"
|
{{ buttonLabel }}
|
||||||
:id="buttonID"
|
</span>
|
||||||
:name="groupName"
|
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition">
|
||||||
:aria-required="isRequired"
|
{{ buttonLabelSubCopy }}
|
||||||
v-model="checkValue"
|
</span>
|
||||||
:checked="checkValue"
|
<span v-if="screenReaderOnlyText" class="sr-only">
|
||||||
@change="handleInputChange()"
|
{{ screenReaderOnlyText }}
|
||||||
/>
|
</span>
|
||||||
<label
|
</div>
|
||||||
tabindex="-1"
|
</baseInputButton>
|
||||||
:for="buttonID"
|
</template>
|
||||||
:aria-label="buttonLabel"
|
|
||||||
class="d-flex flex-column justify-content-center py-3 px-4"
|
|
||||||
@mouseup="triggerButton()"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="m-0"
|
|
||||||
:class="textPosition"
|
|
||||||
>
|
|
||||||
{{buttonLabel}}
|
|
||||||
</span>
|
|
||||||
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition">
|
|
||||||
{{ buttonLabelSubCopy }}
|
|
||||||
</span>
|
|
||||||
<span v-if="screenReaderOnlyText" class="sr-only">
|
|
||||||
{{ screenReaderOnlyText }}
|
|
||||||
</span>
|
|
||||||
<loader v-if="isLoaderDisplayed && selectingInitiatesLoad" :class="[loaderColor, loaderPosition]" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { useField } from "vee-validate";
|
import baseInputButton from "@/common-components/base-input-button/base-input-button";
|
||||||
import loader from "@/ux-components/loader/loader";
|
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||||
import { toRef } from "vue";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "listButtonHorizontal",
|
name: "listButtonHorizontal",
|
||||||
props: {
|
mixins: [inputButtonWrapperMixin],
|
||||||
isMultiSelect: Boolean,
|
computed: {
|
||||||
groupName: String,
|
isStrongStyling() {
|
||||||
buttonID: String,
|
return this.additionalButtonStyling === "listButtonHorizontalStrong";
|
||||||
buttonLabel: String,
|
|
||||||
buttonLabelSubCopy: String,
|
|
||||||
screenReaderOnlyText: String,
|
|
||||||
textPosition: String,
|
|
||||||
selectingInitiatesLoad: Boolean,
|
|
||||||
loaderColor: String,
|
|
||||||
loaderPosition: String,
|
|
||||||
isRequired: Boolean,
|
|
||||||
isCashOrInsurance: Boolean,
|
|
||||||
value: {
|
|
||||||
// Field initial value
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
},
|
||||||
validationRules: String,
|
},
|
||||||
selectedValues: [Array, String],
|
components: {
|
||||||
hasError: Boolean,
|
baseInputButton,
|
||||||
valueToLogType: String,
|
},
|
||||||
},
|
};
|
||||||
data() {
|
</script>
|
||||||
return {
|
|
||||||
isLoaderDisplayed: false,
|
|
||||||
checkValue: Boolean,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
if (Array.isArray(this.selectedValues)) {
|
|
||||||
this.checkValue = this.isMultiSelect
|
|
||||||
? this.selectedValues.includes(this.value)
|
|
||||||
: this.selectedValues[0] == this.value;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.checkValue = this.selectedValues === this.value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
isValueSelectedByArray(arr) {
|
|
||||||
return this.isMultiSelect
|
|
||||||
? arr.includes(this.value)
|
|
||||||
: arr[0];
|
|
||||||
},
|
|
||||||
displayLoader() {
|
|
||||||
this.isLoaderDisplayed = true;
|
|
||||||
},
|
|
||||||
handleInputChange() {
|
|
||||||
if (!this.selectingInitiatesLoad) {
|
|
||||||
this.handleCheckChange();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleKeyupArrow() {
|
|
||||||
if (this.isMultiSelect) {
|
|
||||||
return; // Prevent arrow keys from doing anything if element is a checkbox
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.selectingInitiatesLoad) {
|
<style lang="scss">
|
||||||
this.handleCheckChange();
|
.list-button-horizontal {
|
||||||
}
|
input[type="radio"],
|
||||||
},
|
input[type="checkbox"] {
|
||||||
triggerButton() {
|
|
||||||
if (this.selectingInitiatesLoad) {
|
|
||||||
this.displayLoader();
|
|
||||||
this.handleCheckChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
handleCheckChange() {
|
|
||||||
const emitEvent = {
|
|
||||||
checkValue: this.checkValue, // only read on checkboxes, on handleCheckedChanged on button-question
|
|
||||||
value: this.value,
|
|
||||||
buttonId: this.buttonID && this.buttonID.toString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.handleChange(this.value);
|
|
||||||
this.$emit("isCheckedChanged", emitEvent);
|
|
||||||
this.$emit("update:modelValue", emitEvent);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
loader,
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const inputType = props.isMultiSelect ? "checkbox" : "radio";
|
|
||||||
|
|
||||||
const fieldOptions = {
|
|
||||||
type: inputType,
|
|
||||||
checkedValue: props.value,
|
|
||||||
potentialInitialValue: props.selectedValues,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set initialValue for validation setup if pre-selected
|
|
||||||
// NOTE: props.selectedValues could be an array of strings, or an array of integers...
|
|
||||||
if (props.selectedValues && (props.selectedValues.includes(props.value) || props.selectedValues.includes(parseInt(props.value)))) {
|
|
||||||
fieldOptions['initialValue'] = fieldOptions.potentialInitialValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
handleChange,
|
|
||||||
errors,
|
|
||||||
value
|
|
||||||
} = useField(toRef(props, "groupName"), toRef(props, "validationRules"), fieldOptions);
|
|
||||||
|
|
||||||
const validateValue = value;
|
|
||||||
return {
|
|
||||||
handleChange,
|
|
||||||
errors,
|
|
||||||
validateValue,
|
|
||||||
fieldOptions, // only need to expose this for unit test purposes
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.list-button-horizontal {
|
|
||||||
|
|
||||||
input[type="radio"],
|
|
||||||
input[type="checkbox"] {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 0;
|
height: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
|
|
||||||
&:focus-visible+label {
|
.list-button-horizontal-content {
|
||||||
box-shadow: 0 0 0 2.5px $blue;
|
cursor: pointer;
|
||||||
z-index: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus+label {
|
&:focus-visible + .list-button-horizontal-content {
|
||||||
box-shadow: 0 0 0 2.5px $blue;
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
z-index: 3;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:checked+label {
|
&:focus + .list-button-horizontal-content {
|
||||||
background: $blue-100;
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
box-shadow: 0 0 0 1px $blue;
|
z-index: 3;
|
||||||
outline: none;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:checked:focus+label {
|
&:checked + .list-button-horizontal-content {
|
||||||
box-shadow: 0 0 0 2.5px $blue;
|
background: $blue-100;
|
||||||
|
box-shadow: 0 0 0 1px $blue;
|
||||||
|
outline: none;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:checked+label p:first-child {
|
&:checked:focus + .list-button-horizontal-content {
|
||||||
font-weight: 500;
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
&:checked + .list-button-horizontal-content p:first-child {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-button-horizontal-content {
|
||||||
outline: none;
|
outline: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: $white;
|
background: $white;
|
||||||
|
|
@ -210,135 +88,131 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: $gray-600;
|
color: $gray-600;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
@include media-breakpoint-up(sm) {
|
|
||||||
box-shadow: 0 0 0 4px $blue-300;
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 4 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+p {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
span {
|
||||||
font-size: .875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cash/Insurance option radio button styling
|
// Cash/Insurance option radio button styling
|
||||||
&.radio-fancy {
|
&.strong {
|
||||||
label {
|
.list-button-horizontal-content {
|
||||||
border: 1px solid $blue-700;
|
border: 1px solid $blue-700;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
color: $blue;
|
color: $blue;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
label:hover {
|
.list-button-horizontal-content:hover {
|
||||||
background-color: $blue-700;
|
background-color: $blue-700;
|
||||||
color: $white;
|
color: $white;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="radio"],
|
input[type="radio"],
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 0;
|
height: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
|
|
||||||
&:focus-visible+label {
|
&:focus-visible + .list-button-horizontal-content {
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus+label {
|
&:focus + .list-button-horizontal-content {
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:checked+label {
|
&:checked + .list-button-horizontal-content {
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
color: $white;
|
color: $white;
|
||||||
background: linear-gradient(84.45deg, #125B7E 0%, #3B8FB8 100%);
|
background: linear-gradient(84.45deg, #125b7e 0%, #3b8fb8 100%);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:checked:focus+label {
|
&:checked:focus + .list-button-horizontal-content {
|
||||||
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
|
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:checked+label p:first-child {
|
&:checked + .list-button-horizontal-content p:first-child {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.list-button-horizontal {
|
&.list-button-horizontal {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
label {
|
label {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.col {
|
.col,
|
||||||
&:first-of-type {
|
.list-group {
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
.list-button-horizontal {
|
.list-button-horizontal {
|
||||||
label {
|
|
||||||
border-bottom-left-radius: 0.5rem;
|
border-bottom-left-radius: 0.5rem;
|
||||||
border-top-left-radius: 0.5rem;
|
border-top-left-radius: 0.5rem;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-of-type {
|
.list-button-horizontal-content {
|
||||||
|
border-bottom-left-radius: 0.5rem;
|
||||||
|
border-top-left-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
.list-button-horizontal {
|
.list-button-horizontal {
|
||||||
label {
|
|
||||||
border-bottom-right-radius: 0.5rem;
|
border-bottom-right-radius: 0.5rem;
|
||||||
border-top-right-radius: 0.5rem;
|
border-top-right-radius: 0.5rem;
|
||||||
}
|
|
||||||
|
.list-button-horizontal-content {
|
||||||
|
border-bottom-right-radius: 0.5rem;
|
||||||
|
border-top-right-radius: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
//Cash/insurance styling
|
|
||||||
&:first-of-type {
|
|
||||||
.list-button-horizontal.radio-fancy {
|
|
||||||
input[type="radio"] {
|
|
||||||
&:checked+label {
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:checked:focus+label {
|
|
||||||
border-bottom-right-radius: 0.5rem;
|
|
||||||
border-top-right-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-of-type {
|
|
||||||
.list-button-horizontal.radio-fancy {
|
|
||||||
input[type="radio"] {
|
|
||||||
&:checked+label {
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:checked:focus+label {
|
|
||||||
border-bottom-left-radius: 0.5rem;
|
|
||||||
border-top-left-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
|
//Cash/insurance styling
|
||||||
|
&:first-of-type {
|
||||||
|
.list-button-horizontal.strong {
|
||||||
|
input[type="radio"] {
|
||||||
|
&:checked + .list-button-horizontal-content {
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked:focus + .list-button-horizontal-content {
|
||||||
|
border-bottom-right-radius: 0.5rem;
|
||||||
|
border-top-right-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
.list-button-horizontal.strong {
|
||||||
|
input[type="radio"] {
|
||||||
|
&:checked + .list-button-horizontal-content {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked:focus + .list-button-horizontal-content {
|
||||||
|
border-bottom-left-radius: 0.5rem;
|
||||||
|
border-top-left-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,256 +1,304 @@
|
||||||
import { shallowMount } from "@vue/test-utils";
|
import { mount } from "@vue/test-utils";
|
||||||
import listButton from "./list-button";
|
import listButton from "./list-button";
|
||||||
import { nextTick } from "vue";
|
import { GaActions } from "@/constants/analytics";
|
||||||
|
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||||
|
|
||||||
describe("list-button.vue", () => {
|
describe("list-button.vue", () => {
|
||||||
it("Should return input type checkbox if isMultiSelect is true", async () => {
|
describe("loader", () => {
|
||||||
// Act
|
it("selectingInitiatesLoad is true and answer is changed => show the loader", async () => {
|
||||||
const wrapper = shallowMount(listButton, {
|
// Act
|
||||||
propsData: {
|
const { wrapper } = setupMocks({
|
||||||
isMultiSelect: true,
|
mockData: {
|
||||||
},
|
global: {
|
||||||
|
mocks: {
|
||||||
|
$route: { query: { fmgPage: "page-name" } },
|
||||||
|
GaActions: GaActions,
|
||||||
|
pushEventToGA: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
propsData: {
|
||||||
|
selectingInitiatesLoad: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
wrapper.vm.selectedValue = "something";
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const loader = wrapper.findComponent({ name: "loader" });
|
||||||
|
expect(loader.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return loader color", async () => {
|
||||||
|
// Arrange
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
mockData: {
|
||||||
|
global: {
|
||||||
|
mocks: {
|
||||||
|
$route: { query: { fmgPage: "page-name" } },
|
||||||
|
GaActions: GaActions,
|
||||||
|
pushEventToGA: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
propsData: {
|
||||||
|
loaderColor: "blue",
|
||||||
|
selectingInitiatesLoad: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
wrapper.vm.selectedValue = "something";
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const loader = wrapper.findComponent({ name: "loader" });
|
||||||
|
expect(loader.attributes("class")).toContain("blue");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return loader position", async () => {
|
||||||
|
// Act
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
mockData: {
|
||||||
|
global: {
|
||||||
|
mocks: {
|
||||||
|
$route: { query: { fmgPage: "page-name" } },
|
||||||
|
GaActions: GaActions,
|
||||||
|
pushEventToGA: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
propsData: {
|
||||||
|
loaderPosition: "right",
|
||||||
|
selectingInitiatesLoad: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
wrapper.vm.selectedValue = "something";
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const loader = wrapper.findComponent({ name: "loader" });
|
||||||
|
expect(loader.attributes("class")).toContain("right");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
describe("baseInputButton checks", () => {
|
||||||
const input = wrapper.find("input");
|
it("Should return input type checkbox if isMultiSelect is true", async () => {
|
||||||
|
// Act
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
mockData: {
|
||||||
|
propsData: {
|
||||||
|
isMultiSelect: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
expect(input.attributes().type).toEqual("checkbox");
|
// Assert
|
||||||
});
|
const input = wrapper.find("input");
|
||||||
|
|
||||||
it("Should return input type radio if isMultiSelect is false or not specified", async () => {
|
expect(input.attributes().type).toEqual("checkbox");
|
||||||
// Act
|
});
|
||||||
const wrapper = shallowMount(listButton, {
|
|
||||||
propsData: {
|
it("Should return input type radio if isMultiSelect is false or not specified", async () => {
|
||||||
isMultiSelect: false,
|
// Act
|
||||||
},
|
const { wrapper } = setupMocks({
|
||||||
|
mockData: {
|
||||||
|
propsData: {
|
||||||
|
isMultiSelect: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const input = wrapper.find("input");
|
||||||
|
expect(input.attributes().type).toEqual("radio");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("(Radio) Should emit button value on selectedValue change", async () => {
|
||||||
|
// Act
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
mockData: {
|
||||||
|
propsData: {
|
||||||
|
isRadioHorizontal: true,
|
||||||
|
buttonLabel: "Windshield",
|
||||||
|
value: "List Card Checkbox",
|
||||||
|
groupID: "radio-demo-1",
|
||||||
|
groupName: "radio1",
|
||||||
|
buttonImage: "windshield-damage.svg",
|
||||||
|
isRequired: true,
|
||||||
|
isWide: false,
|
||||||
|
modelValue: "List Card Checkbox",
|
||||||
|
buttonID: "list-card-id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
wrapper.vm.selectedValue = "test";
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.emitted()["update:modelValue"][0][0]).toEqual("test");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("(Checkbox) Should emit button value on selectedValue change", async () => {
|
||||||
|
// Act
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
mockData: {
|
||||||
|
propsData: {
|
||||||
|
isRadioHorizontal: true,
|
||||||
|
buttonLabel: "Windshield",
|
||||||
|
value: "List Card Checkbox",
|
||||||
|
groupID: "radio-demo-1",
|
||||||
|
groupName: "radio 1",
|
||||||
|
buttonImage: "windshield-damage.svg",
|
||||||
|
isRequired: true,
|
||||||
|
isWide: false,
|
||||||
|
modelValue: "List Card Checkbox",
|
||||||
|
buttonID: "list-card-id",
|
||||||
|
isMultiSelect: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
wrapper.vm.selectedValue = ["test"];
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.emitted()["update:modelValue"][0][0]).toEqual(["test"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
describe("styling/UI", () => {
|
||||||
const input = wrapper.find("input");
|
test("has buttonLabel => displays buttonLabel", () => {
|
||||||
|
// Arrange/Act
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
mockData: {
|
||||||
|
propsData: {
|
||||||
|
buttonLabel: "Surprise!",
|
||||||
|
modelValue: "",
|
||||||
|
groupName: "groupName",
|
||||||
|
value: "myValue",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
expect(input.attributes().type).toEqual("radio");
|
// Assert
|
||||||
});
|
const content = wrapper.find(".list-button-content");
|
||||||
|
expect(content.isVisible()).toBe(true);
|
||||||
|
expect(content.text()).toContain("Surprise!");
|
||||||
|
});
|
||||||
|
|
||||||
it("Should return primary label text (buttonID)", async () => {
|
test("has buttonLabelSubCopy => displays buttonLabelSubCopy", () => {
|
||||||
// Act
|
// Arrange/Act
|
||||||
const wrapper = shallowMount(listButton, {
|
const { wrapper } = setupMocks({
|
||||||
propsData: {
|
mockData: {
|
||||||
buttonID: "List Card Checkbox",
|
propsData: {
|
||||||
},
|
buttonLabel: "Surprise!",
|
||||||
|
buttonLabelSubCopy: "Super duper surprise :)",
|
||||||
|
modelValue: "",
|
||||||
|
groupName: "groupName",
|
||||||
|
value: "myValue",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const content = wrapper.find(".list-button-content");
|
||||||
|
expect(content.isVisible()).toBe(true);
|
||||||
|
expect(content.text()).toContain("Super duper surprise :)");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("has screenReaderOnlyText => displays screenReaderOnlyText", () => {
|
||||||
|
// Arrange/Act
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
mockData: {
|
||||||
|
propsData: {
|
||||||
|
buttonLabel: "Surprise!",
|
||||||
|
buttonLabelSubCopy: "Super duper surprise :)",
|
||||||
|
screenReaderOnlyText: "Tests are fun!",
|
||||||
|
modelValue: "",
|
||||||
|
groupName: "groupName",
|
||||||
|
value: "myValue",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const content = wrapper.find(".list-button-content");
|
||||||
|
const screenReaderOnlyText = wrapper.find(".sr-only");
|
||||||
|
expect(content.isVisible()).toBe(true);
|
||||||
|
expect(screenReaderOnlyText.exists()).toBe(true);
|
||||||
|
expect(screenReaderOnlyText.text()).toContain("Tests are fun!");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return screen reader text", async () => {
|
||||||
|
// Act
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
mockData: {
|
||||||
|
propsData: {
|
||||||
|
screenReaderOnlyText: "Screen Reader Only Text",
|
||||||
|
modelValue: "",
|
||||||
|
groupName: "groupName",
|
||||||
|
value: "myValue",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const paragraph = wrapper.find("span.sr-only");
|
||||||
|
|
||||||
|
expect(paragraph.text()).toEqual("Screen Reader Only Text");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return text alignment class", async () => {
|
||||||
|
// Act
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
mockData: {
|
||||||
|
propsData: {
|
||||||
|
textPosition: "text-center",
|
||||||
|
modelValue: "",
|
||||||
|
groupName: "groupName",
|
||||||
|
value: "myValue",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const paragraph = wrapper.find("span.m-0");
|
||||||
|
|
||||||
|
expect(paragraph.attributes("class")).toContain("text-center");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return aria-required state", async () => {
|
||||||
|
// Act
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
mockData: {
|
||||||
|
propsData: {
|
||||||
|
isRequired: true,
|
||||||
|
groupName: "groupName",
|
||||||
|
modelValue: "",
|
||||||
|
value: "myValue",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const input = wrapper.find("input");
|
||||||
|
|
||||||
|
expect(input.attributes()["aria-required"]).toEqual("true");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
|
||||||
const label = wrapper.find("label");
|
|
||||||
|
|
||||||
expect(label.attributes().for).toEqual("List Card Checkbox");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return screen reader text", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButton, {
|
|
||||||
propsData: {
|
|
||||||
screenReaderOnlyText: "Screen Reader Only Text",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const paragraph = wrapper.find("span.sr-only");
|
|
||||||
|
|
||||||
expect(paragraph.text()).toEqual("Screen Reader Only Text");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return text alignment class", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButton, {
|
|
||||||
propsData: {
|
|
||||||
textPosition: "text-center",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const paragraph = wrapper.find("span.m-0");
|
|
||||||
|
|
||||||
expect(paragraph.attributes("class")).toContain("text-center");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return aria-required state", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButton, {
|
|
||||||
propsData: {
|
|
||||||
isRequired: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const input = wrapper.find("input");
|
|
||||||
|
|
||||||
expect(input.attributes()["aria-required"]).toEqual("true");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return loader enabled true", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButton, {
|
|
||||||
global: {
|
|
||||||
mocks: {
|
|
||||||
'$route': { query: { issPage: 'page-name' } },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
propsData: {
|
|
||||||
selectingInitiatesLoad: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
wrapper.vm.handleCheckChange = jest.fn();
|
|
||||||
wrapper.vm.triggerButton();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
const loader = wrapper.find("loader-stub");
|
|
||||||
|
|
||||||
expect(loader.exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return loader color", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButton, {
|
|
||||||
global: {
|
|
||||||
mocks: {
|
|
||||||
'$route': { query: { issPage: 'page-name' } },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
propsData: {
|
|
||||||
loaderColor: "blue",
|
|
||||||
selectingInitiatesLoad: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
wrapper.vm.handleCheckChange = jest.fn();
|
|
||||||
wrapper.vm.triggerButton();
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
const loader = wrapper.find("loader-stub");
|
|
||||||
|
|
||||||
expect(loader.attributes("class")).toContain("blue");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return loader position", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButton, {
|
|
||||||
global: {
|
|
||||||
mocks: {
|
|
||||||
'$route': { query: { issPage: 'page-name' } },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
propsData: {
|
|
||||||
loaderPosition: "right",
|
|
||||||
selectingInitiatesLoad: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
wrapper.vm.handleCheckChange = jest.fn();
|
|
||||||
wrapper.vm.triggerButton();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
const loader = wrapper.find("loader-stub");
|
|
||||||
|
|
||||||
expect(loader.attributes("class")).toContain("right");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should emit button value on click", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButton, {
|
|
||||||
propsData: {
|
|
||||||
isRadioHorizontal: true,
|
|
||||||
buttonLabel: "Windshield",
|
|
||||||
value: "List Card Checkbox",
|
|
||||||
groupID: "radio-demo-1",
|
|
||||||
groupName: "radio 1",
|
|
||||||
buttonImage: "windshield-damage.svg",
|
|
||||||
isRequired: true,
|
|
||||||
isWide: false,
|
|
||||||
modelValue: ["List Card Checkbox"],
|
|
||||||
buttonID: 'list-card-id'
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
wrapper.vm.handleCheckChange();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: false, buttonId: 'list-card-id'}]);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButton, {
|
|
||||||
propsData: {
|
|
||||||
isRadioHorizontal: true,
|
|
||||||
buttonLabel: "Windshield",
|
|
||||||
buttonID: "List Card Checkbox",
|
|
||||||
groupID: "radio-demo-1",
|
|
||||||
groupName: "radio 1",
|
|
||||||
buttonImage: "windshield-damage.svg",
|
|
||||||
isRequired: true,
|
|
||||||
isWide: false,
|
|
||||||
modelValue: ["List Card Checkbox"],
|
|
||||||
selectedValues: "Car-Front"
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// Assert
|
|
||||||
expect(wrapper.componentVM.checkValue).toEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should run handleCheckChange if selectingInitiatesLoad is false and handleInputChange is triggered", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButton, {
|
|
||||||
propsData: {
|
|
||||||
selectingInitiatesLoad: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
wrapper.vm.handleInputChange();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(wrapper.vm.handleCheckChange).toBeCalled;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should do nothing if isMultiSelect is true and handleKeyupArrow is triggered", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButton, {
|
|
||||||
propsData: {
|
|
||||||
isMultiSelect: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
wrapper.vm.handleKeyupArrow();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(wrapper.vm.handleKeyupArrow).toHaveReturned;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should run handleCheckChange if selectingInitiatesLoad is false and handleKeyupArrow is triggered", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listButton, {
|
|
||||||
propsData: {
|
|
||||||
selectingInitiatesLoad: false,
|
|
||||||
isMultiSelect: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
wrapper.vm.handleKeyupArrow();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(wrapper.vm.handleCheckChange).toBeCalled;
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function setupMocks({ mockData }) {
|
||||||
|
const wrapper = mount(listButton, {
|
||||||
|
...mockData,
|
||||||
|
mixins: [inputButtonWrapperMixin],
|
||||||
|
});
|
||||||
|
|
||||||
|
return { wrapper };
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,241 +1,114 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<baseInputButton
|
||||||
class="list-group list-button rounded-3 d-flex flex-column w-100 mb-2"
|
v-bind="$props"
|
||||||
:class="[(errors.length > 0 || hasError) ? 'has-error' : '']"
|
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2"
|
||||||
@keyup.space="triggerButton"
|
v-model="selectedValue">
|
||||||
@keyup.enter="triggerButton"
|
<div
|
||||||
@keyup.up="handleKeyupArrow"
|
:aria-label="buttonLabel"
|
||||||
@keyup.down="handleKeyupArrow"
|
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
|
||||||
@keyup.left="handleKeyupArrow"
|
<span class="m-0" :class="textPosition">
|
||||||
@keyup.right="handleKeyupArrow">
|
{{ buttonLabel }}
|
||||||
<input
|
</span>
|
||||||
:type="isMultiSelect ? 'checkbox' : 'radio'"
|
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition">
|
||||||
:id="buttonID"
|
{{ buttonLabelSubCopy }}
|
||||||
:name="groupName"
|
</span>
|
||||||
:value="value"
|
<span v-if="screenReaderOnlyText" class="sr-only">
|
||||||
:aria-required="isRequired"
|
{{ screenReaderOnlyText }}
|
||||||
v-model="checkValue"
|
</span>
|
||||||
:checked="checkValue"
|
<loader
|
||||||
@change="handleInputChange"
|
v-if="isLoaderDisplayed && selectingInitiatesLoad"
|
||||||
>
|
:class="[this.loaderColor, this.loaderPosition]" />
|
||||||
<label
|
</div>
|
||||||
tabindex="-1"
|
</baseInputButton>
|
||||||
:for="buttonID"
|
</template>
|
||||||
:aria-label="buttonLabel"
|
|
||||||
class="d-flex flex-column justify-content-center py-3 px-4"
|
|
||||||
@mouseup="triggerButton"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="m-0"
|
|
||||||
:class="textPosition"
|
|
||||||
>
|
|
||||||
{{ buttonLabel }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="buttonLabelSubCopy"
|
|
||||||
class="m-0 small"
|
|
||||||
:class="textPosition"
|
|
||||||
>
|
|
||||||
{{ buttonLabelSubCopy }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="screenReaderOnlyText"
|
|
||||||
class="sr-only"
|
|
||||||
>
|
|
||||||
{{ screenReaderOnlyText }}
|
|
||||||
</span>
|
|
||||||
<loader
|
|
||||||
v-if="isLoaderDisplayed && selectingInitiatesLoad"
|
|
||||||
:class="[this.loaderColor, this.loaderPosition]"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { useField } from "vee-validate";
|
import loader from "@/ux-components/loader/loader";
|
||||||
import { toRef } from "vue";
|
import baseInputButton from "@/common-components/base-input-button/base-input-button";
|
||||||
import loader from "@/ux-components/loader/loader";
|
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "listButton",
|
name: "listButton",
|
||||||
props: {
|
mixins: [inputButtonWrapperMixin],
|
||||||
isMultiSelect: Boolean,
|
props: {
|
||||||
groupName: String,
|
|
||||||
buttonLabel: [Number, String],
|
|
||||||
buttonID: [Number, String],
|
|
||||||
isRequired: Boolean,
|
|
||||||
textPosition: String,
|
|
||||||
buttonLabelSubCopy: String,
|
|
||||||
screenReaderOnlyText: String,
|
|
||||||
selectingInitiatesLoad: Boolean,
|
|
||||||
loaderColor: String,
|
loaderColor: String,
|
||||||
loaderPosition: String,
|
loaderPosition: {
|
||||||
value: {
|
type: String,
|
||||||
// Field initial value
|
default: "right",
|
||||||
type: [String, Number],
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
|
|
||||||
validationRules: String,
|
|
||||||
selectedValues: [Array, String],
|
|
||||||
hasError: Boolean,
|
|
||||||
valueToLogType: String,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isLoaderDisplayed: false,
|
|
||||||
checkValue: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (Array.isArray(this.validateValue)) {
|
|
||||||
this.checkValue = this.isValueSelectedByArray(this.selectedValues);
|
|
||||||
const isSelectedByValidator = this.isValueSelectedByArray(this.validateValue);
|
|
||||||
|
|
||||||
if (this.checkValue != isSelectedByValidator) {
|
|
||||||
this.handleChange(this.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.checkValue = this.selectedValues == this.value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
isValueSelectedByArray(arr) {
|
|
||||||
return this.isMultiSelect
|
|
||||||
? arr.includes(this.value)
|
|
||||||
: arr[0];
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoaderDisplayed: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
displayLoader() {
|
displayLoader() {
|
||||||
this.isLoaderDisplayed = true;
|
this.isLoaderDisplayed = true;
|
||||||
},
|
},
|
||||||
handleInputChange() {
|
preHandleAnswerChange() {
|
||||||
if(!this.selectingInitiatesLoad) {
|
if (this.selectingInitiatesLoad) {
|
||||||
this.handleCheckChange();
|
this.displayLoader();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleKeyupArrow() {
|
},
|
||||||
if (this.isMultiSelect) {
|
components: {
|
||||||
return; // Prevent arrow keys from doing anything if element is a checkbox
|
|
||||||
}
|
|
||||||
},
|
|
||||||
triggerButton() {
|
|
||||||
if(this.selectingInitiatesLoad) {
|
|
||||||
this.displayLoader();
|
|
||||||
this.handleCheckChange();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleCheckChange() {
|
|
||||||
const emitEvent = {
|
|
||||||
checkValue: this.checkValue, // only read on checkboxes, on handleCheckedChanged on button-question
|
|
||||||
value: this.value.toString(),
|
|
||||||
buttonId: this.buttonID && this.buttonID.toString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.handleChange(this.value);
|
|
||||||
this.$emit("isCheckedChanged", emitEvent);
|
|
||||||
this.$emit("update:modelValue", emitEvent);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
loader,
|
loader,
|
||||||
},
|
baseInputButton,
|
||||||
setup(props) {
|
},
|
||||||
const inputType = props.isMultiSelect ? "checkbox" : "radio";
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
const fieldOptions = {
|
<style lang="scss" scoped>
|
||||||
type: inputType,
|
.loader {
|
||||||
checkedValue: props.value,
|
position: absolute;
|
||||||
potentialInitialValue: props.selectedValues,
|
}
|
||||||
};
|
.list-button {
|
||||||
|
outline: none;
|
||||||
|
input[type="radio"],
|
||||||
|
input[type="checkbox"] {
|
||||||
|
position: static; //override bootstrap
|
||||||
|
|
||||||
// Set initialValue for validation setup if pre-selected
|
&:focus-visible + .list-button-content {
|
||||||
// NOTE: props.selectedValues could be an array of strings, or an array of integers...
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
if (props.selectedValues && (props.selectedValues.includes(props.value) || props.selectedValues.includes(parseInt(props.value)))) {
|
|
||||||
fieldOptions['initialValue'] = fieldOptions.potentialInitialValue;
|
|
||||||
}
|
}
|
||||||
|
&:focus + .list-button-content {
|
||||||
const {
|
|
||||||
handleChange,
|
|
||||||
errors,
|
|
||||||
value
|
|
||||||
} = useField(toRef(props, "groupName"), toRef(props, "validationRules"), fieldOptions);
|
|
||||||
|
|
||||||
const validateValue = value;
|
|
||||||
|
|
||||||
return {
|
|
||||||
handleChange,
|
|
||||||
errors,
|
|
||||||
validateValue,
|
|
||||||
fieldOptions, // only need to expose this for unit test purposes
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.list-group {
|
|
||||||
&.list-button {
|
|
||||||
outline: none;
|
|
||||||
input[type="radio"],
|
|
||||||
input[type="checkbox"] {
|
|
||||||
position: static; //override bootstrap
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
&:focus-visible + label {
|
|
||||||
box-shadow: 0 0 0 2.5px $blue;
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
}
|
}
|
||||||
&:focus + label {
|
&:checked + .list-button-content {
|
||||||
box-shadow: 0 0 0 2.5px $blue;
|
|
||||||
}
|
|
||||||
&:checked + label {
|
|
||||||
color: $black;
|
color: $black;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
background: $blue-100;
|
background: $blue-100;
|
||||||
box-shadow: 0 0 0 1px $blue;
|
box-shadow: 0 0 0 1px $blue;
|
||||||
}
|
}
|
||||||
&:checked:focus + label {
|
&:checked:focus + .list-button-content {
|
||||||
box-shadow: 0 0 0 2.5px $blue;
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
}
|
}
|
||||||
&:checked + label p,
|
&:checked + .list-button-content p,
|
||||||
&:checked + label span {
|
&:checked + .list-button-content span {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
&:checked + label span:nth-child(2) {
|
&:checked + .list-button-content span:nth-child(2) {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: $gray-600;
|
color: $gray-600;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
label {
|
|
||||||
color: $gray-600;
|
|
||||||
position: relative;
|
|
||||||
background: $white;
|
|
||||||
transition: all 150ms linear;
|
|
||||||
border-radius: $border-radius-lg;
|
|
||||||
border: 1px solid $gray-500;
|
|
||||||
width: 100%;
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
span {
|
|
||||||
&.small {
|
|
||||||
font-size: .75rem;
|
|
||||||
color: $gray-550;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
@include media-breakpoint-up(sm) {
|
|
||||||
box-shadow: 0 0 0 4px $blue-300;
|
|
||||||
}
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
+ p {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
}
|
||||||
|
.list-button-content {
|
||||||
|
color: $gray-600;
|
||||||
|
position: relative;
|
||||||
|
background: $white;
|
||||||
|
transition: all 150ms linear;
|
||||||
|
border-radius: $border-radius-lg;
|
||||||
|
border: 1px solid $gray-500;
|
||||||
|
width: 100%;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
span {
|
||||||
|
&.small {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: $gray-550;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,329 +1,179 @@
|
||||||
import { shallowMount } from "@vue/test-utils";
|
import { mount } from "@vue/test-utils";
|
||||||
import listCard from "./list-card";
|
import listCard from "./list-card";
|
||||||
import { nextTick } from "vue";
|
import { nextTick } from "vue";
|
||||||
|
|
||||||
describe("list-card.vue", () => {
|
describe("list-card.vue", () => {
|
||||||
it("Should return input type checkbox if isMultiSelect is true", async () => {
|
it("Should return input type checkbox if isMultiSelect is true", () => {
|
||||||
// Act
|
// Act
|
||||||
const wrapper = shallowMount(listCard, {
|
const wrapper = mount(listCard, {
|
||||||
propsData: {
|
propsData: {
|
||||||
isMultiSelect: true,
|
isMultiSelect: true,
|
||||||
buttonLabel: "Windshield",
|
buttonLabel: "Windshield",
|
||||||
buttonID: "List Card Checkbox",
|
buttonID: "List Card Checkbox",
|
||||||
groupID: "checkbox-demo-1",
|
groupID: "checkbox-demo-1",
|
||||||
groupName: "Checkbox 1",
|
groupName: "Checkbox 1",
|
||||||
buttonImage: "windshield-damage.svg",
|
buttonImage: "windshield-damage.svg",
|
||||||
},
|
value: "test value",
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
const input = wrapper.find("input");
|
const input = wrapper.find("input");
|
||||||
expect(input.attributes().type).toEqual("checkbox");
|
expect(input.attributes().type).toEqual("checkbox");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return primary label text", async () => {
|
it("Should return primary label text", () => {
|
||||||
// Act
|
// Act
|
||||||
const wrapper = shallowMount(listCard, {
|
const wrapper = mount(listCard, {
|
||||||
propsData: {
|
propsData: {
|
||||||
isRadioHorizontal: true,
|
isRadioHorizontal: true,
|
||||||
buttonLabel: "Windshield",
|
buttonLabel: "Windshield",
|
||||||
buttonID: "List Card Checkbox",
|
buttonID: "List Card Checkbox",
|
||||||
groupID: "radio-demo-1",
|
groupID: "radio-demo-1",
|
||||||
groupName: "radio 1",
|
groupName: "radio 1",
|
||||||
buttonImage: "windshield-damage.svg",
|
buttonImage: "windshield-damage.svg",
|
||||||
},
|
value: "test value",
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
const paragraph = wrapper.find("p");
|
const paragraph = wrapper.find("p");
|
||||||
expect(paragraph.text()).toEqual("Windshield");
|
expect(paragraph.text()).toEqual("Windshield");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return secondary (sub) label text", async () => {
|
it("Should return secondary (sub) label text", () => {
|
||||||
// Act
|
// Act
|
||||||
const wrapper = shallowMount(listCard, {
|
const wrapper = mount(listCard, {
|
||||||
propsData: {
|
propsData: {
|
||||||
isRadioHorizontal: true,
|
isRadioHorizontal: true,
|
||||||
buttonLabel: "Windshield",
|
buttonLabel: "Windshield",
|
||||||
buttonID: "List Card Checkbox",
|
buttonID: "List Card Checkbox",
|
||||||
groupID: "radio-demo-1",
|
groupID: "radio-demo-1",
|
||||||
groupName: "radio 1",
|
groupName: "radio 1",
|
||||||
buttonImage: "windshield-damage.svg",
|
buttonImage: "windshield-damage.svg",
|
||||||
buttonLabelSubCopy: "Test",
|
buttonLabelSubCopy: "Test",
|
||||||
},
|
value: "test value",
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
const paragraph = wrapper.find("p:nth-of-type(2)");
|
const paragraph = wrapper.find("p:nth-of-type(2)");
|
||||||
expect(paragraph.text()).toEqual("Test");
|
expect(paragraph.text()).toEqual("Test");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return value used for various text settings including the label 'for' and input id", async () => {
|
it("Should return input group name used for radio or checkbox", () => {
|
||||||
// Act
|
// Act
|
||||||
const wrapper = shallowMount(listCard, {
|
const wrapper = mount(listCard, {
|
||||||
propsData: {
|
propsData: {
|
||||||
isRadioHorizontal: true,
|
isRadioHorizontal: true,
|
||||||
buttonLabel: "Windshield",
|
buttonLabel: "Windshield",
|
||||||
buttonID: "List Card Checkbox",
|
buttonID: "List Card Checkbox",
|
||||||
groupID: "radio-demo-1",
|
groupID: "radio-demo-1",
|
||||||
groupName: "radio 1",
|
groupName: "radio 1",
|
||||||
buttonImage: "windshield-damage.svg",
|
buttonImage: "windshield-damage.svg",
|
||||||
buttonLabelSubCopy: "Test",
|
buttonLabelSubCopy: "Test",
|
||||||
},
|
value: "test value",
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
const label = wrapper.find("label");
|
const input = wrapper.find("input");
|
||||||
expect(label.attributes().for).toEqual("List Card Checkbox");
|
expect(input.attributes().name).toEqual("radio 1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return input group name used for radio or checkbox", async () => {
|
it("Should return aria-required state", () => {
|
||||||
// Act
|
// Act
|
||||||
const wrapper = shallowMount(listCard, {
|
const wrapper = mount(listCard, {
|
||||||
propsData: {
|
propsData: {
|
||||||
isRadioHorizontal: true,
|
isRadioHorizontal: true,
|
||||||
buttonLabel: "Windshield",
|
buttonLabel: "Windshield",
|
||||||
buttonID: "List Card Checkbox",
|
buttonID: "List Card Checkbox",
|
||||||
groupID: "radio-demo-1",
|
groupID: "radio-demo-1",
|
||||||
groupName: "radio 1",
|
groupName: "radio 1",
|
||||||
buttonImage: "windshield-damage.svg",
|
buttonImage: "windshield-damage.svg",
|
||||||
buttonLabelSubCopy: "Test",
|
isRequired: true,
|
||||||
},
|
value: "test value",
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
const input = wrapper.find("input");
|
const input = wrapper.find("input");
|
||||||
expect(input.attributes().name).toEqual("radio 1");
|
expect(input.attributes()["aria-required"]).toEqual("true");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return aria-required state", async () => {
|
it("Should return flex row classes if isWide is true", () => {
|
||||||
// Act
|
// Act
|
||||||
const wrapper = shallowMount(listCard, {
|
const wrapper = mount(listCard, {
|
||||||
propsData: {
|
propsData: {
|
||||||
isRadioHorizontal: true,
|
isRadioHorizontal: true,
|
||||||
buttonLabel: "Windshield",
|
buttonLabel: "Windshield",
|
||||||
buttonID: "List Card Checkbox",
|
buttonID: "List Card Checkbox",
|
||||||
groupID: "radio-demo-1",
|
groupID: "radio-demo-1",
|
||||||
groupName: "radio 1",
|
groupName: "radio 1",
|
||||||
buttonImage: "windshield-damage.svg",
|
buttonImage: "windshield-damage.svg",
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
},
|
isWide: true,
|
||||||
});
|
buttonLabelSubCopy: "",
|
||||||
|
value: "test value",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
const input = wrapper.find("input");
|
const label = wrapper.find(".list-card-content");
|
||||||
expect(input.attributes()["aria-required"]).toEqual("true");
|
expect(label.exists()).toBe(true);
|
||||||
|
const labelClasses = wrapper.vm.labelClasses;
|
||||||
|
expect(labelClasses).toContain("flex-row");
|
||||||
|
expect(label.classes()).toContain("flex-row");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return flex row classes if isWide is true", async () => {
|
it("Should return flex row classes if isWide is true and checkboxTop if buttonLabelSubCopy is provided", () => {
|
||||||
// Act
|
// Act
|
||||||
const wrapper = shallowMount(listCard, {
|
const wrapper = mount(listCard, {
|
||||||
propsData: {
|
propsData: {
|
||||||
isRadioHorizontal: true,
|
isRadioHorizontal: true,
|
||||||
buttonLabel: "Windshield",
|
buttonLabel: "Windshield",
|
||||||
buttonID: "List Card Checkbox",
|
buttonID: "List Card Checkbox",
|
||||||
groupID: "radio-demo-1",
|
groupID: "radio-demo-1",
|
||||||
groupName: "radio 1",
|
groupName: "radio 1",
|
||||||
buttonImage: "windshield-damage.svg",
|
buttonImage: "windshield-damage.svg",
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
isWide: true,
|
isWide: true,
|
||||||
buttonLabelSubCopy: "",
|
buttonLabelSubCopy: "Button Subcopy",
|
||||||
},
|
value: "test value",
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
const label = wrapper.find("label");
|
const label = wrapper.find(".list-card-content");
|
||||||
expect(label.classes()).toEqual(["d-flex", "w-100", "align-items-center", "px-2", "h-100", "flex-row", "py-2", "ps-4", "pe-4"]);
|
expect(label.exists()).toBe(true);
|
||||||
|
const labelClasses = wrapper.vm.labelClasses;
|
||||||
|
expect(labelClasses).toContain("flex-row");
|
||||||
|
expect(label.classes()).toContain("flex-row");
|
||||||
|
expect(labelClasses).toContain("checkboxTop");
|
||||||
|
expect(label.classes()).toContain("checkboxTop");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return flex row classes if isWide is true and checkboxTop if buttonLabelSubCopy is true", async () => {
|
it("Should return flex column classes if isWide is false", () => {
|
||||||
// Act
|
// Act
|
||||||
const wrapper = shallowMount(listCard, {
|
const wrapper = mount(listCard, {
|
||||||
propsData: {
|
propsData: {
|
||||||
isRadioHorizontal: true,
|
isRadioHorizontal: true,
|
||||||
buttonLabel: "Windshield",
|
buttonLabel: "Windshield",
|
||||||
buttonID: "List Card Checkbox",
|
buttonID: "List Card Checkbox",
|
||||||
groupID: "radio-demo-1",
|
groupID: "radio-demo-1",
|
||||||
groupName: "radio 1",
|
groupName: "radio 1",
|
||||||
buttonImage: "windshield-damage.svg",
|
buttonImage: "windshield-damage.svg",
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
isWide: true,
|
isWide: false,
|
||||||
buttonLabelSubCopy: "Button Subcopy",
|
value: "test value",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
const label = wrapper.find("label");
|
const label = wrapper.find(".list-card-content");
|
||||||
expect(label.classes()).toEqual(["d-flex", "w-100", "align-items-center", "px-2", "h-100", "flex-row", "py-2", "ps-4", "pe-4", "checkboxTop"]);
|
expect(label.exists()).toBe(true);
|
||||||
|
const labelClasses = wrapper.vm.labelClasses;
|
||||||
|
expect(labelClasses).toContain("flex-column");
|
||||||
|
expect(label.classes()).toContain("flex-column");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return flex column classes if isWide is false", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listCard, {
|
|
||||||
propsData: {
|
|
||||||
isRadioHorizontal: true,
|
|
||||||
buttonLabel: "Windshield",
|
|
||||||
buttonID: "List Card Checkbox",
|
|
||||||
groupID: "radio-demo-1",
|
|
||||||
groupName: "radio 1",
|
|
||||||
buttonImage: "windshield-damage.svg",
|
|
||||||
isRequired: true,
|
|
||||||
isWide: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const label = wrapper.find("label");
|
|
||||||
expect(label.classes()).toEqual(["d-flex", "w-100", "align-items-center", "px-2", "h-100", "flex-column", "pt-4", "pb-3"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should emit button value on click", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listCard, {
|
|
||||||
propsData: {
|
|
||||||
isRadioHorizontal: true,
|
|
||||||
buttonLabel: "Windshield",
|
|
||||||
value: "List Card Checkbox",
|
|
||||||
groupID: "radio-demo-1",
|
|
||||||
groupName: "radio 1",
|
|
||||||
buttonImage: "windshield-damage.svg",
|
|
||||||
isRequired: true,
|
|
||||||
isWide: false,
|
|
||||||
buttonID: 'list-card-id',
|
|
||||||
selectedValues: "List Card Checkbox"
|
|
||||||
},
|
|
||||||
});
|
|
||||||
wrapper.vm.handleCheckChange();
|
|
||||||
// Assert
|
|
||||||
expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: true, buttonId: 'list-card-id'}]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listCard, {
|
|
||||||
propsData: {
|
|
||||||
isRadioHorizontal: true,
|
|
||||||
buttonLabel: "Windshield",
|
|
||||||
value: "List Card Checkbox",
|
|
||||||
groupID: "radio-demo-1",
|
|
||||||
groupName: "radio 1",
|
|
||||||
buttonImage: "windshield-damage.svg",
|
|
||||||
isRequired: true,
|
|
||||||
isWide: false,
|
|
||||||
selectedValues: "Car-Front"
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// Assert
|
|
||||||
expect(wrapper.componentVM.checkValue).toEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should set an initial value for validation if selectedValues include the value", async () => {
|
|
||||||
// Arrange
|
|
||||||
const wrapper = shallowMount(listCard, {
|
|
||||||
propsData: {
|
|
||||||
value: "Windshield",
|
|
||||||
groupName: "radio 1",
|
|
||||||
selectedValues: ["Windshield"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(wrapper.vm.fieldOptions.initialValue).toEqual([ 'Windshield' ]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should run handleCheckChange if selectingInitiatesLoad is false and handleInputChange is triggered", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listCard, {
|
|
||||||
propsData: {
|
|
||||||
selectingInitiatesLoad: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
wrapper.vm.handleInputChange();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(wrapper.vm.handleCheckChange).toBeCalled;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should do nothing if isMultiSelect is true and handleKeyupArrow is triggered", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listCard, {
|
|
||||||
propsData: {
|
|
||||||
isMultiSelect: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
wrapper.vm.handleKeyupArrow();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(wrapper.vm.handleKeyupArrow).toHaveReturned;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should run handleCheckChange if selectingInitiatesLoad is false and handleKeyupArrow is triggered", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listCard, {
|
|
||||||
propsData: {
|
|
||||||
selectingInitiatesLoad: false,
|
|
||||||
isMultiSelect: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
wrapper.vm.handleKeyupArrow();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(wrapper.vm.handleCheckChange).toBeCalled;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should run handleChange if triggerButton is triggered", async () => {
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listCard, {
|
|
||||||
global: {
|
|
||||||
mocks: {
|
|
||||||
'$route': { query: { issPage: 'page-name' } },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
propsData: {
|
|
||||||
selectingInitiatesLoad: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
wrapper.vm.triggerButton();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(wrapper.vm.handleChange).toBeCalled;
|
|
||||||
expect(wrapper.vm.handleCheckChange).not.toBeCalled;
|
|
||||||
expect(wrapper.vm.displayLoader).not.toBeCalled;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should run handleCheckChange and displayLoader if triggerButton is triggered and seletingInitiatesLoad is true", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(listCard, {
|
|
||||||
global: {
|
|
||||||
mocks: {
|
|
||||||
'$route': { query: { issPage: 'page-name' } },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
propsData: {
|
|
||||||
selectingInitiatesLoad: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
wrapper.vm.triggerButton();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(wrapper.vm.handleCheckChange).toBeCalled;
|
|
||||||
expect(wrapper.vm.displayLoader).toBeCalled;
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,399 +1,243 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="{'h-100': !isWide}">
|
<baseInputButton
|
||||||
<div
|
v-bind="$props"
|
||||||
class="list-card w-100 rounded-3 d-flex align-items-center"
|
:buttonWrapperClasses="[
|
||||||
:class="[
|
'list-card w-100 rounded-3 d-flex align-items-center h-100 base-input-button',
|
||||||
'h-100',
|
{ horizontal: isWide },
|
||||||
isWide ? 'horizontal' : '',
|
|
||||||
(errors.length > 0 || hasError) ? 'has-error' : '',
|
|
||||||
]"
|
]"
|
||||||
@keyup.space="triggerButton"
|
v-model="selectedValue">
|
||||||
@keyup.up="handleKeyupArrow"
|
<div
|
||||||
@keyup.down="handleKeyupArrow"
|
class="d-flex w-100 align-items-center px-2 h-100 list-card-content button-content rounded-3"
|
||||||
@keyup.left="handleKeyupArrow"
|
:class="labelClasses">
|
||||||
@keyup.right="handleKeyupArrow"
|
<img
|
||||||
>
|
:id="buttonImageId"
|
||||||
<input
|
:class="!isWide ? 'order-1' : 'ms-auto order-3'"
|
||||||
:type="isMultiSelect ? 'checkbox' : 'radio'"
|
:src="buttonImage"
|
||||||
:id="buttonID"
|
:alt="altText" />
|
||||||
:name="groupName"
|
<p v-if="!isWide" class="small order-3" :class="isMultiSelect ? 'm-0' : 'mt-2 mb-0'">
|
||||||
:value="value"
|
{{ buttonLabel }}
|
||||||
:aria-required="isRequired"
|
|
||||||
v-model="checkValue"
|
|
||||||
:checked="checkValue"
|
|
||||||
@change="handleInputChange"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
tabindex="-1"
|
|
||||||
:for="buttonID"
|
|
||||||
:aria-label="buttonLabel"
|
|
||||||
class="d-flex w-100 align-items-center px-2 h-100"
|
|
||||||
:class="getLabelClasses"
|
|
||||||
@mouseup="triggerButton"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
:id="buttonImageId"
|
|
||||||
:class="!isWide ? 'order-1' : 'ms-auto order-3'"
|
|
||||||
:src="buttonImage"
|
|
||||||
:alt="altText"
|
|
||||||
/>
|
|
||||||
<p
|
|
||||||
v-if="!isWide"
|
|
||||||
class="small order-3"
|
|
||||||
:class="isMultiSelect ? 'm-0' : 'mt-2 mb-0'"
|
|
||||||
>
|
|
||||||
{{ buttonLabel }}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
v-if="buttonLabelSubCopy && !isWide"
|
|
||||||
class="fs-7 m-0 order-4 sub-copy"
|
|
||||||
>
|
|
||||||
{{ buttonLabelSubCopy }}
|
|
||||||
</p>
|
|
||||||
<div v-if="isWide" class="order-2">
|
|
||||||
<p class="m-0 small">{{ buttonLabel }}</p>
|
|
||||||
<p v-if="buttonLabelSubCopy" class="m-0 fs-7 sub-copy">
|
|
||||||
{{ buttonLabelSubCopy }}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<p v-if="buttonLabelSubCopy && !isWide" class="fs-7 m-0 order-4 sub-copy">
|
||||||
</label>
|
{{ buttonLabelSubCopy }}
|
||||||
</div>
|
</p>
|
||||||
</div>
|
<div v-if="isWide" class="order-2">
|
||||||
</template>
|
<p class="m-0 small">{{ buttonLabel }}</p>
|
||||||
|
<p v-if="buttonLabelSubCopy" class="m-0 fs-7 sub-copy">
|
||||||
|
{{ buttonLabelSubCopy }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</baseInputButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { useField } from "vee-validate";
|
import baseInputButton from "@/common-components/base-input-button/base-input-button";
|
||||||
import { toRef } from "vue";
|
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "listCard",
|
name: "listCard",
|
||||||
props: {
|
mixins: [inputButtonWrapperMixin],
|
||||||
isMultiSelect: Boolean, //Defines use as checkbox
|
components: {
|
||||||
isWide: Boolean,
|
baseInputButton,
|
||||||
buttonImage: String, //Required: File name of image
|
|
||||||
buttonImageId: String,
|
|
||||||
buttonLabel: String, //Required: Label text
|
|
||||||
isRequired: Boolean, //Required: is aria-required required or not?
|
|
||||||
altText: String, //Leave empty. Screen readers read the buttonLabel text. If alt has content, it will repeat unnecessarily.
|
|
||||||
buttonID: String, //Required: Unique
|
|
||||||
groupName: String, //Rquired: Unique
|
|
||||||
buttonLabelSubCopy: String, //Optional: sub text
|
|
||||||
value: {
|
|
||||||
// Field initial value
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
colLength: String,
|
|
||||||
validationRules: String,
|
|
||||||
selectedValues: [Array, String],
|
|
||||||
hasError: Boolean,
|
|
||||||
valueToLogType: String,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
checkValue: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (Array.isArray(this.validateValue)) {
|
|
||||||
this.checkValue = this.isValueSelectedByArray(this.selectedValues);
|
|
||||||
const isSelectedByValidator = this.isValueSelectedByArray(this.validateValue);
|
|
||||||
|
|
||||||
if (this.checkValue != isSelectedByValidator) {
|
|
||||||
this.handleChange(this.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.checkValue = this.selectedValues == this.value;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
getLabelClasses() {
|
labelClasses() {
|
||||||
if (this.isWide) {
|
if (this.isWide) {
|
||||||
let classes = "flex-row py-2 ps-4 pe-4";
|
let classes = "flex-row py-2 ps-4 pe-4";
|
||||||
if (this.buttonLabelSubCopy) {
|
if (this.buttonLabelSubCopy) {
|
||||||
classes += " checkboxTop";
|
classes += " checkboxTop";
|
||||||
}
|
}
|
||||||
return classes;
|
return classes;
|
||||||
} else {
|
} else {
|
||||||
return "flex-column pt-4 pb-3";
|
return "flex-column pt-4 pb-3";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
};
|
||||||
isValueSelectedByArray(arr) {
|
</script>
|
||||||
return this.isMultiSelect
|
|
||||||
? arr.includes(this.value)
|
|
||||||
: arr[0];
|
|
||||||
},
|
|
||||||
handleInputChange() {
|
|
||||||
if(!this.selectingInitiatesLoad) {
|
|
||||||
this.handleCheckChange();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleKeyupArrow() {
|
|
||||||
if (this.isMultiSelect) {
|
|
||||||
return; // Prevent arrow keys from doing anything if element is a checkbox
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!this.selectingInitiatesLoad) {
|
<style lang="scss">
|
||||||
this.handleCheckChange();
|
@mixin list-card-focus($box-shadow-color) {
|
||||||
}
|
&:focus-visible + .list-card-content {
|
||||||
},
|
box-shadow: 0 0 0 2.5px $box-shadow-color;
|
||||||
triggerButton() {
|
border-radius: 0.5rem;
|
||||||
if(this.selectingInitiatesLoad) {
|
}
|
||||||
this.displayLoader();
|
&:focus + .list-card-content {
|
||||||
this.handleCheckChange();
|
box-shadow: 0 0 0 2.5px $box-shadow-color;
|
||||||
}
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
},
|
&:checked:focus + .list-card-content {
|
||||||
handleCheckChange() {
|
box-shadow: 0 0 0 2.5px $box-shadow-color;
|
||||||
const emitEvent = {
|
}
|
||||||
checkValue: this.checkValue, // only read on checkboxes, on handleCheckedChanged on button-question
|
}
|
||||||
value: this.value.toString(),
|
.list-card {
|
||||||
buttonId: this.buttonID && this.buttonID.toString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.handleChange(this.value);
|
|
||||||
this.$emit("isCheckedChanged", emitEvent);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
// Changing this will impact pre-selection data loads on vehicle-parts.
|
|
||||||
// If changed, please regression test that vehicle-parts data still loads correctly with previous selections.
|
|
||||||
selectedValues(newVal) {
|
|
||||||
if (typeof newVal === "string") {
|
|
||||||
this.checkValue = newVal == this.value;
|
|
||||||
}
|
|
||||||
else if (newVal !== undefined) {
|
|
||||||
this.checkValue = newVal.value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const inputType = props.isMultiSelect ? "checkbox" : "radio";
|
|
||||||
|
|
||||||
const fieldOptions = {
|
|
||||||
type: inputType,
|
|
||||||
checkedValue: props.value, // EX: "Single" or "Passenger"
|
|
||||||
potentialInitialValue: props.selectedValues,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set initialValue for validation setup if pre-selected
|
|
||||||
// NOTE: props.selectedValues could be an array of strings, or an array of integers...
|
|
||||||
if (props.selectedValues && (props.selectedValues.includes(props.value) || props.selectedValues.includes(parseInt(props.value)))) {
|
|
||||||
fieldOptions['initialValue'] = fieldOptions.potentialInitialValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
handleChange,
|
|
||||||
errors,
|
|
||||||
value
|
|
||||||
} = useField(toRef(props, "groupName"), toRef(props, "validationRules"), fieldOptions);
|
|
||||||
|
|
||||||
// First land on the blank, unselected page, no handleChange
|
|
||||||
// Land on page with initial values, handleChange
|
|
||||||
const validateValue = value;
|
|
||||||
return {
|
|
||||||
handleChange,
|
|
||||||
errors,
|
|
||||||
validateValue,
|
|
||||||
fieldOptions, // only need to expose this for unit test purposes
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.list-card {
|
|
||||||
border: 1px solid $gray-500;
|
border: 1px solid $gray-500;
|
||||||
|
|
||||||
&.invalid {
|
&.has-error {
|
||||||
//Red border if invalid
|
input[type="checkbox"],
|
||||||
border: 1px solid $red;
|
input[type="radio"] {
|
||||||
|
@include list-card-focus($red);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
// svg's should be constructed on the same canvas size/viewbox to ensure they occupy the same space in the DOM. This will allow easy/proper alignment of elements. See exisitng svg's for examples.
|
// svg's should be constructed on the same canvas size/viewbox to ensure they occupy the same space in the DOM. This will allow easy/proper alignment of elements. See exisitng svg's for examples.
|
||||||
height: auto;
|
height: auto;
|
||||||
width: 6.5rem;
|
width: 6.5rem;
|
||||||
margin-bottom: 2.2rem;
|
margin-bottom: 2.2rem;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
@include media-breakpoint-up(sm) {
|
|
||||||
box-shadow: 0px 0px 0px 4px $blue-300;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"],
|
input[type="checkbox"],
|
||||||
input[type="radio"] {
|
input[type="radio"] {
|
||||||
opacity: 0;
|
|
||||||
width: 0;
|
|
||||||
height: 0.1px; // NOTE: cannot be zero or safari can't put focus on it
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
+ label {
|
|
||||||
outline: none;
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: $gray-600;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&.sub-copy {
|
|
||||||
color: $gray-550;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:checked + label {
|
|
||||||
background: $blue-100;
|
|
||||||
box-shadow: 0 0 0 1px $blue;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
&:focus-visible + label {
|
|
||||||
box-shadow: 0 0 0 2.5px $blue;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
&:focus + label {
|
|
||||||
box-shadow: 0 0 0 2.5px $blue;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
&:checked:focus + label {
|
|
||||||
box-shadow: 0 0 0 2.5px $blue;
|
|
||||||
}
|
|
||||||
&:checked + label {
|
|
||||||
p {
|
|
||||||
color: $black;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sub-copy {
|
|
||||||
color: $gray-600;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+ label::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
margin: 3rem 0 0 0;
|
|
||||||
background: white;
|
|
||||||
border: 1px solid $gray-500;
|
|
||||||
border-radius: 2px;
|
|
||||||
order: 2;
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: $gray-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
+ label.checkboxTop::before {
|
+ .list-card-content {
|
||||||
margin: -1.25rem 0.5rem 0 0 !important;
|
outline: none;
|
||||||
}
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
+ label.checkboxTop::after {
|
p {
|
||||||
margin: -1.5rem 0.5rem 0 0 !important;
|
color: $gray-600;
|
||||||
}
|
text-align: center;
|
||||||
|
|
||||||
&:checked + label::before {
|
&.sub-copy {
|
||||||
background: $blue;
|
color: $gray-550;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:checked + label::after {
|
&:checked + .list-card-content {
|
||||||
content: "";
|
background: $blue-100;
|
||||||
position: absolute;
|
box-shadow: 0 0 0 1px $blue;
|
||||||
margin: 3.2rem 0 0 0;
|
border-radius: 0.5rem;
|
||||||
border-left: 2px solid $white;
|
}
|
||||||
border-bottom: 2px solid $white;
|
|
||||||
height: 6px;
|
@include list-card-focus($blue);
|
||||||
width: 11px;
|
|
||||||
transform: rotate(-45deg);
|
&:checked + .list-card-content {
|
||||||
z-index: 1;
|
p {
|
||||||
}
|
color: $black;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-copy {
|
||||||
|
color: $gray-600;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+ .list-card-content::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
margin: 3rem 0 0 0;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid $gray-500;
|
||||||
|
border-radius: 2px;
|
||||||
|
order: 2;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: $gray-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ .list-card-content.checkboxTop::before {
|
||||||
|
margin: -1.25rem 0.5rem 0 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ .list-card-content.checkboxTop::after {
|
||||||
|
margin: -1.5rem 0.5rem 0 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked + .list-card-content::before {
|
||||||
|
background: $blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked + .list-card-content::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
margin: 3.2rem 0 0 0;
|
||||||
|
border-left: 2px solid $white;
|
||||||
|
border-bottom: 2px solid $white;
|
||||||
|
height: 6px;
|
||||||
|
width: 11px;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="radio"] {
|
input[type="radio"] {
|
||||||
+ label::before {
|
+ .list-card-content::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
+ label::after {
|
+ .list-card-content::after {
|
||||||
content: "";
|
content: "";
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
+ label {
|
+ .list-card-content {
|
||||||
img {
|
img {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.horizontal {
|
&.horizontal {
|
||||||
img {
|
img {
|
||||||
margin-bottom: 0;
|
|
||||||
width: 5.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"],
|
|
||||||
input[type="radio"] {
|
|
||||||
+ label::before {
|
|
||||||
content: "";
|
|
||||||
position: relative;
|
|
||||||
margin: 0 0.5rem 0 0;
|
|
||||||
order: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:checked + label::after {
|
|
||||||
content: "";
|
|
||||||
margin: -0.15rem 0 0 0;
|
|
||||||
left: 1.175rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:checked + label {
|
|
||||||
p {
|
|
||||||
color: $black;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sub-copy {
|
|
||||||
color: $gray-600;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+ label {
|
|
||||||
outline: none;
|
|
||||||
min-height: 48px;
|
|
||||||
color: $gray-600;
|
|
||||||
|
|
||||||
img {
|
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
width: 5.5rem;
|
||||||
|
|
||||||
p {
|
|
||||||
color: $gray-600;
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
&.sub-copy {
|
|
||||||
color: $gray-550;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
input[type="checkbox"],
|
||||||
|
input[type="radio"] {
|
||||||
|
+ .list-card-content::before {
|
||||||
|
content: "";
|
||||||
|
position: relative;
|
||||||
|
margin: 0 0.5rem 0 0;
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked + .list-card-content::after {
|
||||||
|
content: "";
|
||||||
|
margin: -0.15rem 0 0 0;
|
||||||
|
left: 1.175rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked + .list-card-content {
|
||||||
|
p {
|
||||||
|
color: $black;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-copy {
|
||||||
|
color: $gray-600;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+ .list-card-content {
|
||||||
|
outline: none;
|
||||||
|
min-height: 48px;
|
||||||
|
color: $gray-600;
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: $gray-600;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&.sub-copy {
|
||||||
|
color: $gray-550;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,34 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="loader"
|
class="loader"
|
||||||
role="alert"
|
role="alert"
|
||||||
aria-label="Loading new page"
|
aria-label="Loading new page"
|
||||||
v-bind:class="[this.loaderColor, this.loaderPosition]"
|
v-bind:class="[this.loaderColor, this.loaderPosition]"></div>
|
||||||
></div>
|
</template>
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: "loader",
|
name: "loader",
|
||||||
/* Specify size in number value which translates to rem value. For example, 1.5 = 1.5rem = 24px */
|
/* Specify size in number value which translates to rem value. For example, 1.5 = 1.5rem = 24px */
|
||||||
props: {
|
props: {
|
||||||
/* Color options: red, green, blue, white, black */
|
/* Color options: red, green, blue, white, black */
|
||||||
loaderColor: {
|
loaderColor: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
/* Position options: center, right, left (OPTIONAL, do NOT use on btn-* classes) */
|
/* Position options: center, right, left (OPTIONAL, do NOT use on btn-* classes) */
|
||||||
loaderPosition: {
|
loaderPosition: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.loader {
|
.loader {
|
||||||
display: flex;
|
display: flex;
|
||||||
//Open an overlay to prevent page interaction
|
|
||||||
&:before {
|
//Open an overlay to prevent page interaction
|
||||||
|
&:before {
|
||||||
content: "";
|
content: "";
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
@ -38,9 +38,9 @@
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
//Spinner basics
|
//Spinner basics
|
||||||
&:after {
|
&:after {
|
||||||
content: "";
|
content: "";
|
||||||
mask: url(../../assets/icons/spinner.svg);
|
mask: url(../../assets/icons/spinner.svg);
|
||||||
mask-size: cover;
|
mask-size: cover;
|
||||||
|
|
@ -49,42 +49,38 @@
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
animation: rotation 1s infinite linear;
|
animation: rotation 1s infinite linear;
|
||||||
@keyframes rotation {
|
@keyframes rotation {
|
||||||
100% {
|
100% {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Spinner position
|
//Spinner position
|
||||||
&.center {
|
&.center {
|
||||||
position: absolute;
|
|
||||||
right: 50%;
|
right: 50%;
|
||||||
transform: translateX(50%);
|
transform: translateX(50%);
|
||||||
}
|
}
|
||||||
&.right {
|
&.right {
|
||||||
position: absolute;
|
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
}
|
}
|
||||||
&.left {
|
&.left {
|
||||||
position: absolute;
|
|
||||||
left: 1rem;
|
left: 1rem;
|
||||||
}
|
}
|
||||||
//Spinner color
|
//Spinner color
|
||||||
&:after {
|
&:after {
|
||||||
//Default spinner color (blue) if no other color is specified from the options below
|
//Default spinner color (blue) if no other color is specified from the options below
|
||||||
background-color: $blue;
|
background-color: $blue;
|
||||||
}
|
|
||||||
&.red:after {
|
|
||||||
background-color: $red;
|
|
||||||
}
|
|
||||||
&.green:after {
|
|
||||||
background-color: $green;
|
|
||||||
}
|
|
||||||
&.white:after {
|
|
||||||
background-color: $white;
|
|
||||||
}
|
|
||||||
&.black:after {
|
|
||||||
background-color: $black;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
&.red:after {
|
||||||
|
background-color: $red;
|
||||||
|
}
|
||||||
|
&.green:after {
|
||||||
|
background-color: $green;
|
||||||
|
}
|
||||||
|
&.white:after {
|
||||||
|
background-color: $white;
|
||||||
|
}
|
||||||
|
&.black:after {
|
||||||
|
background-color: $black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue