Merge branch 'develop' into feature/Digital/SSR-117
This commit is contained in:
commit
51ef43177c
6 changed files with 559 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>
|
||||
78
src/ux-components/checkbox/checkbox.spec.js
Normal file
78
src/ux-components/checkbox/checkbox.spec.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import checkbox from "./checkbox";
|
||||
import { nextTick } from "vue";
|
||||
|
||||
describe("checkbox.vue", () => {
|
||||
it("Should return checkbox name", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(checkbox, {
|
||||
propsData: {
|
||||
checkboxName: "Checkbox",
|
||||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
const input = wrapper.find("input");
|
||||
|
||||
// Expect
|
||||
expect(input.attributes().name).toEqual("Checkbox");
|
||||
});
|
||||
|
||||
it("Should return checkbox id", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(checkbox, {
|
||||
propsData: {
|
||||
buttonID: "Checkbox ID",
|
||||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
const input = wrapper.find("input");
|
||||
|
||||
// Expect
|
||||
expect(input.attributes().id).toEqual("Checkbox ID");
|
||||
});
|
||||
|
||||
it("Should return tabindex value", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(checkbox, {
|
||||
propsData: {
|
||||
tabIndex: "1",
|
||||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
const input = wrapper.find("input");
|
||||
|
||||
// Expect
|
||||
expect(input.attributes().tabindex).toEqual("1");
|
||||
});
|
||||
|
||||
it("Should return label text", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(checkbox, {
|
||||
propsData: {
|
||||
checkboxLabel: "label text",
|
||||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
const paragraph = wrapper.find("p");
|
||||
|
||||
expect(paragraph.text()).toEqual("label text");
|
||||
});
|
||||
|
||||
it("Should return label text", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(checkbox, {
|
||||
propsData: {
|
||||
screenReaderOnlyText: "screenreader text",
|
||||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
const paragraph = wrapper.find("span");
|
||||
|
||||
expect(paragraph.text()).toEqual("screenreader text");
|
||||
});
|
||||
});
|
||||
65
src/ux-components/checkbox/checkbox.vue
Normal file
65
src/ux-components/checkbox/checkbox.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<!-- Checkbox groups MUST be wrapped in a <fieldset> and <legend> tag -->
|
||||
<div class="form-check ui-checkbox" :class="[hasError ? 'has-error' : '']">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
aria-checked="false"
|
||||
:name="checkboxName"
|
||||
:id="buttonID"
|
||||
:tabindex="tabIndex"
|
||||
:aria-required="isRequired" />
|
||||
<label class="d-flex align-items-start" :for="buttonID">
|
||||
<p v-if="checkboxLabel" class="m-0">{{ checkboxLabel }}</p>
|
||||
<span v-if="screenReaderOnlyText" class="sr-only">{{ screenReaderOnlyText }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "checkbox",
|
||||
props: {
|
||||
checkboxName: String,
|
||||
buttonID: String,
|
||||
tabIndex: Number,
|
||||
checkboxLabel: String,
|
||||
screenReaderOnlyText: String,
|
||||
isRequired: Boolean,
|
||||
hasError: Boolean,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-check {
|
||||
.form-check-input {
|
||||
border: 1px solid $gray-500;
|
||||
border-radius: 2px;
|
||||
&:checked {
|
||||
background-size: 125%;
|
||||
border: 1px solid $blue;
|
||||
+ label {
|
||||
p {
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.form-check-input {
|
||||
box-shadow: 0 0 0 4px $blue-300;
|
||||
}
|
||||
}
|
||||
p {
|
||||
font-weight: 400;
|
||||
font-size: 0.875rem;
|
||||
color: $gray-600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in a new issue