commit
c68a7b43d1
23 changed files with 3153 additions and 79 deletions
|
|
@ -8,7 +8,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import "./node_modules/bootstrap/scss/bootstrap";
|
@import "./node_modules/bootstrap/scss/bootstrap";
|
||||||
@import "@/styles/common-animations.scss";
|
@import "@/styles/common-styles.scss";
|
||||||
|
@import "@/styles/common-typography-styles.scss";
|
||||||
|
@import "@/styles/common-error-styles.scss";
|
||||||
|
@import "@/styles/common-animations.scss";
|
||||||
</style>
|
</style>
|
||||||
3
src/assets/icons/spinner.svg
Normal file
3
src/assets/icons/spinner.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" xml:space="preserve">
|
||||||
|
<path d="M24.01 48c-2.93 0-5.92-.52-8.85-1.58-6.25-2.25-11.33-7.33-13.58-13.57C-1.25 25-.25 16.76 4.34 10.22A24.05 24.05 0 0 1 24 0v2.26c-7.08 0-13.74 3.46-17.81 9.27-4.16 5.91-5.07 13.4-2.49 20.55 2.03 5.62 6.6 10.19 12.22 12.22 7.14 2.58 14.64 1.67 20.56-2.49A21.78 21.78 0 0 0 45.74 24H48c0 7.81-3.82 15.16-10.22 19.66A23.846 23.846 0 0 1 24.01 48z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 446 B |
117
src/common-components/button-question/button-question.spec.js
Normal file
117
src/common-components/button-question/button-question.spec.js
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import buttonQuestion from "@/common-components/button-question/button-question";
|
||||||
|
|
||||||
|
jest.mock("@/store", () => { return {}; }, { virtual: true });
|
||||||
|
|
||||||
|
describe("buttonQuestion.vue", () => {
|
||||||
|
it("Should show overflow classes on fieldset if isOverflowScrollable is true", () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(buttonQuestion, {
|
||||||
|
propsData: {
|
||||||
|
isOverflowScrollable: true,
|
||||||
|
groupName: "group-name"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const fieldSet = wrapper.find('fieldset');
|
||||||
|
expect(fieldSet.classes()).toContain("overflow-scroll");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("buttonQuestion.vue", () => {
|
||||||
|
it("Fieldset classes should contain row if button type is listCard", () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(buttonQuestion, {
|
||||||
|
propsData: {
|
||||||
|
buttonType: "listCard",
|
||||||
|
groupName: "group-name"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Assert
|
||||||
|
const Div = wrapper.find('fieldset div');
|
||||||
|
expect(Div.classes()).toContain("row");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("buttonQuestion.vue", () => {
|
||||||
|
it("Fieldset classes should contain d-flex if button type is listButtonHorizontal", () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(buttonQuestion, {
|
||||||
|
propsData: {
|
||||||
|
buttonType: "listButtonHorizontal",
|
||||||
|
groupName: "group-name"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Assert
|
||||||
|
const Div = wrapper.find('fieldset div');
|
||||||
|
expect(Div.classes()).toContain("d-flex");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("buttonQuestion.vue", () => {
|
||||||
|
it("Fieldset classes should contain ui-radio if button type is radio", () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(buttonQuestion, {
|
||||||
|
propsData: {
|
||||||
|
buttonType: "radio",
|
||||||
|
groupName: "group-name"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Assert
|
||||||
|
const Div = wrapper.find('fieldset div');
|
||||||
|
expect(Div.classes()).toContain("ui-radio");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// testing a computed property
|
||||||
|
describe("buttonQuestion.vue", () => {
|
||||||
|
it("getColLength should return '12' if prop isWide is set to true", () => {
|
||||||
|
// Act
|
||||||
|
const localThis = { isWide: true }
|
||||||
|
|
||||||
|
expect(buttonQuestion.computed.getColLength.call(localThis)).toBe("12");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("buttonQuestion.vue", () => {
|
||||||
|
it("getColLength should return '' if prop isWide is set to false", () => {
|
||||||
|
// Act
|
||||||
|
const localThis = {
|
||||||
|
isWide: false,
|
||||||
|
answers: ['a', 'b'],
|
||||||
|
groupName: "group-name"
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(buttonQuestion.computed.getColLength.call(localThis)).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("buttonQuestion.vue", () => {
|
||||||
|
it("Should return answer.Text if prop useTextForValue is true", async () => {
|
||||||
|
// Act
|
||||||
|
const localThis = { useTextForValue: true };
|
||||||
|
const answer = { 'Name': 'testName', 'Text': 'testText' };
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(buttonQuestion.methods.getValue.call(localThis, answer)).toBe('testText');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("buttonQuestion.vue", () => {
|
||||||
|
it("Should return answer.Name if prop useTextForValue is false and answer.Name exists", async () => {
|
||||||
|
// Act
|
||||||
|
const localThis = { useTextForValue: false };
|
||||||
|
const answer = { 'Name': 'testName', 'Text': 'testText' };
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(buttonQuestion.methods.getValue.call(localThis, answer)).toBe('testName');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupMocks(mountOptionsMockData = {}) {
|
||||||
|
const defaultMountOptions = { route: { query: { issPage: 'page-name' } } };
|
||||||
|
|
||||||
|
return defaultMountOptions;
|
||||||
|
}
|
||||||
253
src/common-components/button-question/button-question.vue
Normal file
253
src/common-components/button-question/button-question.vue
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
<template>
|
||||||
|
<div :class="isOverflowScrollable ? 'button-question button-question-overflow' : 'button-question'">
|
||||||
|
<div v-if="questionText && answers && answers.length > 0" class="question-text d-flex">
|
||||||
|
<span class="fw-bold w-100">{{ questionText }}</span>
|
||||||
|
</div>
|
||||||
|
<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)">
|
||||||
|
<legend class="sr-only" :data-focus-target="formatString(groupName)" :id="formatString(groupName)" tabindex="-1">
|
||||||
|
{{ questionText }}
|
||||||
|
{{(isMultiSelect && answers && answers.length > 1) ? 'Select one or more options below.' : 'Select an option below.' }}
|
||||||
|
</legend>
|
||||||
|
<div :class="getComponentLoopWrapperClasses">
|
||||||
|
<div :class="getComponentWrapperClasses" v-for="answer in answers" :key="answer.Name ? answer.Name : answer">
|
||||||
|
<component
|
||||||
|
:is="buttonType"
|
||||||
|
@isCheckedChanged="handleCheckedChanged"
|
||||||
|
:buttonID="answer.Name ? formatString(groupName) + '-' + answer.Name : formatString(groupName) + '-' + getAnswerString(answer, 'Text')"
|
||||||
|
:value="getValue(answer)"
|
||||||
|
:buttonLabel="answer.Text ? answer.Text : getAnswerString(answer, 'Name')"
|
||||||
|
:buttonLabelSubCopy="answer.SubText"
|
||||||
|
:textPosition="textPosition"
|
||||||
|
:isMultiSelect="isMultiSelect"
|
||||||
|
:groupName="formatString(groupName)"
|
||||||
|
:selectingInitiatesLoad="selectingInitiatesLoad"
|
||||||
|
:loaderColor="loaderColor"
|
||||||
|
:loaderPosition="loaderPosition"
|
||||||
|
:isWide=isWide
|
||||||
|
:isCashOrInsurance=isCashOrInsurance
|
||||||
|
:isRequired=isRequired
|
||||||
|
:buttonImage="answer.AnswerImageUrl"
|
||||||
|
:buttonImageId="answer.ImageId"
|
||||||
|
:altText="answer.Name ? answer.Name : answer"
|
||||||
|
screenReaderOnlyText="(opens new window)"
|
||||||
|
:colLength="getColLength"
|
||||||
|
:selectedValues="selectedValues"
|
||||||
|
data-test="button"
|
||||||
|
:validationRules="validationRules"
|
||||||
|
:class="[suppressError ? 'alertError' : '' , isCashOrInsurance ? 'radio-fancy' : '']"
|
||||||
|
:valueToLogType="valueToLogType"
|
||||||
|
/>
|
||||||
|
<!-- For nested questions -->
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<div v-if="typeof selectedValues == 'string' && selectedValues == answer.Name">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<div class="row form-test-error mt-1">
|
||||||
|
<error-message :name="formatString(groupName)" v-if="!suppressError"></error-message>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import listButton from "@/ux-components/list-button/list-button";
|
||||||
|
import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-button-horizontal";
|
||||||
|
import listCard from "@/ux-components/list-card/list-card";
|
||||||
|
import { ErrorMessage } from 'vee-validate';
|
||||||
|
import radio from "@/ux-components/radio/radio";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "buttonQuestion",
|
||||||
|
props: {
|
||||||
|
buttonType: {
|
||||||
|
type: String,
|
||||||
|
default: "listButton",
|
||||||
|
},
|
||||||
|
isMultiSelect: Boolean,
|
||||||
|
groupName: String,
|
||||||
|
questionText: String,
|
||||||
|
answers: Array,
|
||||||
|
textPosition: {
|
||||||
|
type: String,
|
||||||
|
default: "text-center",
|
||||||
|
},
|
||||||
|
selectingInitiatesLoad: Boolean,
|
||||||
|
loaderColor: {
|
||||||
|
type: String,
|
||||||
|
default: "blue",
|
||||||
|
},
|
||||||
|
loaderPosition: {
|
||||||
|
type: String,
|
||||||
|
default: "right",
|
||||||
|
},
|
||||||
|
isRequired: Boolean,
|
||||||
|
isOverflowScrollable: Boolean,
|
||||||
|
isWide: Boolean,
|
||||||
|
isCashOrInsurance: Boolean,
|
||||||
|
modelValue: [Array, String],
|
||||||
|
validationRules: String,
|
||||||
|
suppressError: Boolean,
|
||||||
|
useTextForValue: Boolean,
|
||||||
|
valueToLogType: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
getFieldSetClasses() {
|
||||||
|
if (this.isOverflowScrollable) {
|
||||||
|
return "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0";
|
||||||
|
}
|
||||||
|
else if (this.buttonType == "listCard") {
|
||||||
|
return "w-100";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getComponentLoopWrapperClasses() {
|
||||||
|
let classes;
|
||||||
|
switch (this.buttonType) {
|
||||||
|
case "listButton":
|
||||||
|
classes = "w-100";
|
||||||
|
break;
|
||||||
|
case "listButtonHorizontal":
|
||||||
|
classes = "d-flex flex-row p-0";
|
||||||
|
break;
|
||||||
|
case 'listCard':
|
||||||
|
classes = "row g-2 justify-content-center";
|
||||||
|
break;
|
||||||
|
case 'radio':
|
||||||
|
classes = 'ui-radio d-flex'
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return classes;
|
||||||
|
},
|
||||||
|
getComponentWrapperClasses() {
|
||||||
|
let classes = "";
|
||||||
|
|
||||||
|
classes += this.isWide ? "col-12" : "col";
|
||||||
|
|
||||||
|
if (this.buttonType == "radio") {
|
||||||
|
classes += " radio-button-container";
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes;
|
||||||
|
},
|
||||||
|
getColLength(){
|
||||||
|
if(this.isWide) {
|
||||||
|
return "12"
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedValues: {
|
||||||
|
get: function() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set: function(newValue) {
|
||||||
|
this.$emit("update:modelValue", newValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatString(str) {
|
||||||
|
return str.replace(" ", "-");
|
||||||
|
},
|
||||||
|
getValue(answer){
|
||||||
|
if (this.useTextForValue) { return answer.Text }
|
||||||
|
return answer.Name ? answer.Name : answer;
|
||||||
|
},
|
||||||
|
getAnswerString(answer, prop = "Name") {
|
||||||
|
switch (typeof answer) {
|
||||||
|
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,
|
||||||
|
listButtonHorizontal,
|
||||||
|
listCard,
|
||||||
|
ErrorMessage,
|
||||||
|
radio,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.button-question-overflow {
|
||||||
|
height: calc(100vh - 274px);
|
||||||
|
|
||||||
|
.overflow-scroll {
|
||||||
|
// Height will be determined by overall height of content above list
|
||||||
|
height: calc(100% - 314px);
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.button-question {
|
||||||
|
color: $black;
|
||||||
|
|
||||||
|
.radio-button-container {
|
||||||
|
&:not(:last-child) {
|
||||||
|
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: .875rem;
|
||||||
|
text-align: left;
|
||||||
|
margin: 0 0 .5rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.question-text {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
fieldset {
|
||||||
|
.ui-radio {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
@ -11,6 +11,10 @@ const endpoints = {
|
||||||
url: (applicationAbbreviation, pageName) => `/content/api/v1/content/${applicationAbbreviation}/${pageName}`,
|
url: (applicationAbbreviation, pageName) => `/content/api/v1/content/${applicationAbbreviation}/${pageName}`,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
},
|
},
|
||||||
|
GetVehicleYears: {
|
||||||
|
url: "/vehicle/api/v1/vehicle/years",
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export { endpoints };
|
export { endpoints };
|
||||||
|
|
|
||||||
|
|
@ -1,82 +1,74 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="page-container-grouped-styles">
|
||||||
<div class="fade-on-route-transition">
|
<siteHeader cmsWidgetName="SiteHeaderWidget" />
|
||||||
<siteHeader cmsWidgetName="Header"/>
|
<div class="select-car">
|
||||||
<navButton type="button" text="back" :scenario="this.navigationScenarios.CLICKED_BACK"></navButton>
|
<div class="select-car-form rounded text-center">
|
||||||
<p>Vehicle Year Placeholder</p>
|
<siteSubHeader cmsWidgetName="SiteSubHeaderWidget" />
|
||||||
<input type="text" v-model="year"/>
|
<div class="fade-on-route-transition">
|
||||||
<span>{{returnVehicleYear}}</span>
|
<yearQuestion
|
||||||
<button @click="updateVehicleYear(year)">Add</button>
|
class="fade-on-route-transition"
|
||||||
|
v-model="selectedYear"
|
||||||
|
ref="yearQuestion"
|
||||||
|
cmsWidgetName="VehicleYearQuestion"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</template>
|
||||||
<textboxQuestion cmsWidgetName="PolicyZipQuestionWidget" v-model="policyZipCode" inputId="policyZipCode" mask="#####" validationRules="policy-zip-required|policy-zip-format" />
|
|
||||||
</div>
|
<script>
|
||||||
</div>
|
// Components
|
||||||
</template>
|
import yearQuestion from "@/layouts/vehicle-year/year-question/year-question";
|
||||||
|
import siteHeader from "@/common-components/site-header/site-header";
|
||||||
<script>
|
import siteSubHeader from "@/common-components/site-sub-header/site-sub-header";
|
||||||
import navButton from '@/common-components/nav-button/nav-button';
|
|
||||||
import siteHeader from '@/common-components/site-header/site-header';
|
// Supporting files
|
||||||
import textboxQuestion from "@/common-components/textbox-question/textbox-question";
|
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
|
||||||
import { defineRule } from "vee-validate";
|
export default {
|
||||||
import { required } from "@/helpers/validation-rules";
|
name: "vehicle-year",
|
||||||
import { regex } from "@/helpers/validation-rules";
|
data() {
|
||||||
import { errorMessages } from "@/constants/error-messages";
|
return {
|
||||||
|
selectedYear: null,
|
||||||
defineRule("policy-zip-required", required(errorMessages.POLICY_ZIP_REQUIRED));
|
};
|
||||||
defineRule("policy-zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.POLICY_ZIP_FORMAT));
|
},
|
||||||
|
computed: {},
|
||||||
export default {
|
|
||||||
name: 'vehicle-year',
|
async beforeRouteEnter(to, from, next) {
|
||||||
|
// Call APIs
|
||||||
async beforeRouteEnter(to, from, next)
|
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
|
||||||
|
const yearQuestionInitialDataPromise = yearQuestion.methods.loadInitialData();
|
||||||
|
|
||||||
|
// Settle promises and get results
|
||||||
|
const promiseResultMap = [
|
||||||
{
|
{
|
||||||
// Call APIs
|
resultKey: "cmsContent",
|
||||||
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
|
promise: cmsContentPromise,
|
||||||
|
|
||||||
// Settle promises and get results
|
|
||||||
const promiseResultMap = [
|
|
||||||
{
|
|
||||||
resultKey: "cmsContent",
|
|
||||||
promise: cmsContentPromise,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
//use resultMap to populate layout content.
|
|
||||||
let resultMap = await settleAllPromises(promiseResultMap);
|
|
||||||
|
|
||||||
// Call the "next" function to complete the transition to this page.
|
|
||||||
next((vm) => {
|
|
||||||
vm.setCmsContent(resultMap.cmsContent);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
data()
|
|
||||||
{
|
{
|
||||||
return {
|
resultKey: "yearQuestionInitialData",
|
||||||
policyZipCode: ""
|
promise: yearQuestionInitialDataPromise,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods:
|
];
|
||||||
{
|
|
||||||
updateVehicleYear(item)
|
let resultMap = await settleAllPromises(promiseResultMap);
|
||||||
{
|
|
||||||
this.mainStore.updateVehicleYear(item);
|
// Call the "next" function to complete the transition to this page.
|
||||||
}
|
next((vm) => {
|
||||||
},
|
vm.setCmsContent(resultMap.cmsContent);
|
||||||
computed:
|
vm.$refs.yearQuestion.initializeComponent(
|
||||||
{
|
resultMap.yearQuestionInitialData
|
||||||
returnVehicleYear()
|
);
|
||||||
{
|
});
|
||||||
return this.mainStore.order.vehicle.year;
|
},
|
||||||
}
|
|
||||||
},
|
components: {
|
||||||
components: {
|
yearQuestion,
|
||||||
navButton,
|
siteHeader,
|
||||||
textboxQuestion,
|
siteSubHeader,
|
||||||
siteHeader,
|
},
|
||||||
},
|
};
|
||||||
}
|
</script>
|
||||||
</script>
|
|
||||||
56
src/layouts/vehicle-year/year-question/year-question.vue
Normal file
56
src/layouts/vehicle-year/year-question/year-question.vue
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
<template>
|
||||||
|
<buttonQuestion
|
||||||
|
class="radioQuestion"
|
||||||
|
isOverflowScrollable
|
||||||
|
selectingInitiatesLoad
|
||||||
|
:questionText="questionText"
|
||||||
|
:answers="years"
|
||||||
|
groupName="ChooseVehicleYear"
|
||||||
|
textPosition="text-start"
|
||||||
|
v-model="selectedValue"
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import buttonQuestion from "@/common-components/button-question/button-question";
|
||||||
|
import { useMainStore } from '@/store';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "year-question",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
years: Array,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: String,
|
||||||
|
cmsWidgetName: String,
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
buttonQuestion,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
questionText(){
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
|
||||||
|
},
|
||||||
|
selectedValue: {
|
||||||
|
get: function() {
|
||||||
|
return this.modelValue
|
||||||
|
},
|
||||||
|
set: function(newValue) {
|
||||||
|
this.$emit("update:modelValue", newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadInitialData() {
|
||||||
|
return useMainStore().getVehicleYears();
|
||||||
|
},
|
||||||
|
initializeComponent(initialData) {
|
||||||
|
this.years = initialData;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
@ -74,6 +74,15 @@ export const useMainStore = defineStore({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Vehicle API Actions
|
||||||
|
getVehicleYears() {
|
||||||
|
return globalMethods.callHttpClient({
|
||||||
|
method: endpoints.GetVehicleYears.method,
|
||||||
|
endpoint: endpoints.GetVehicleYears.url,
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// Vehicle API Actions
|
// Vehicle API Actions
|
||||||
updateVehicleYear(year)
|
updateVehicleYear(year)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
151
src/styles/common-error-styles.scss
Normal file
151
src/styles/common-error-styles.scss
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
html {
|
||||||
|
.has-error {
|
||||||
|
&.list-button,
|
||||||
|
&.list-card,
|
||||||
|
&.list-card.list-button {
|
||||||
|
border: none;
|
||||||
|
color: $red;
|
||||||
|
input[type=checkbox]:focus + label,
|
||||||
|
input[type=radio]:focus + label {
|
||||||
|
box-shadow: 0 0 0 2.5px $red;
|
||||||
|
}
|
||||||
|
input[type=checkbox]:checked + label {
|
||||||
|
box-shadow: 0 0 0 1px $red;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0px 0px 0px 4px $red-200;
|
||||||
|
border-radius: .5rem;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
border: 1px solid $red;
|
||||||
|
border-radius: .5rem;
|
||||||
|
}
|
||||||
|
label:hover {
|
||||||
|
box-shadow: 0px 0px 0px 4px $red-200;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid $red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.list-button-horizontal {
|
||||||
|
color: $red;
|
||||||
|
label {
|
||||||
|
border: 1px solid $red;
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0px 0px 0px 4px $red-200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input[type=checkbox]:focus + label,
|
||||||
|
input[type=radio]:focus + label {
|
||||||
|
box-shadow: 0 0 1px $red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ui-radio,
|
||||||
|
&.ui-checkbox {
|
||||||
|
input[type=checkbox],
|
||||||
|
input[type=radio],
|
||||||
|
input[type=radio]+label:before,
|
||||||
|
input[type=checkbox]+label:before {
|
||||||
|
border: 1px solid $red;
|
||||||
|
background-color: initial;
|
||||||
|
}
|
||||||
|
input[type=checkbox]:checked + label:before {
|
||||||
|
border: 1px solid $blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.textbox-question,
|
||||||
|
&.dropdown-question {
|
||||||
|
input:hover {
|
||||||
|
box-shadow: 0px 0px 0px 4px $red-200;
|
||||||
|
border-radius: .5rem;
|
||||||
|
border: 1px solid $red;
|
||||||
|
}
|
||||||
|
input:focus {
|
||||||
|
box-shadow: 0 0 0 2.5px $red;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
color: $red;
|
||||||
|
}
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
box-shadow: 0 0 0 1px $red;
|
||||||
|
&:focus {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%23d4281c'/%3e%3c/svg%3e");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 0.75rem center;
|
||||||
|
background-size: 16px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Restore to default style if alert box is present
|
||||||
|
.alertError {
|
||||||
|
.has-error {
|
||||||
|
&.list-button,
|
||||||
|
&.list-card {
|
||||||
|
border: 1px solid $gray-500;
|
||||||
|
input:not(:focus) {
|
||||||
|
+ label {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 0 0 1px $gray-500;
|
||||||
|
border-radius: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input:checked:focus {
|
||||||
|
+ label {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
|
border-radius: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input:checked:not(:focus) {
|
||||||
|
+ label {
|
||||||
|
box-shadow: 0 0 0 1px $blue;
|
||||||
|
border-radius: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input:focus {
|
||||||
|
border: 1px solid $gray-500;
|
||||||
|
+ label {
|
||||||
|
box-shadow: 0 0 0 2.5px transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 0 0 4px $blue-300;
|
||||||
|
+ label {
|
||||||
|
box-shadow: 0 0 0 2.5px transparent;
|
||||||
|
border: 1px solid $blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-test-error {
|
||||||
|
color: $red;
|
||||||
|
font-size: .875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-test-invalid {
|
||||||
|
&.btn.btn-primary {
|
||||||
|
color: $gray-600;
|
||||||
|
background: $gray-200;
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: all;
|
||||||
|
font-weight: $font-weight-normal;
|
||||||
|
}
|
||||||
|
&.btn.btn-primary:hover {
|
||||||
|
background: $gray-200;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
&.btn.btn-primary:focus,
|
||||||
|
&.btn.btn-primary:focus-visible {
|
||||||
|
box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $blue-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
60
src/styles/common-styles.scss
Normal file
60
src/styles/common-styles.scss
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
// Common/Global Styles
|
||||||
|
// Use this file for global styles that don't or won't have their own stylesheet
|
||||||
|
body {
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
color: #4D5151;
|
||||||
|
.container-fluid {
|
||||||
|
max-width: 576px; //Remove once desktop app is complete
|
||||||
|
&.container-shadow {
|
||||||
|
box-shadow: 0px 0px 6px 0px rgba(0,0,0,0.15); //Use instead of Bootstrap's helper
|
||||||
|
}
|
||||||
|
&.make-tall {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.prevent-squish{
|
||||||
|
overflow-x: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.container,
|
||||||
|
.container-fluid {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-container{
|
||||||
|
&.make-tall {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
left: -10000px;
|
||||||
|
top: auto;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container-grouped-styles {
|
||||||
|
@extend .container-fluid, .shadow, .rounded-3, .p-2, .position-relative, .make-tall, .px-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Footer modal backdrop adjustments for positioning
|
||||||
|
.modal-backdrop {
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
max-width: 576px;
|
||||||
|
height: calc(100% - 72px);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
67
src/styles/common-typography-styles.scss
Normal file
67
src/styles/common-typography-styles.scss
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
//Typography
|
||||||
|
p,
|
||||||
|
body {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.625;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Headings
|
||||||
|
//add helper fw-bold to any element to get bold style (500)
|
||||||
|
h1,.h1 {
|
||||||
|
line-height: 1.325;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
h2,.h2 {
|
||||||
|
line-height: 1.325;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
h3,.h3 {
|
||||||
|
line-height: 1.375;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
h4,.h4 {
|
||||||
|
line-height: 1.6;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
h5,.h5 {
|
||||||
|
line-height: 1.325;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
h6,.h6 {
|
||||||
|
line-height: 1.7;
|
||||||
|
letter-spacing: .75px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
font-size: .875rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
label,
|
||||||
|
.label {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
caption,
|
||||||
|
.caption {
|
||||||
|
font-size: .75rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Font size
|
||||||
|
.fs-5 {
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fs-6 {
|
||||||
|
font-size: 1rem !important;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
186
src/styles/ux-variables.scss
Normal file
186
src/styles/ux-variables.scss
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
//Colors
|
||||||
|
|
||||||
|
// White/black
|
||||||
|
$white: #FFFFFF;
|
||||||
|
$black: #000000;
|
||||||
|
|
||||||
|
// Blues
|
||||||
|
$blue-100: #E4F1F7;// Used in theme
|
||||||
|
$blue-200: #C1DFEE;
|
||||||
|
$blue-300: #9FCEE6;
|
||||||
|
$blue-400: #69ADCF;// Used in theme
|
||||||
|
$blue-500: #3B8FB8;
|
||||||
|
$blue: #1574A1;// Default Blue
|
||||||
|
$blue-700: #06577C;
|
||||||
|
$blue-800: #003D58;
|
||||||
|
$blue-900: #002433;// Used in theme
|
||||||
|
|
||||||
|
// Reds
|
||||||
|
$red-100: #FFE6E4;
|
||||||
|
$red-200: #FCBFBB;
|
||||||
|
$red-300: #F89892;
|
||||||
|
$red-400: #E65C53;
|
||||||
|
$red: #D4281C;// Default Red
|
||||||
|
$red-600: #AC160B;
|
||||||
|
$red-700: #840900;
|
||||||
|
$red-800: #5B0600;
|
||||||
|
$red-900: #330300;
|
||||||
|
|
||||||
|
// Greens
|
||||||
|
$green-100: #E3F2EA;
|
||||||
|
$green-200: #BCEDD4;
|
||||||
|
$green-300: #94D7B6;
|
||||||
|
$green-400: #5ABC8C;// Used in theme
|
||||||
|
$green-500: #2CA168;
|
||||||
|
$green: #0C7E47;// Default Green
|
||||||
|
$green-700: #006A36;
|
||||||
|
$green-800: #004F28;
|
||||||
|
$green-900: #00331A;
|
||||||
|
|
||||||
|
// Yellows
|
||||||
|
$yellow-100: #FFF5EB;
|
||||||
|
$yellow-200: #FFE1C6;
|
||||||
|
$yellow-300: #FFCAA0;
|
||||||
|
$yellow-400: #F5975D;
|
||||||
|
$yellow: #E86421;// Default Yellow
|
||||||
|
$yellow-600: #BB4B06;
|
||||||
|
$yellow-700: #8E3C00;
|
||||||
|
$yellow-800: #602D00;
|
||||||
|
$yellow-900: #331A00;
|
||||||
|
|
||||||
|
// Grays
|
||||||
|
$gray-100: #F5F5F5;// Used in theme
|
||||||
|
$gray-200: #E3E4E4;// Used in theme
|
||||||
|
$gray-300: #D2D4D4;
|
||||||
|
$gray: #B0B3B3;// Default Gray
|
||||||
|
$gray-500: #8E9292;// Used in theme
|
||||||
|
$gray-550: #727676;// Used in theme
|
||||||
|
$gray-600: #4D5151;// Used in theme
|
||||||
|
$gray-700: #303333;// Used in theme
|
||||||
|
$gray-800: #222424;// Used in theme
|
||||||
|
$gray-900: #181A1A;// Used in theme
|
||||||
|
|
||||||
|
// Miscellaneous Colors
|
||||||
|
$indigo: #6610f2;
|
||||||
|
$purple: #6f42c1;
|
||||||
|
$pink: #d63384;
|
||||||
|
$orange: #fd7e14;
|
||||||
|
$teal: #20c997;
|
||||||
|
$cyan: #0dcaf0;
|
||||||
|
|
||||||
|
// scss-docs-start colors-map
|
||||||
|
$colors: (
|
||||||
|
"blue": $blue,
|
||||||
|
"indigo": $indigo,
|
||||||
|
"purple": $purple,
|
||||||
|
"pink": $pink,
|
||||||
|
"red": $red,
|
||||||
|
"orange": $orange,
|
||||||
|
"yellow": $yellow,
|
||||||
|
"green": $green,
|
||||||
|
"teal": $teal,
|
||||||
|
"cyan": $cyan,
|
||||||
|
"white": $white,
|
||||||
|
"gray": $gray,
|
||||||
|
"gray-dark": $gray-500
|
||||||
|
);
|
||||||
|
|
||||||
|
// scss-docs-start theme-color-variables
|
||||||
|
$primary: $blue;
|
||||||
|
$secondary: $red;
|
||||||
|
$success: $green;
|
||||||
|
$info: $cyan;
|
||||||
|
$warning: $yellow;
|
||||||
|
$danger: $red;
|
||||||
|
$light: $gray-100;
|
||||||
|
$dark: $gray-500;
|
||||||
|
|
||||||
|
// scss-docs-start theme-colors-map
|
||||||
|
$theme-colors: (
|
||||||
|
"primary": $primary,
|
||||||
|
"secondary": $secondary,
|
||||||
|
"success": $success,
|
||||||
|
"info": $info,
|
||||||
|
"warning": $warning,
|
||||||
|
"danger": $danger,
|
||||||
|
"light": $light,
|
||||||
|
"dark": $dark
|
||||||
|
);
|
||||||
|
|
||||||
|
//Default font color
|
||||||
|
$body-color: $gray-600;
|
||||||
|
|
||||||
|
//Fonts
|
||||||
|
$font-family-sans-serif: Roboto, Arial, Helvetica, sans-serif;
|
||||||
|
$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
// stylelint-enable value-keyword-case
|
||||||
|
$font-family-base: $font-family-sans-serif;
|
||||||
|
$font-family-code: $font-family-monospace;
|
||||||
|
$font-size-base: 1rem; // Assumes the browser default, typically `16px`
|
||||||
|
|
||||||
|
//Custom Font size (extra small)
|
||||||
|
$font-size-xsm: $font-size-base * .75;
|
||||||
|
$font-sizes: (
|
||||||
|
7: $font-size-xsm
|
||||||
|
);
|
||||||
|
|
||||||
|
//Font weight
|
||||||
|
$font-weight-lighter: lighter;
|
||||||
|
$font-weight-light: 300;
|
||||||
|
$font-weight-normal: 400;
|
||||||
|
$font-weight-bold: 500;
|
||||||
|
$font-weight-bolder: bolder;
|
||||||
|
|
||||||
|
//Headings
|
||||||
|
$h1-font-size: $font-size-base * 3;
|
||||||
|
$h2-font-size: $font-size-base * 2.625;
|
||||||
|
$h3-font-size: $font-size-base * 2;
|
||||||
|
$h4-font-size: $font-size-base * 1.625;
|
||||||
|
$h5-font-size: $font-size-base * 1.25;
|
||||||
|
$h6-font-size: $font-size-base * .875;
|
||||||
|
|
||||||
|
//Border Radius
|
||||||
|
// Helper classes are rounded, rounded-1, rounded-2, rounded-3
|
||||||
|
$border-radius: .25rem;
|
||||||
|
$border-radius-sm: .2rem;
|
||||||
|
$border-radius-lg: .5rem;//Used for buttons. Can be used for other things, of course.
|
||||||
|
$border-radius-pill: 50rem;
|
||||||
|
|
||||||
|
//Spacing
|
||||||
|
// 8 spacers available instead of the usual 5
|
||||||
|
$spacer: 1rem;
|
||||||
|
$spacers: (
|
||||||
|
0: 0,
|
||||||
|
1: $spacer * .25, /* 4px */
|
||||||
|
2: $spacer * .5, /* 8px */
|
||||||
|
3: $spacer * .75, /* 12px */
|
||||||
|
4: $spacer * 1, /* 16px */
|
||||||
|
5: $spacer * 1.5, /* 24px */
|
||||||
|
6: $spacer * 2, /* 32px */
|
||||||
|
7: $spacer * 2.5, /* 40px */
|
||||||
|
8: $spacer * 3, /* 48px */
|
||||||
|
);
|
||||||
|
|
||||||
|
//Grid breakpoints
|
||||||
|
$grid-breakpoints: (
|
||||||
|
xs: 0,
|
||||||
|
sm: 576px,
|
||||||
|
md: 838px,
|
||||||
|
lg: 1074px,
|
||||||
|
xl: 1416px
|
||||||
|
);
|
||||||
|
|
||||||
|
//Shadow
|
||||||
|
$box-shadow: 0 .5rem 1rem rgba($black, .15);
|
||||||
|
$box-shadow-sm: 0 .125rem .25rem rgba($black, .075);
|
||||||
|
$box-shadow-lg: 0 1rem 3rem rgba($black, .25);//Safelite default
|
||||||
|
$box-shadow-inset: inset 0 1px 2px rgba($black, .075);
|
||||||
|
|
||||||
|
//Alerts
|
||||||
|
$alert-bg-scale: -90%;
|
||||||
|
$alert-border-scale: -100%;
|
||||||
|
$alert-color-scale: 40%;
|
||||||
|
|
||||||
|
//Modal animation
|
||||||
|
$modal-fade-transform: translate(0, 0);
|
||||||
|
$modal-backdrop-opacity: 0;
|
||||||
|
|
@ -0,0 +1,264 @@
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import listButtonHorizontal from "./list-button-horizontal";
|
||||||
|
import { nextTick } from "vue";
|
||||||
|
|
||||||
|
describe("list-button-horizontal.vue", () => {
|
||||||
|
it("Should return input type checkbox if isMultiSelect is true", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(listButtonHorizontal, {
|
||||||
|
propsData: {
|
||||||
|
isMultiSelect: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,344 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="list-group list-button-horizontal d-flex flex-column w-100"
|
||||||
|
:class="[(errors.length > 0 || hasError) ? 'has-error' : '']"
|
||||||
|
@keyup.space="triggerButton()"
|
||||||
|
@keyup.up="handleKeyupArrow()"
|
||||||
|
@keyup.down="handleKeyupArrow()"
|
||||||
|
@keyup.left="handleKeyupArrow()"
|
||||||
|
@keyup.right="handleKeyupArrow()"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:type="isMultiSelect ? 'checkbox' : 'radio'"
|
||||||
|
:id="buttonID"
|
||||||
|
:name="groupName"
|
||||||
|
:aria-required="isRequired"
|
||||||
|
v-model="checkValue"
|
||||||
|
:checked="checkValue"
|
||||||
|
@change="handleInputChange()"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
tabindex="-1"
|
||||||
|
:for="buttonID"
|
||||||
|
: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>
|
||||||
|
import { useField } from "vee-validate";
|
||||||
|
import loader from "@/ux-components/loader/loader";
|
||||||
|
import { toRef } from "vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "listButtonHorizontal",
|
||||||
|
props: {
|
||||||
|
isMultiSelect: Boolean,
|
||||||
|
groupName: String,
|
||||||
|
buttonID: String,
|
||||||
|
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],
|
||||||
|
hasError: Boolean,
|
||||||
|
valueToLogType: String,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
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) {
|
||||||
|
this.handleCheckChange();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
|
||||||
|
&:focus-visible+label {
|
||||||
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus+label {
|
||||||
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked+label {
|
||||||
|
background: $blue-100;
|
||||||
|
box-shadow: 0 0 0 1px $blue;
|
||||||
|
outline: none;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked:focus+label {
|
||||||
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked+label p:first-child {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
outline: none;
|
||||||
|
position: relative;
|
||||||
|
background: $white;
|
||||||
|
transition: all 150ms linear;
|
||||||
|
border: 1px solid $gray-500;
|
||||||
|
border-radius: 0;
|
||||||
|
width: 100%;
|
||||||
|
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 {
|
||||||
|
font-size: .875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cash/Insurance option radio button styling
|
||||||
|
&.radio-fancy {
|
||||||
|
label {
|
||||||
|
border: 1px solid $blue-700;
|
||||||
|
z-index: 2;
|
||||||
|
color: $blue;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label:hover {
|
||||||
|
background-color: $blue-700;
|
||||||
|
color: $white;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"],
|
||||||
|
input[type="checkbox"] {
|
||||||
|
position: absolute;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
|
||||||
|
&:focus-visible+label {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus+label {
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked+label {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
color: $white;
|
||||||
|
background: linear-gradient(84.45deg, #125B7E 0%, #3B8FB8 100%);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked:focus+label {
|
||||||
|
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked+label p:first-child {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.list-button-horizontal {
|
||||||
|
height: 100%;
|
||||||
|
label {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.col {
|
||||||
|
&:first-of-type {
|
||||||
|
.list-button-horizontal {
|
||||||
|
label {
|
||||||
|
border-bottom-left-radius: 0.5rem;
|
||||||
|
border-top-left-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
.list-button-horizontal {
|
||||||
|
label {
|
||||||
|
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>
|
||||||
|
|
||||||
256
src/ux-components/list-button/list-button.spec.js
Normal file
256
src/ux-components/list-button/list-button.spec.js
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import listButton from "./list-button";
|
||||||
|
import { nextTick } from "vue";
|
||||||
|
|
||||||
|
describe("list-button.vue", () => {
|
||||||
|
it("Should return input type checkbox if isMultiSelect is true", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(listButton, {
|
||||||
|
propsData: {
|
||||||
|
isMultiSelect: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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(listButton, {
|
||||||
|
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(listButton, {
|
||||||
|
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(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;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
241
src/ux-components/list-button/list-button.vue
Normal file
241
src/ux-components/list-button/list-button.vue
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="list-group list-button rounded-3 d-flex flex-column w-100 mb-2"
|
||||||
|
:class="[(errors.length > 0 || hasError) ? 'has-error' : '']"
|
||||||
|
@keyup.space="triggerButton"
|
||||||
|
@keyup.enter="triggerButton"
|
||||||
|
@keyup.up="handleKeyupArrow"
|
||||||
|
@keyup.down="handleKeyupArrow"
|
||||||
|
@keyup.left="handleKeyupArrow"
|
||||||
|
@keyup.right="handleKeyupArrow">
|
||||||
|
<input
|
||||||
|
:type="isMultiSelect ? 'checkbox' : 'radio'"
|
||||||
|
:id="buttonID"
|
||||||
|
:name="groupName"
|
||||||
|
:value="value"
|
||||||
|
:aria-required="isRequired"
|
||||||
|
v-model="checkValue"
|
||||||
|
:checked="checkValue"
|
||||||
|
@change="handleInputChange"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
tabindex="-1"
|
||||||
|
:for="buttonID"
|
||||||
|
: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>
|
||||||
|
import { useField } from "vee-validate";
|
||||||
|
import { toRef } from "vue";
|
||||||
|
import loader from "@/ux-components/loader/loader";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "listButton",
|
||||||
|
props: {
|
||||||
|
isMultiSelect: Boolean,
|
||||||
|
groupName: String,
|
||||||
|
buttonLabel: [Number, String],
|
||||||
|
buttonID: [Number, String],
|
||||||
|
isRequired: Boolean,
|
||||||
|
textPosition: String,
|
||||||
|
buttonLabelSubCopy: String,
|
||||||
|
screenReaderOnlyText: String,
|
||||||
|
selectingInitiatesLoad: Boolean,
|
||||||
|
loaderColor: String,
|
||||||
|
loaderPosition: String,
|
||||||
|
value: {
|
||||||
|
// Field initial value
|
||||||
|
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];
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
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" 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;
|
||||||
|
}
|
||||||
|
&:focus + label {
|
||||||
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
|
}
|
||||||
|
&:checked + label {
|
||||||
|
color: $black;
|
||||||
|
font-weight: 500;
|
||||||
|
background: $blue-100;
|
||||||
|
box-shadow: 0 0 0 1px $blue;
|
||||||
|
}
|
||||||
|
&:checked:focus + label {
|
||||||
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
|
}
|
||||||
|
&:checked + label p,
|
||||||
|
&:checked + label span {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
&:checked + label span:nth-child(2) {
|
||||||
|
font-weight: 400;
|
||||||
|
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>
|
||||||
|
|
||||||
329
src/ux-components/list-card/list-card.spec.js
Normal file
329
src/ux-components/list-card/list-card.spec.js
Normal file
|
|
@ -0,0 +1,329 @@
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import listCard from "./list-card";
|
||||||
|
import { nextTick } from "vue";
|
||||||
|
|
||||||
|
describe("list-card.vue", () => {
|
||||||
|
it("Should return input type checkbox if isMultiSelect is true", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(listCard, {
|
||||||
|
propsData: {
|
||||||
|
isMultiSelect: true,
|
||||||
|
buttonLabel: "Windshield",
|
||||||
|
buttonID: "List Card Checkbox",
|
||||||
|
groupID: "checkbox-demo-1",
|
||||||
|
groupName: "Checkbox 1",
|
||||||
|
buttonImage: "windshield-damage.svg",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const input = wrapper.find("input");
|
||||||
|
expect(input.attributes().type).toEqual("checkbox");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return primary label text", 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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const paragraph = wrapper.find("p");
|
||||||
|
expect(paragraph.text()).toEqual("Windshield");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return secondary (sub) label text", 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",
|
||||||
|
buttonLabelSubCopy: "Test",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const paragraph = wrapper.find("p:nth-of-type(2)");
|
||||||
|
expect(paragraph.text()).toEqual("Test");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return value used for various text settings including the label 'for' and input id", 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",
|
||||||
|
buttonLabelSubCopy: "Test",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const label = wrapper.find("label");
|
||||||
|
expect(label.attributes().for).toEqual("List Card Checkbox");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return input group name used for radio or checkbox", 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",
|
||||||
|
buttonLabelSubCopy: "Test",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const input = wrapper.find("input");
|
||||||
|
expect(input.attributes().name).toEqual("radio 1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return aria-required state", 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const input = wrapper.find("input");
|
||||||
|
expect(input.attributes()["aria-required"]).toEqual("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return flex row classes if isWide is true", 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: true,
|
||||||
|
buttonLabelSubCopy: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const label = wrapper.find("label");
|
||||||
|
expect(label.classes()).toEqual(["d-flex", "w-100", "align-items-center", "px-2", "h-100", "flex-row", "py-2", "ps-4", "pe-4"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return flex row classes if isWide is true and checkboxTop if buttonLabelSubCopy is true", 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: true,
|
||||||
|
buttonLabelSubCopy: "Button Subcopy",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const label = wrapper.find("label");
|
||||||
|
expect(label.classes()).toEqual(["d-flex", "w-100", "align-items-center", "px-2", "h-100", "flex-row", "py-2", "ps-4", "pe-4", "checkboxTop"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
399
src/ux-components/list-card/list-card.vue
Normal file
399
src/ux-components/list-card/list-card.vue
Normal file
|
|
@ -0,0 +1,399 @@
|
||||||
|
<template>
|
||||||
|
<div :class="{'h-100': !isWide}">
|
||||||
|
<div
|
||||||
|
class="list-card w-100 rounded-3 d-flex align-items-center"
|
||||||
|
:class="[
|
||||||
|
'h-100',
|
||||||
|
isWide ? 'horizontal' : '',
|
||||||
|
(errors.length > 0 || hasError) ? 'has-error' : '',
|
||||||
|
]"
|
||||||
|
@keyup.space="triggerButton"
|
||||||
|
@keyup.up="handleKeyupArrow"
|
||||||
|
@keyup.down="handleKeyupArrow"
|
||||||
|
@keyup.left="handleKeyupArrow"
|
||||||
|
@keyup.right="handleKeyupArrow"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:type="isMultiSelect ? 'checkbox' : 'radio'"
|
||||||
|
:id="buttonID"
|
||||||
|
:name="groupName"
|
||||||
|
:value="value"
|
||||||
|
: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>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useField } from "vee-validate";
|
||||||
|
import { toRef } from "vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "listCard",
|
||||||
|
props: {
|
||||||
|
isMultiSelect: Boolean, //Defines use as checkbox
|
||||||
|
isWide: Boolean,
|
||||||
|
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: {
|
||||||
|
getLabelClasses() {
|
||||||
|
if (this.isWide) {
|
||||||
|
let classes = "flex-row py-2 ps-4 pe-4";
|
||||||
|
if (this.buttonLabelSubCopy) {
|
||||||
|
classes += " checkboxTop";
|
||||||
|
}
|
||||||
|
return classes;
|
||||||
|
} else {
|
||||||
|
return "flex-column pt-4 pb-3";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isValueSelectedByArray(arr) {
|
||||||
|
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) {
|
||||||
|
this.handleCheckChange();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
|
||||||
|
&.invalid {
|
||||||
|
//Red border if invalid
|
||||||
|
border: 1px solid $red;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
height: auto;
|
||||||
|
width: 6.5rem;
|
||||||
|
margin-bottom: 2.2rem;
|
||||||
|
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="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;
|
||||||
|
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 {
|
||||||
|
margin: -1.25rem 0.5rem 0 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ label.checkboxTop::after {
|
||||||
|
margin: -1.5rem 0.5rem 0 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked + label::before {
|
||||||
|
background: $blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked + label::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"] {
|
||||||
|
+ label::before {
|
||||||
|
content: "";
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ label::after {
|
||||||
|
content: "";
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ label {
|
||||||
|
img {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.horizontal {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: $gray-600;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&.sub-copy {
|
||||||
|
color: $gray-550;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
90
src/ux-components/loader/loader.vue
Normal file
90
src/ux-components/loader/loader.vue
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="loader"
|
||||||
|
role="alert"
|
||||||
|
aria-label="Loading new page"
|
||||||
|
v-bind:class="[this.loaderColor, this.loaderPosition]"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "loader",
|
||||||
|
/* Specify size in number value which translates to rem value. For example, 1.5 = 1.5rem = 24px */
|
||||||
|
props: {
|
||||||
|
/* Color options: red, green, blue, white, black */
|
||||||
|
loaderColor: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
/* Position options: center, right, left (OPTIONAL, do NOT use on btn-* classes) */
|
||||||
|
loaderPosition: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.loader {
|
||||||
|
display: flex;
|
||||||
|
//Open an overlay to prevent page interaction
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
z-index: 9999;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
//Spinner basics
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
mask: url(../../assets/icons/spinner.svg);
|
||||||
|
mask-size: cover;
|
||||||
|
position: relative;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
animation: rotation 1s infinite linear;
|
||||||
|
@keyframes rotation {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Spinner position
|
||||||
|
&.center {
|
||||||
|
position: absolute;
|
||||||
|
right: 50%;
|
||||||
|
transform: translateX(50%);
|
||||||
|
}
|
||||||
|
&.right {
|
||||||
|
position: absolute;
|
||||||
|
right: 1rem;
|
||||||
|
}
|
||||||
|
&.left {
|
||||||
|
position: absolute;
|
||||||
|
left: 1rem;
|
||||||
|
}
|
||||||
|
//Spinner color
|
||||||
|
&:after {
|
||||||
|
//Default spinner color (blue) if no other color is specified from the options below
|
||||||
|
background-color: $blue;
|
||||||
|
}
|
||||||
|
&.red:after {
|
||||||
|
background-color: $red;
|
||||||
|
}
|
||||||
|
&.green:after {
|
||||||
|
background-color: $green;
|
||||||
|
}
|
||||||
|
&.white:after {
|
||||||
|
background-color: $white;
|
||||||
|
}
|
||||||
|
&.black:after {
|
||||||
|
background-color: $black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
110
src/ux-components/radio/radio.spec.js
Normal file
110
src/ux-components/radio/radio.spec.js
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import radio from "./radio";
|
||||||
|
|
||||||
|
describe("radio.vue", () => {
|
||||||
|
it("Should return group name", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(radio, {
|
||||||
|
propsData: {
|
||||||
|
groupName: "radio-button-test",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const input = wrapper.find("input");
|
||||||
|
|
||||||
|
// Expect
|
||||||
|
expect(input.attributes().name).toEqual("radio-button-test");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return checkbox id", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(radio, {
|
||||||
|
propsData: {
|
||||||
|
buttonID: "Radio ID",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const input = wrapper.find("input");
|
||||||
|
|
||||||
|
// Expect
|
||||||
|
expect(input.attributes().id).toEqual("Radio ID");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return label text", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(radio, {
|
||||||
|
propsData: {
|
||||||
|
buttonLabel: "label text",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const paragraph = wrapper.find("p");
|
||||||
|
|
||||||
|
expect(paragraph.text()).toEqual("label text");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return label text", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(radio, {
|
||||||
|
propsData: {
|
||||||
|
screenReaderOnlyText: "screenreader text",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const paragraph = wrapper.find("span");
|
||||||
|
|
||||||
|
expect(paragraph.text()).toEqual("screenreader text");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should emit button value on click", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(radio, {
|
||||||
|
global: {
|
||||||
|
mocks: {
|
||||||
|
'$route': { query: { issPage: 'page-name' } },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
propsData: {
|
||||||
|
buttonLabel: "Windshield",
|
||||||
|
value: "List Card Checkbox",
|
||||||
|
buttonID: "List Card Checkbox",
|
||||||
|
groupID: "radio-demo-1",
|
||||||
|
groupName: "radio 1",
|
||||||
|
isRequired: true,
|
||||||
|
isWide: false,
|
||||||
|
modelValue: ["List Card Checkbox"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
wrapper.vm.handleCheckChange();
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{"buttonID": "List Card Checkbox", value: "List Card Checkbox", checkValue: false}]);;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(radio, {
|
||||||
|
global: {
|
||||||
|
mocks: {
|
||||||
|
'$route': { query: { issPage: 'page-name' } },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
propsData: {
|
||||||
|
buttonLabel: "Windshield",
|
||||||
|
buttonID: "List Card Checkbox",
|
||||||
|
groupID: "radio-demo-1",
|
||||||
|
groupName: "radio 1",
|
||||||
|
buttonImage: "windshield-damage.svg",
|
||||||
|
isRequired: true,
|
||||||
|
modelValue: ["List Card Checkbox"],
|
||||||
|
value: "Car-Front",
|
||||||
|
selectedValues: "Car-Front"
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.componentVM.checkValue).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
139
src/ux-components/radio/radio.vue
Normal file
139
src/ux-components/radio/radio.vue
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
<template>
|
||||||
|
<!-- Checkbox groups MUST be wrapped in a <fieldset> and <legend> tag and must contain tabindex -->
|
||||||
|
<div class="ui-radio form-check" :class="[(errors.length > 0 || hasError) ? 'has-error' : '']">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="form-check-input"
|
||||||
|
aria-checked="false"
|
||||||
|
:name="groupName"
|
||||||
|
:id="buttonID"
|
||||||
|
:aria-required="isRequired"
|
||||||
|
:value="value"
|
||||||
|
:v-model="checkValue"
|
||||||
|
@change="handleCheckChange"
|
||||||
|
:checked="checkValue"
|
||||||
|
:validationRules="validationRules"
|
||||||
|
/>
|
||||||
|
<label class="d-flex align-items-start form-check-label" :for="buttonID">
|
||||||
|
<p v-if="buttonLabel" class="m-0">{{ buttonLabel }}</p>
|
||||||
|
<span v-if="screenReaderOnlyText" class="sr-only">{{
|
||||||
|
screenReaderOnlyText
|
||||||
|
}}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useField } from "vee-validate";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "radio",
|
||||||
|
props: {
|
||||||
|
groupName: String,
|
||||||
|
buttonLabel: String,
|
||||||
|
buttonID: String,
|
||||||
|
isRequired: Boolean,
|
||||||
|
value: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
screenReaderOnlyText: String,
|
||||||
|
selectedValues: String,
|
||||||
|
hasError: Boolean,
|
||||||
|
validationRules: String,
|
||||||
|
valueToLogType: String,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
checkValue: Boolean,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.selectedValues) {
|
||||||
|
this.checkValue = this.selectedValues === this.value;
|
||||||
|
this.handleCheckChange();
|
||||||
|
} else{
|
||||||
|
this.checkValue = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleCheckChange() {
|
||||||
|
this.handleChange(this.value);
|
||||||
|
const emitEvent = {
|
||||||
|
checkValue: this.checkValue,
|
||||||
|
value: this.value.toString(),
|
||||||
|
buttonID: this.buttonID && this.buttonID.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$emit("isCheckedChanged", emitEvent);
|
||||||
|
this.$emit("update:modelValue", emitEvent);
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const inputType = "radio";
|
||||||
|
const {
|
||||||
|
value: inputValue,
|
||||||
|
handleChange,
|
||||||
|
errors,
|
||||||
|
} = useField(props.groupName, props.validationRules,
|
||||||
|
{
|
||||||
|
type: inputType,
|
||||||
|
checkedValue: props.value,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
handleChange,
|
||||||
|
errors,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.form-check {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.form-check-input {
|
||||||
|
border: 1px solid $gray-500;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
|
||||||
|
&:checked {
|
||||||
|
background-color: $white;
|
||||||
|
background-size: 71%;
|
||||||
|
background-position: center;
|
||||||
|
border: 1px solid $blue;
|
||||||
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50' xml:space='preserve'%3e%3ccircle cx='25' cy='25' r='25' fill='%231574A1'/%3e%3c/svg%3e");
|
||||||
|
+ label {
|
||||||
|
p {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: .875rem;
|
||||||
|
color: $black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
&, & + label {
|
||||||
|
margin-top: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.form-check-input {
|
||||||
|
box-shadow: 0 0 0 4px $blue-300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: .875rem;
|
||||||
|
color: $gray-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
@ -10,6 +10,7 @@ module.exports = {
|
||||||
// Note: only include functions, variables and mixins here
|
// Note: only include functions, variables and mixins here
|
||||||
additionalData: `
|
additionalData: `
|
||||||
@import "./node_modules/bootstrap/scss/functions";
|
@import "./node_modules/bootstrap/scss/functions";
|
||||||
|
@import "@/styles/ux-variables.scss";
|
||||||
@import "./node_modules/bootstrap/scss/variables";
|
@import "./node_modules/bootstrap/scss/variables";
|
||||||
@import "./node_modules/bootstrap/scss/mixins";
|
@import "./node_modules/bootstrap/scss/mixins";
|
||||||
`,
|
`,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ module.exports = {
|
||||||
// Note: only include functions, variables and mixins here
|
// Note: only include functions, variables and mixins here
|
||||||
additionalData: `
|
additionalData: `
|
||||||
@import "./node_modules/bootstrap/scss/functions";
|
@import "./node_modules/bootstrap/scss/functions";
|
||||||
|
@import "@/styles/ux-variables.scss";
|
||||||
@import "./node_modules/bootstrap/scss/variables";
|
@import "./node_modules/bootstrap/scss/variables";
|
||||||
@import "./node_modules/bootstrap/scss/mixins";
|
@import "./node_modules/bootstrap/scss/mixins";
|
||||||
`,
|
`,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue