Merge branch 'develop' into feature/CSR-254

This commit is contained in:
Adam Caouette 2022-02-10 09:15:45 -05:00
commit 7deaffe68f
17 changed files with 410 additions and 331 deletions

View file

@ -9,29 +9,22 @@ describe("funnel-footer.vue", () => {
const r = { const r = {
test:"testing", test:"testing",
clientHeight: 10, clientHeight: 10,
classList: { offsetHeight: 24,
add: jest.fn(() => ''),
remove: jest.fn()
},
}; };
global.document.getElementById = jest.fn().mockImplementation(()=> {
global.document.querySelector = jest.fn().mockImplementation(()=> {
return r; return r;
}); });
const wrapper = mount(funnelFooter, { const wrapper = mount(funnelFooter, {
propsData: {
footer: true
},
}); });
// Assert // Assert
const link = wrapper.find("a"); const link = wrapper.find("a");
console.log(wrapper.html());
// Expect // Expect
expect(link.attributes('class')).toContain("footer-link"); expect(link.attributes('class')).toContain("footer");
expect(global.document.getElementById).toBeCalled();
expect(r.classList.add).toBeCalledWith("justify-content-end");
expect(r.classList.remove).toBeCalledWith("justify-content-start");
}); });
@ -64,32 +57,4 @@ describe("funnel-footer.vue", () => {
expect(input.attributes("class")).toContain("btn-primary"); expect(input.attributes("class")).toContain("btn-primary");
}); });
it("Should return class justify-content-end if paddingHeight < 80px", async () => {
global.document.getElementById = jest.fn().mockImplementation(()=> {
return {
test:"testing",
clientHeight: 10,
classList: {
add: jest.fn(),
remove: jest.fn()
}
}
});
// Act
const wrapper = mount(funnelFooter, {
propsData: {
footer: true
},
});
const infoBox = document.getElementById('infoBox');
// Assert
const stacked = wrapper.find("#stacked");
wrapper.vm.paddingHeight = 100;
// Expect
expect(stacked.attributes('class')).toContain("justify-content-end");
});
}); });

View file

@ -1,54 +1,41 @@
<template> <template>
<div class="container-fluid g-2 footer"> <div class="footer pt-4">
<div class="row"> <div class="container-fluid g-2">
<div class="col-12 d-flex justify-content-center pb-2 fs-7"> <div class="row">
&copy;&nbsp;<span id="years"></span>&nbsp;Safelite Group <div class="col-12 d-flex justify-content-center pb-2 fs-7">
&copy;&nbsp;{{new Date().getFullYear()}}&nbsp;Safelite Group
</div>
</div>
<div class="row" :style="`padding-bottom: ${paddingHeight}px`">
<div class="col d-flex justify-content-center justify-content-around text-center">
<textLink linkType="footer" text="Terms of use" href="https://www.safelite.com/terms-of-use" />
<textLink linkType="footer" text="Privacy policy" href="https://www.safelite.com/safelite-group-privacy-policy" />
<textLink linkType="footer" text="Do not sell my information" href="https://privacyportal-cdn.onetrust.com/dsarwebform/d3b95a93-e22e-4d4d-a806-482052406557/9e371601-eae3-4338-9430-b90b9036022b.html" />
</div>
</div> </div>
</div> </div>
<div class="row" :style="`padding-bottom: ${paddingHeight}px`"> <div class="container-fluid fixed-bottom g-4 bg-light py-4" id="infoBox">
<div class="col d-flex justify-content-center justify-content-around text-center"> <div class="row d-flex flex-row-reverse align-items-center">
<textLink <div class="col-12 button-col d-flex justify-content-end" id="stacked">
footer=true <buttonMain
text="Terms of use" isPrimary
href="https://www.safelite.com/terms-of-use" :buttonText="buttonText"
/> loaderColor="white"
<textLink sizeInRem="1"
footer=true :class="isDisabled && 'form-test-invalid'"
text="Privacy policy" :aria-disabled="isDisabled"
href="https://www.safelite.com/safelite-group-privacy-policy" @click-event="buttonClick"
/> />
<textLink </div>
footer=true <div class="col-12 link-col py-1">
text="Do not sell my information" <textLink linkType="navigation" :text="backLink" @click-event="linkClick"/>
href="https://privacyportal-cdn.onetrust.com/dsarwebform/d3b95a93-e22e-4d4d-a806-482052406557/9e371601-eae3-4338-9430-b90b9036022b.html" </div>
/>
</div>
</div>
</div>
<div class="container-fluid fixed-bottom g-4 bg-light py-4" id="infoBox">
<div class="row d-flex flex-row-reverse align-items-center">
<div class="col d-flex justify-content-end" id="stacked">
<buttonMain
isPrimary
buttonText="Button test"
loaderColor="white"
sizeInRem="1"
:class="isDisabled && 'form-test-invalid'"
:aria-disabled="isDisabled"
/>
</div>
<div class="col">
<textLink
navigation=true
text="back"
/>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import textLink from "@/ux-components/text-link/text-link"; import textLink from "@/ux-components/text-link/text-link";
import buttonMain from "@/ux-components/button-main/button-main"; import buttonMain from "@/ux-components/button-main/button-main";
@ -63,47 +50,55 @@ export default {
}, },
data() { data() {
return { return {
paddingHeight: 0 paddingHeight: 0,
backLink: "",
buttonText: "",
} }
}, },
mounted() { mounted() {
this.paddingHeight = document.getElementById("infoBox").offsetHeight; this.paddingHeight = document.querySelector(".footer #infoBox").offsetHeight + 24;
this.$nextTick(() => { this.$nextTick(() => {
window.addEventListener('resize', this.onResize); window.addEventListener('resize', this.onResize);
}) })
setTimeout(function(){ // Give it a moment to set the date
document.getElementById('years').innerHTML += new Date().getFullYear();
}, 100);
this.checkHeight();
}, },
beforeUnmount() { beforeUnmount() {
window.removeEventListener('resize', this.onResize); window.removeEventListener('resize', this.onResize);
}, },
methods: { methods: {
onResize() { onResize() {
this.paddingHeight = document.getElementById("infoBox").offsetHeight; this.paddingHeight = document.querySelector(".footer #infoBox").offsetHeight;
this.checkHeight(); },
initializeComponent(cmsContent) {
this.backLink = cmsContent.BackButtonText;
this.buttonText = cmsContent.ForwardButtonText;
},
buttonClick() {
this.$emit("forwardClicked");
},
linkClick() {
this.$emit("backClicked");
}, },
checkHeight() {
const parentHeight = document.getElementById('infoBox').clientHeight;
const wrapper2 = document.getElementById('stacked');
console.log(parentHeight);
console.log(wrapper2);
if (parentHeight > 80) {
wrapper2.classList.add("justify-content-start");
wrapper2.classList.remove("justify-content-end");
} else {
wrapper2.classList.add("justify-content-end");
wrapper2.classList.remove("justify-content-start");
}
}
} }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
button { .footer {
min-width: 200px; display: flex;
margin-top: auto;
.button-col,
.link-col,
.btn-primary {
width: 100%; width: 100%;
} }
@media only screen and (min-width: 340px) {
.button-col,
.link-col {
width: 50% !important;
}
.btn-primary {
width: auto;
}
}
}
</style> </style>

View file

@ -35,12 +35,16 @@ export default {
}, },
props: { props: {
hasBackButton: Boolean, hasBackButton: Boolean,
backButtonAccessibleText: String, backButtonAccessibleText: String
hasSubText: Boolean
}, },
components: { components: {
buttonBack, buttonBack,
}, },
computed: {
hasSubText(){
return this.subText.length > 0;
}
},
methods: { methods: {
clickEvent() { clickEvent() {
this.$emit("click-event"); this.$emit("click-event");

View file

@ -19,7 +19,6 @@ export default {
}, },
data() { data() {
return { return {
vehicleImageSrc: '',
genericVehicleImage: '', genericVehicleImage: '',
carUnmatchedVehicleIcon: '', carUnmatchedVehicleIcon: '',
truckUnmatchedVehicleIcon: '', truckUnmatchedVehicleIcon: '',

View file

@ -5,20 +5,93 @@
<vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=true /> <vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=true />
</div> </div>
</div> </div>
<div class="row my-4"> <form>
<div class="row my-4">
<div class="col">
<label for="autocomplete">Street address</label>
<input ref="autocomplete"
id="autocomplete"
placeholder="Search"
class="form-control search-location"
type="text"
autocomplete="off" />
</div>
</div>
</form>
<form>
<div v-show="showAddressFields" class="row my-4">
<div class="col">
<label for="city">City</label>
<input ref="city"
id="city"
class="form-control"
type="text"
autocomplete="off" />
</div>
</div>
</form>
<form>
<div v-show="showAddressFields" class="row my-4">
<div class="col-6">
<label for="state">State</label>
<input ref="state"
id="state"
class="form-control"
type="text"
autocomplete="off" />
</div>
<div class="col-6">
<label for="zip">Zip</label>
<input ref="zip"
id="zip"
class="form-control"
type="text"
autocomplete="off" />
</div>
</div>
</form>
<div v-show="displayVerificationWarning" class="row my-4">
<div class="col"> <div class="col">
<label for="autocomplete">Home address</label> <span style="color: red;">An accurate match may not have have been located. Please verify your address before continuing</span>
<input
ref="autocomplete"
id="autocomplete"
placeholder="Search"
class="form-control search-location"
onfocus="value = ''"
type="text"
/>
</div> </div>
</div> </div>
<div v-show="displayNoMatchWarning" class="row my-4">
<div class="col">
<span style="color: red;">An accurate match could not be located. Please re-enter your street address before continuing</span>
</div>
</div>
<div class="row my-4">
<div class="col">
<label for="firstName">First name</label>
<input ref="firstName"
id="firstName"
class="form-control"
type="text"
autocomplete="off" />
</div>
</div>
<div class="row my-4">
<div class="col">
<label for="lastName">Last name</label>
<input ref="lastName"
id="lastName"
class="form-control"
type="text"
autocomplete="off" />
</div>
</div>
<div class="row my-4">
<div class="col">
<label for="email">Email address</label>
<input ref="email"
id="email"
class="form-control"
type="text"
autocomplete="off" />
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
@ -28,21 +101,29 @@ export default {
components: { components: {
vehicleBanner, vehicleBanner,
}, },
data() {
return {
showAddressFields: false,
displayVerificationWarning: false,
displayNoMatchWarning: false
};
},
mounted() { mounted() {
this.$refs.vehicleBanner.initializeComponent({ this.$refs.vehicleBanner.initializeComponent({
GenericVehicleImage: GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3", "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
}); });
let addressField1 = document.getElementById("autocomplete");
let self = this; let self = this;
//AIzaSyDptGCkOPgN2uWJOy4ou4M33phRD4MAoJo - localhost:8080
//AIzaSyBji_2ApAj2vE_XNGyV-LaUEKrmuElpJ1w //let apiKey = "AIzaSyDptGCkOPgN2uWJOy4ou4M33phRD4MAoJo"; // localhost:8080
this.$loadScript( let apiKey = "AIzaSyBji_2ApAj2vE_XNGyV-LaUEKrmuElpJ1w"; // Dev and above
"https://maps.googleapis.com/maps/api/js?key=AIzaSyBji_2ApAj2vE_XNGyV-LaUEKrmuElpJ1w&libraries=places"
) this.$loadScript(`https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`)
.then(() => { .then(() => {
// Script is loaded, do something
// Script is loaded, initialize the autocomplete textbox
self.autocomplete = new window.google.maps.places.Autocomplete( self.autocomplete = new window.google.maps.places.Autocomplete(
self.$refs.autocomplete, self.$refs.autocomplete,
{ {
@ -52,53 +133,101 @@ export default {
} }
); );
self.autocomplete.addListener("place_changed", () => { self.autocomplete.addListener('place_changed', fillInAddress);
let place = self.autocomplete.getPlace();
let address1 = ""; addressField1.onblur = function() {
let postcode = ""; var hover = document.querySelector(".pac-container .pac-item:hover");
let input = document.getElementById("autocomplete"); // if an item has been clicked, do nothing, otherwise get first solution and use Geocoder to get the place
if (hover === null) {
var item = document.querySelector(".pac-container .pac-item");
if (item != null) {
var firstResult = item.textContent;
var geocoder = new window.google.maps.Geocoder();
geocoder.geocode({
address: firstResult
}, function (results, status) {
if (status === window.google.maps.GeocoderStatus.OK) {
fillInAddress(results[0]);
self.displayVerificationWarning = true;
self.displayNoMatchWarning = false;
}
});
}
else {
let city = document.querySelector("#city");
let state = document.querySelector("#state");
let zip = document.querySelector("#zip");
for (const component of place.address_components) { city.value = "";
const componentType = component.types[0]; state.value = "";
zip.value = "";
switch (componentType) { self.displayVerificationWarning = false;
case "street_number": { self.displayNoMatchWarning = true;
address1 = `${component.long_name} ${address1}`; }
break;
}
case "route": {
address1 += component.short_name;
break;
}
case "locality":
address1 += ", " + component.long_name;
break;
case "administrative_area_level_1": {
address1 += ", " + component.short_name;
break;
}
case "postal_code": {
postcode = `${component.long_name}${postcode}`;
break;
}
case "postal_code_suffix": {
postcode = `${postcode}-${component.long_name}`;
break;
}
} }
};
let addressField1 = document.querySelector("#autocomplete"); function fillInAddress(place) {
addressField1.value = address1 + " " + postcode; if (!place) {
place = self.autocomplete.getPlace();
} }
});
if (place && place.address_components) {
let address1 = "";
let city = document.querySelector("#city");
let state = document.querySelector("#state");
let zip = document.querySelector("#zip");
self.showAddressFields = true;
for (const component of place.address_components) {
const componentType = component.types[0];
switch (componentType) {
case "street_number": {
address1 = `${component.long_name} ${address1}`;
break;
}
case "route": {
address1 += component.short_name;
break;
}
case "locality": {
city.value = component.long_name;
break;
}
case "administrative_area_level_1": {
state.value = component.short_name;
break;
}
case "postal_code": {
zip.value = component.long_name;
break;
}
}
addressField1.value = address1;
self.displayVerificationWarning = false;
self.displayNoMatchWarning = false;
}
}
else {
self.displayVerificationWarning = true;
self.displayNoMatchWarning = false;
}
}
}) })
.catch(() => { .catch(() => {
// Failed to fetch script // Failed to fetch script
console.log("Unable to load Google Places API script"); console.log("Unable to load Google Places API script");
}); });
}, },
}; };
</script> </script>

View file

@ -48,18 +48,19 @@
<div class="row"> <div class="row">
<div class="col my-3"> <div class="col my-3">
<textLink <textLink
navigation=true linkType=navigation
text="Navigation Link" text="Navigation Link"
/><br><br> /><br><br>
<textLink <textLink
linkType="text"
text="Default Link" text="Default Link"
/><br><br> /><br><br>
<textLink <textLink
textSmall=true linkType=textSmall
text="Small Link" text="Small Link"
/><br><br> /><br><br>
<textLink <textLink
footer=true linkType=footer
text="Footer Link" text="Footer Link"
href="https://google.com" href="https://google.com"
/> />
@ -673,19 +674,6 @@
/> />
</div> </div>
</div> </div>
<div class="row my-4">
<div class="col">
<h4 class="m-0 p-2 bg-light rounded">Links</h4>
</div>
</div>
<div class="row">
<div class="col my-3">
<textLink navigation="true" text="Navigation Link" /><br /><br />
<textLink text="Default Link" /><br /><br />
<textLink textSmall="true" text="Small Link" /><br /><br />
<textLink footer="true" text="Footer Link" href="https://google.com" />
</div>
</div>
<div class="row my-4"> <div class="row my-4">
<div class="col"> <div class="col">
<h4 class="m-0 p-2 bg-light rounded">Typography</h4> <h4 class="m-0 p-2 bg-light rounded">Typography</h4>
@ -925,16 +913,6 @@
/> />
</div> </div>
</div> </div>
<div class="row my-4">
<div class="col">
<h4 class="m-0 p-2 bg-light rounded">Funnel Footer</h4>
</div>
</div>
<div class="row my-2">
<div class="col">
<funnelFooter/>
</div>
</div>
</div> </div>
</template> </template>
@ -948,9 +926,7 @@ import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelHeader from "@/common-components/funnel-header/funnel-header"; import funnelHeader from "@/common-components/funnel-header/funnel-header";
import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-button-horizontal"; import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-button-horizontal";
import checkbox from "@/ux-components/checkbox/checkbox"; import checkbox from "@/ux-components/checkbox/checkbox";
import buttonQuestion from "@/common-components/button-question/button-question";
import textLink from "@/ux-components/text-link/text-link"; import textLink from "@/ux-components/text-link/text-link";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
export default { export default {
name: "App", name: "App",
components: { components: {
@ -963,9 +939,7 @@ export default {
listButtonHorizontal, listButtonHorizontal,
checkbox, checkbox,
funnelHeader, funnelHeader,
buttonQuestion, textLink
textLink,
funnelFooter
}, },
data() { data() {
return { return {

View file

@ -1,6 +1,7 @@
// Components // Components
import vehicleDamage from "@/layouts/vehicle-damage/vehicle-damage.vue"; import vehicleDamage from "@/layouts/vehicle-damage/vehicle-damage.vue";
import funnelHeader from "@/common-components/funnel-header/funnel-header"; import funnelHeader from "@/common-components/funnel-header/funnel-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question"; import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question";
@ -144,7 +145,7 @@ describe("vehicle-damage.vue", () => {
describe("vehicle-damage.vue", () => { describe("vehicle-damage.vue", () => {
test("Call invalidation, ResetPartsAndDeps should be called", async () => { test("Call invalidation, ResetPartsAndDeps should be called", async () => {
//Arrange //Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
@ -221,6 +222,10 @@ function setupMocks({
damageLocationQuestion.methods = { damageLocationQuestion.methods = {
initializeComponent: jest.fn(), initializeComponent: jest.fn(),
};
funnelFooter.methods = {
initializeComponent: jest.fn(),
} }
const mountOptions = getMountOptions(mountOptionsMockData); const mountOptions = getMountOptions(mountOptionsMockData);
@ -252,5 +257,11 @@ function setupMocks({
damageLocationQuestionWrapper.vm.initializeComponent = damageLocationQuestionWrapper.vm.initializeComponent =
damageLocationQuestion.methods.initializeComponent; damageLocationQuestion.methods.initializeComponent;
const funnelFooterWrapper = wrapper.findComponent({
name: "funnelFooter",
});
funnelFooterWrapper.vm.initializeComponent = funnelFooter.methods.initializeComponent;
return { wrapper, apiPromise }; return { wrapper, apiPromise };
} }

View file

@ -1,11 +1,8 @@
<template> <template>
<div class="container-fluid shadow rounded-3 p-2 position-relative"> <div class="container-fluid shadow rounded-3 p-2 position-relative make-tall">
<funnelHeader ref="funnelHeader" /> <funnelHeader ref="funnelHeader" />
<vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=false /> <vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=false />
<funnelSubHeader <funnelSubHeader ref="funnelSubHeader" />
ref="funnelSubHeader"
:hasSubText=true
/>
<Form <Form
@submit="onSubmit" @submit="onSubmit"
@invalid-submit="onInvalidSubmit" @invalid-submit="onInvalidSubmit"
@ -36,6 +33,7 @@
groupName="DriverSideReplaceOptionsQuestion" groupName="DriverSideReplaceOptionsQuestion"
/> />
<funnel-footer <funnel-footer
ref="funnelFooter"
:isDisabled="!meta.valid" :isDisabled="!meta.valid"
/> />
@ -56,6 +54,7 @@
<script> <script>
// Components // Components
import funnelHeader from "@/common-components/funnel-header/funnel-header"; import funnelHeader from "@/common-components/funnel-header/funnel-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question"; import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-question/replace-options-question";
@ -63,8 +62,6 @@ import damageLocationQuestion from "@/layouts/vehicle-damage/damage-location-que
import windshieldOptions from "@/layouts/vehicle-damage/windshield-options/windshield-options"; import windshieldOptions from "@/layouts/vehicle-damage/windshield-options/windshield-options";
import alert from "@/ux-components/alert/alert"; import alert from "@/ux-components/alert/alert";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import { Form } from 'vee-validate'; import { Form } from 'vee-validate';
@ -118,6 +115,9 @@ export default {
vm.$refs.damageLocation.initializeComponent( vm.$refs.damageLocation.initializeComponent(
resultMap.cmsContent.DamageLocationQuestion, resultMap.damageOptions resultMap.cmsContent.DamageLocationQuestion, resultMap.damageOptions
); );
vm.$refs.funnelFooter.initializeComponent(
resultMap.cmsContent.FunnelFooterWidget
);
}); });
}, },
data(){ data(){

View file

@ -1,10 +1,10 @@
<template> <template>
<div class="container-fluid shadow rounded-3 p-0 position-relative"> <div class="container-fluid shadow rounded-3 p-0 position-relative make-tall">
<funnelHeader ref="funnelHeader" /> <funnelHeader ref="funnelHeader" />
<div class="select-car"> <div class="select-car">
<div class="select-car-form rounded text-center"> <div class="select-car-form rounded text-center">
<vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=true /> <vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=true />
<funnelSubHeader <funnelSubHeader
ref="funnelSubHeader" ref="funnelSubHeader"
:hasBackButton="true" :hasBackButton="true"
backButtonAccessibleText="Change Vehicle Year" backButtonAccessibleText="Change Vehicle Year"

View file

@ -73,7 +73,7 @@ export default {
watch: { watch: {
selectedYear(year) { selectedYear(year) {
this.$store.commit(this.storeMutations.UPDATE_YEAR, year); this.$store.commit(this.storeMutations.UPDATE_YEAR, year);
this.$router.navigateAfterSave( this.$router.navigateAfterSave(
this.navigationScenarios.SELECTED_YEAR, this.navigationScenarios.SELECTED_YEAR,

View file

@ -9,6 +9,11 @@ body {
&.container-shadow { &.container-shadow {
box-shadow: 0px 0px 6px 0px rgba(0,0,0,0.15); //Use instead of Bootstrap's helper 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;
}
} }
.pointer { .pointer {
cursor: pointer; cursor: pointer;

View file

@ -2,7 +2,7 @@
<button <button
:disabled="isDisabled" :disabled="isDisabled"
:aria-disabled="isDisabled" :aria-disabled="isDisabled"
class="btn d-flex align-items-center py-3 px-4" class="btn d-flex align-items-center py-3 px-2"
:class="[isPrimary ? 'btn-primary' : 'btn-secondary',isFloat ? 'float-end' : '']" :class="[isPrimary ? 'btn-primary' : 'btn-secondary',isFloat ? 'float-end' : '']"
@click="clicked()" @click="clicked()"
> >

View file

@ -1,34 +1,34 @@
<template> <template>
<div <div
class="list-group list-button-horizontal d-flex flex-column w-100 mb-2" class="list-group list-button-horizontal d-flex flex-column w-100 mb-2"
@mouseup="handleClick(value)" @mouseup="handleClick(value)"
@keyup.space="handleClick(value)" @keyup.space="handleClick(value)"
> >
<input <input
:type="isMultiSelect ? 'checkbox' : 'radio'" :type="isMultiSelect ? 'checkbox' : 'radio'"
:id="buttonID" :id="buttonID"
:name="groupName" :name="groupName"
:value="buttonID" :value="buttonID"
:aria-required="isRequired" :aria-required="isRequired"
:data-focus-target="groupName" :data-focus-target="groupName"
v-model="checkValue" v-model="checkValue"
@change="handleCheckChange" @change="handleCheckChange"
/> />
<label <label
tabindex="-1" tabindex="-1"
:for="buttonID" :for="buttonID"
:aria-labelledby="buttonID" :aria-labelledby="buttonID"
class="d-flex flex-column justify-content-center py-3 px-4" class="d-flex flex-column justify-content-center py-3 px-4"
> >
<span <span
class="m-0" class="m-0"
:class="textPosition" :class="textPosition"
> >
{{buttonLabel}} {{buttonLabel}}
</span> </span>
<span <span
v-if="buttonLabelSubCopy" v-if="buttonLabelSubCopy"
class="m-0 small" class="m-0 small"
:class="textPosition" :class="textPosition"
> >
{{buttonLabelSubCopy}} {{buttonLabelSubCopy}}
@ -36,10 +36,10 @@
<span v-if="screenReaderOnlyText" class="sr-only"> <span v-if="screenReaderOnlyText" class="sr-only">
{{screenReaderOnlyText}} {{screenReaderOnlyText}}
</span> </span>
<loader <loader
v-if="isLoaderDisplayed && !isMultiSelect" v-if="isLoaderDisplayed && !isMultiSelect"
:style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" :style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}"
:class="[loaderColor, loaderPosition]" :class="[loaderColor, loaderPosition]"
/> />
</label> </label>
</div> </div>
@ -151,8 +151,9 @@ export default {
border: 1px solid $gray-500; border: 1px solid $gray-500;
border-radius: 0; border-radius: 0;
width: 100%; width: 100%;
color: $gray-600;
&:hover { &:hover {
box-shadow: 0 0 0 4px $blue-100; box-shadow: 0 0 0 4px $blue-300;
cursor: pointer; cursor: pointer;
z-index: 2; z-index: 2;
} }

View file

@ -1,40 +1,40 @@
<template> <template>
<div <div
class="list-group list-button d-flex flex-column w-100 mb-2" class="list-group list-button d-flex flex-column w-100 mb-2"
@mouseup="handleClick(value)" @mouseup="handleClick(value)"
@keyup.space="handleClick(value)" @keyup.space="handleClick(value)"
> >
<input <input
:type="isMultiSelect ? 'checkbox' : 'radio'" :type="isMultiSelect ? 'checkbox' : 'radio'"
:id="buttonID" :id="buttonID"
:name="groupName" :name="groupName"
:value="buttonID" :value="buttonID"
:aria-required="isRequired" :aria-required="isRequired"
:data-focus-target="groupName" :data-focus-target="groupName"
v-model="checkValue" v-model="checkValue"
@change="handleCheckChange" @change="handleCheckChange"
> >
<label <label
tabindex="-1" tabindex="-1"
:for="buttonID" :for="buttonID"
:aria-labelledby="buttonID" :aria-labelledby="buttonID"
class="d-flex flex-column justify-content-center py-3 px-4" class="d-flex flex-column justify-content-center py-3 px-4"
> >
<span <span
class="m-0" class="m-0"
:class="textPosition" :class="textPosition"
> >
{{ buttonLabel }} {{ buttonLabel }}
</span> </span>
<span <span
v-if="buttonLabelSubCopy" v-if="buttonLabelSubCopy"
class="m-0 small" class="m-0 small"
:class="textPosition" :class="textPosition"
> >
{{ buttonLabelSubCopy }} {{ buttonLabelSubCopy }}
</span> </span>
<span <span
v-if="screenReaderOnlyText" v-if="screenReaderOnlyText"
class="sr-only" class="sr-only"
> >
{{ screenReaderOnlyText }} {{ screenReaderOnlyText }}
@ -67,7 +67,7 @@ export default {
loaderColor: String, loaderColor: String,
loaderPosition: String, loaderPosition: String,
sizeInRem: [Number,String], sizeInRem: [Number,String],
value: { value: {
// Field initial value // Field initial value
type: [String, Number], type: [String, Number],
default: "", default: "",
@ -132,21 +132,23 @@ export default {
input[type="checkbox"] { input[type="checkbox"] {
position: static; //override bootstrap position: static; //override bootstrap
height: 0; height: 0;
} opacity: 0;
&:focus-visible + label {
box-shadow: 0 0 0 2.5px $blue inset; &:focus-visible + label {
} box-shadow: 0 0 0 2.5px $blue inset;
&:focus + label { }
box-shadow: 0 0 0 2.5px $blue inset; &:focus + label {
} box-shadow: 0 0 0 2.5px $blue inset;
&:checked + label { }
color: $black; &:checked + label {
font-weight: 500; color: $black;
background: $blue-100; font-weight: 500;
box-shadow: 0 0 0 1px $blue; background: $blue-100;
} box-shadow: 0 0 0 1px $blue;
&:checked + label p:first-child { }
font-weight: 500; &:checked + label p:first-child {
font-weight: 500;
}
} }
} }
label { label {
@ -160,7 +162,7 @@ export default {
&:hover { &:hover {
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
box-shadow: 0 0 0 4px $blue-100; box-shadow: 0 0 0 4px $blue-300;
} }
cursor: pointer; cursor: pointer;
} }

View file

@ -4,45 +4,45 @@
class="list-card w-100 rounded-3 d-flex align-items-center h-100" class="list-card w-100 rounded-3 d-flex align-items-center h-100"
:class="isWide ? 'horizontal' : ''" :class="isWide ? 'horizontal' : ''"
> >
<input <input
:type="isMultiSelect ? 'checkbox' : 'radio'" :type="isMultiSelect ? 'checkbox' : 'radio'"
:id="buttonID" :id="buttonID"
:name="groupName" :name="groupName"
:value="buttonID" :value="buttonID"
:aria-required="isRequired" :aria-required="isRequired"
:data-focus-target="groupName" :data-focus-target="groupName"
@click="handleChange(value)" @click="handleChange(value)"
v-model="checkValue" v-model="checkValue"
@change="handleCheckChange" @change="handleCheckChange"
/>
<label
:for="buttonID"
class="d-flex w-100 align-items-center px-2 h-100"
:class="getLabelClasses" tabindex="-1"
>
<img
:id="buttonImageId"
:class="!isWide ? 'order-1' : 'ms-auto order-3'"
:src="buttonImage"
:alt="altText"
/> />
<label <p
:for="buttonID" v-if="!isWide"
class="d-flex w-100 align-items-center px-2 h-100" class="small order-3"
:class="getLabelClasses" tabindex="-1" :class="isMultiSelect ? 'm-0' : 'mt-2 mb-0'"
> >
<img {{buttonLabel}}
:id="buttonImageId" </p>
:class="!isWide ? 'order-1' : 'ms-auto order-3'" <p v-if="buttonLabelSubCopy && !isWide" class="fs-7 m-0 order-4 sub-copy">
:src="buttonImage" {{buttonLabelSubCopy}}
:alt="altText" </p>
/> <div v-if="isWide" class="order-2">
<p <p class="m-0 small">{{ buttonLabel }}</p>
v-if="!isWide" <p v-if="buttonLabelSubCopy" class="m-0 fs-7 sub-copy">
class="small order-3" {{ buttonLabelSubCopy }}
:class="isMultiSelect ? 'm-0' : 'mt-2 mb-0'"
>
{{buttonLabel}}
</p> </p>
<p v-if="buttonLabelSubCopy && !isWide" class="fs-7 m-0 order-4"> </div>
{{buttonLabelSubCopy}} </label>
</p>
<div v-if="isWide" class="order-2">
<p class="m-0 small">{{ buttonLabel }}</p>
<p v-if="buttonLabelSubCopy" class="m-0 fs-7">
{{ buttonLabelSubCopy }}
</p>
</div>
</label>
</div> </div>
</div> </div>
</template> </template>
@ -137,9 +137,10 @@ export default {
height: auto; height: auto;
width: 6.5rem; width: 6.5rem;
margin-bottom: 3rem; margin-bottom: 3rem;
max-width: 100%;
} }
&:hover { &:hover {
box-shadow: 0px 0px 0px 6px $blue-100; box-shadow: 0px 0px 0px 4px $blue-300;
border: 1px solid transparent; border: 1px solid transparent;
} }
input[type="checkbox"], input[type="checkbox"],
@ -155,6 +156,9 @@ export default {
} }
p { p {
text-align: center; text-align: center;
&.sub-copy {
color: $gray-550;
}
} }
} }
&:focus + label { &:focus + label {
@ -179,6 +183,7 @@ export default {
border-radius: 2px; border-radius: 2px;
order: 2; order: 2;
flex-shrink: 0; flex-shrink: 0;
color: $gray-600;
} }
+ label.checkboxTop::before { + label.checkboxTop::before {
margin: -1.25rem 0.5rem 0 0 !important; margin: -1.25rem 0.5rem 0 0 !important;
@ -235,11 +240,15 @@ export default {
} }
+ label { + label {
min-height: 48px; min-height: 48px;
color: $gray-600;
img { img {
margin-bottom: 0; margin-bottom: 0;
} }
p { p {
text-align: left; text-align: left;
&.sub-copy {
color: $gray-550;
}
} }
} }
} }

View file

@ -3,11 +3,12 @@ import textLink from "./text-link";
import { nextTick } from "vue"; import { nextTick } from "vue";
describe("text-link.vue", () => { describe("text-link.vue", () => {
it("Should return text-link type is navigation if prop navigation is true", async () => {
it("Should return class navigation-link", async () => {
// Act // Act
const wrapper = shallowMount(textLink, { const wrapper = shallowMount(textLink, {
propsData: { propsData: {
navigation: true, linkType: "navigation",
}, },
}); });
@ -18,11 +19,11 @@ describe("text-link.vue", () => {
expect(paragraph.attributes("class")).toContain("navigation-link"); expect(paragraph.attributes("class")).toContain("navigation-link");
}); });
it("Should return text-link type is footer if prop footer is true", async () => { it("Should return class footer", async () => {
// Act // Act
const wrapper = shallowMount(textLink, { const wrapper = shallowMount(textLink, {
propsData: { propsData: {
footer: true, linkType: "footer",
}, },
}); });
@ -30,14 +31,14 @@ describe("text-link.vue", () => {
const paragraph = wrapper.find("a"); const paragraph = wrapper.find("a");
// Expect // Expect
expect(paragraph.attributes("class")).toContain("footer-link"); expect(paragraph.attributes("class")).toContain("footer");
}); });
it("Should return text-link type is textSmall if prop textSmall is true", async () => { it("Should return class text-small", async () => {
// Act // Act
const wrapper = shallowMount(textLink, { const wrapper = shallowMount(textLink, {
propsData: { propsData: {
textSmall: true, linkType: "textSmall",
}, },
}); });
@ -48,18 +49,4 @@ describe("text-link.vue", () => {
expect(paragraph.attributes("class")).toContain("small"); expect(paragraph.attributes("class")).toContain("small");
}); });
it("Should return text for href", async () => {
// Act
const wrapper = shallowMount(textLink, {
propsData: {
text: "This is link text",
},
});
// Assert
const paragraph = wrapper.find("a");
// Expect
expect(paragraph.text()).toEqual("This is link text");
});
}); });

View file

@ -1,17 +1,15 @@
<template> <template>
<a v-if="navigation" @click="handleClick" class="navigation-link" :href="href">{{text}}</a> <a v-if="linkType === 'navigation'" @click="handleClick" class="navigation-link" :href="href">{{text}}</a>
<a v-else-if="footer" @click="handleClick" class="footer-link" :href="href" target="_blank">{{text}}</a> <a v-else-if="linkType === 'footer'" @click="handleClick" class="footer-link" :href="href" target="_blank">{{text}}</a>
<a v-else-if="textSmall" @click="handleClick" class="small" :href="href">{{text}}</a> <a v-else-if="linkType === 'textSmall'" @click="handleClick" class="small" :href="href">{{text}}</a>
<a v-else @click="handleClick" :href="href">{{text}}</a> <a v-else-if="linkType === 'text'" @click="handleClick" :href="href">{{text}}</a>
</template> </template>
<script> <script>
export default { export default {
name: "textLink", name: "textLink",
props: { props: {
navigation: Boolean, linkType: String,
footer: Boolean,
textSmall: Boolean,
text: String, text: String,
href: { href: {
type: String, type: String,