Merge pull request #38 from Safelite/feature/SSR-36
Site footer changes.
This commit is contained in:
commit
fae5ef732d
4 changed files with 416 additions and 0 deletions
42
src/common-components/site-footer/site-footer.spec.js
Normal file
42
src/common-components/site-footer/site-footer.spec.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { mount } from "@vue/test-utils";
|
||||||
|
import siteFooter from "./site-footer";
|
||||||
|
|
||||||
|
describe("site-footer.vue", () => {
|
||||||
|
it("Should emit ForwardClicked on button click", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = mount(siteFooter, {
|
||||||
|
mixins: [mockMixin],
|
||||||
|
});
|
||||||
|
wrapper.vm.buttonClick();
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.emitted()["ForwardClicked"][0]).toHaveBeenCalled;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should emit BackClicked on link click", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = mount(siteFooter, {
|
||||||
|
mixins: [mockMixin],
|
||||||
|
});
|
||||||
|
wrapper.vm.linkClick();
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.emitted()["BackClicked"][0]).toHaveBeenCalled;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should change button text when update button text is called", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = mount(siteFooter, {
|
||||||
|
mixins: [mockMixin],
|
||||||
|
});
|
||||||
|
wrapper.vm.updateButtonText("newText");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.componentVM.customButtontext).toBe("newText");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockMixin = {
|
||||||
|
methods: {
|
||||||
|
getCmsContent: jest.fn(),
|
||||||
|
getFooterInfoBoxHeight: jest.fn(() => 80),
|
||||||
|
},
|
||||||
|
};
|
||||||
132
src/common-components/site-footer/site-footer.vue
Normal file
132
src/common-components/site-footer/site-footer.vue
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
<template>
|
||||||
|
<div class="row" :style="`padding-bottom: ${paddingHeight}px`"></div>
|
||||||
|
<footer class="footer container-fluid fixed-bottom g-5 bg-light py-4" id="infoBox">
|
||||||
|
<div class="row d-flex flex-row-reverse align-items-center vw-100">
|
||||||
|
<div class="col button-col d-flex" id="stacked">
|
||||||
|
<buttonMain
|
||||||
|
ref="buttonMain"
|
||||||
|
isPrimary
|
||||||
|
:buttonText="buttonText"
|
||||||
|
loaderColor="white"
|
||||||
|
:class="isForwardActionDisabled && 'form-test-invalid'"
|
||||||
|
:aria-disabled="isForwardActionDisabled"
|
||||||
|
:isDisabled="isForwardActionDisabled"
|
||||||
|
@click-event="buttonClick"
|
||||||
|
data-bs-target="#footerModal"
|
||||||
|
data-bs-dismiss="modal" />
|
||||||
|
</div>
|
||||||
|
<div v-if="!isBackButtonHidden" class="col-auto link-col py-1 text-break">
|
||||||
|
<textLink
|
||||||
|
linkType="navigation"
|
||||||
|
:text="backLink"
|
||||||
|
@click-event="linkClick"
|
||||||
|
href="javascript:void(0)"
|
||||||
|
data-bs-target="#footerModal"
|
||||||
|
data-bs-dismiss="modal" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import textLink from "@/ux-components/text-link/text-link";
|
||||||
|
import buttonMain from "@/ux-components/button-main/button-main";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "funnelFooter",
|
||||||
|
props: {
|
||||||
|
isForwardActionDisabled: Boolean,
|
||||||
|
isBackButtonHidden: { type: Boolean, default: false },
|
||||||
|
cmsWidgetName: String,
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
textLink,
|
||||||
|
buttonMain,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
paddingHeight: 0,
|
||||||
|
customButtontext: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.paddingHeight = this.getFooterInfoBoxHeight() + 24;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
window.addEventListener("resize", this.onResize);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
window.removeEventListener("resize", this.onResize);
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
document.onkeydown = null;
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
backLink() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "BackButtonText");
|
||||||
|
},
|
||||||
|
buttonText() {
|
||||||
|
return this.customButtontext
|
||||||
|
? this.customButtontext
|
||||||
|
: this.getCmsContent(this.cmsWidgetName, "ForwardButtonText");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onResize() {
|
||||||
|
this.paddingHeight = this.getFooterInfoBoxHeight();
|
||||||
|
},
|
||||||
|
updateButtonText(newText) {
|
||||||
|
this.customButtontext = newText;
|
||||||
|
},
|
||||||
|
removeLoader() {
|
||||||
|
this.$refs.buttonMain.removeLoader();
|
||||||
|
document.onkeydown = function (e) {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
buttonClick() {
|
||||||
|
//prevent keyboard input after button click
|
||||||
|
document.onkeydown = function (e) {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
this.$emit("ForwardClicked");
|
||||||
|
},
|
||||||
|
linkClick() {
|
||||||
|
this.$emit("BackClicked");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.col,
|
||||||
|
.col-auto,
|
||||||
|
.col button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: 340px) {
|
||||||
|
.col,
|
||||||
|
.col-auto,
|
||||||
|
.col button {
|
||||||
|
width: auto;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
& > .container-fluid {
|
||||||
|
overflow-x: visible; // needed to fix hidden footer on some iphones
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
102
src/ux-components/button-main/button-main.spec.js
Normal file
102
src/ux-components/button-main/button-main.spec.js
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import buttonMain from "./button-main";
|
||||||
|
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||||
|
import { nextTick } from "vue";
|
||||||
|
|
||||||
|
describe("buttonMain.vue", () => {
|
||||||
|
it("Should return btn-primary class", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(
|
||||||
|
buttonMain,
|
||||||
|
setupMocks({
|
||||||
|
propsData: {
|
||||||
|
isPrimary: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const button = wrapper.find("button");
|
||||||
|
|
||||||
|
// Expect
|
||||||
|
expect(button.attributes("class")).toContain("btn-primary");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return aria-disabled state", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(
|
||||||
|
buttonMain,
|
||||||
|
setupMocks({
|
||||||
|
propsData: {
|
||||||
|
isDisabled: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const button = wrapper.find("button");
|
||||||
|
|
||||||
|
// Expect
|
||||||
|
expect(button.attributes()["aria-disabled"]).toEqual("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return loader color", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(
|
||||||
|
buttonMain,
|
||||||
|
setupMocks({
|
||||||
|
propsData: {
|
||||||
|
loaderColor: "blue",
|
||||||
|
loaderEnabled: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
const label = wrapper.find("label");
|
||||||
|
|
||||||
|
wrapper.vm.clicked();
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const loader = wrapper.find("loader-stub");
|
||||||
|
|
||||||
|
expect(loader.attributes("class")).toContain("blue");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return loader position", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(
|
||||||
|
buttonMain,
|
||||||
|
setupMocks({
|
||||||
|
propsData: {
|
||||||
|
loaderPosition: "right",
|
||||||
|
loaderEnabled: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
const label = wrapper.find("label");
|
||||||
|
|
||||||
|
wrapper.vm.clicked();
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const loader = wrapper.find("loader-stub");
|
||||||
|
|
||||||
|
expect(loader.attributes("class")).toContain("right");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupMocks(mountOptionsMockData = {}) {
|
||||||
|
const defaultMountOptions = { route: { query: { issPage: "page-name" } } };
|
||||||
|
const baseMountOptions = getMountOptions(
|
||||||
|
Object.assign(defaultMountOptions, mountOptionsMockData)
|
||||||
|
);
|
||||||
|
const allMountOptions = Object.assign(defaultMountOptions, baseMountOptions);
|
||||||
|
|
||||||
|
return allMountOptions;
|
||||||
|
}
|
||||||
140
src/ux-components/button-main/button-main.vue
Normal file
140
src/ux-components/button-main/button-main.vue
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
:aria-disabled="isDisabled"
|
||||||
|
class="btn d-flex align-items-center py-3 px-4 delay"
|
||||||
|
:class="[
|
||||||
|
isPrimary ? 'btn-primary' : 'btn-secondary',
|
||||||
|
isFloat ? 'float-end' : '',
|
||||||
|
isLoaderDisplayed ? 'has-loader' : '',
|
||||||
|
]"
|
||||||
|
@click="clicked">
|
||||||
|
<span class="m-0">{{ this.buttonText }}</span>
|
||||||
|
<loader
|
||||||
|
class="ms-2"
|
||||||
|
v-if="isLoaderDisplayed"
|
||||||
|
v-bind:class="[this.loaderColor, this.loaderPosition]" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import loader from "@/ux-components/loader/loader";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "buttonMain",
|
||||||
|
props: {
|
||||||
|
isPrimary: Boolean,
|
||||||
|
buttonText: String,
|
||||||
|
isDisabled: Boolean,
|
||||||
|
loaderColor: String,
|
||||||
|
loaderPosition: String,
|
||||||
|
isFloat: Boolean,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoaderDisplayed: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
removeLoader() {
|
||||||
|
this.isLoaderDisplayed = false;
|
||||||
|
},
|
||||||
|
clicked() {
|
||||||
|
if (!this.isDisabled) {
|
||||||
|
this.isLoaderDisplayed = true;
|
||||||
|
this.$emit("click-event");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
loader,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.btn {
|
||||||
|
&.btn-primary {
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(270deg, $blue 0%, $blue-800 100%);
|
||||||
|
border: none;
|
||||||
|
border-radius: $border-radius-lg;
|
||||||
|
color: $white;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 500;
|
||||||
|
@media (hover: hover) {
|
||||||
|
background: linear-gradient(270deg, $blue 0%, $blue-800 100%);
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
|
||||||
|
}
|
||||||
|
&:focus, // Mouse, touch, stylus focus
|
||||||
|
&:focus-visible {
|
||||||
|
// Keyboard focus for accessibility
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
|
||||||
|
color: $white;
|
||||||
|
background: linear-gradient(270deg, rgba(6, 87, 124, 1) 0%, rgba(6, 87, 124, 1) 100%);
|
||||||
|
}
|
||||||
|
&:disabled {
|
||||||
|
background: $gray-200 !important;
|
||||||
|
background: linear-gradient(270deg, $gray-200 0%, $gray-200 100%) !important;
|
||||||
|
color: $gray-600 !important;
|
||||||
|
font-weight: 400;
|
||||||
|
height: 48px;
|
||||||
|
border: none;
|
||||||
|
border-radius: $border-radius-lg;
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
&.has-loader {
|
||||||
|
color: $white;
|
||||||
|
background: $blue-700;
|
||||||
|
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
|
||||||
|
}
|
||||||
|
&.delay {
|
||||||
|
// fixes flicker while transitioning between states
|
||||||
|
transition: background 0s 0s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.btn-secondary {
|
||||||
|
position: relative;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid $blue;
|
||||||
|
border-radius: $border-radius-lg;
|
||||||
|
color: $blue;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 150ms linear;
|
||||||
|
height: 3rem;
|
||||||
|
&:hover {
|
||||||
|
color: $white;
|
||||||
|
@include blue-gradient;
|
||||||
|
}
|
||||||
|
&:focus, // Mouse, touch, stylus focus
|
||||||
|
&:focus-visible {
|
||||||
|
// Keyboard focus for accessibility
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $blue-700;
|
||||||
|
color: $white;
|
||||||
|
@include blue-gradient;
|
||||||
|
}
|
||||||
|
&:disabled {
|
||||||
|
background: transparent;
|
||||||
|
color: $gray-550 !important;
|
||||||
|
font-weight: 400;
|
||||||
|
height: 48px;
|
||||||
|
border: 1px solid $gray-550;
|
||||||
|
border-radius: $border-radius-lg;
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
&.has-loader {
|
||||||
|
color: $white;
|
||||||
|
@include blue-gradient;
|
||||||
|
}
|
||||||
|
&.delay {
|
||||||
|
// fixes flicker while transitioning between states
|
||||||
|
transition: background 0s 0s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in a new issue