Merge pull request #3288 from Safelite/feature/CASH-2951
Unify pattern for stopping user interaction during loading.
This commit is contained in:
commit
fd760ce8f0
21 changed files with 227 additions and 279 deletions
|
|
@ -114,11 +114,9 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<loader
|
<div v-if="loadingStatus !== 'none'" class="date-picker-loader">
|
||||||
class="date-picker-loader"
|
<loader loaderColor="blue" loaderPosition="center" />
|
||||||
:class="[loadingStatus !== 'none' ? 'show-loader' : '']"
|
</div>
|
||||||
loaderColor="blue"
|
|
||||||
loaderPosition="center" />
|
|
||||||
<div class="row form-test-error" id="date-of-month-error">
|
<div class="row form-test-error" id="date-of-month-error">
|
||||||
<ErrorMessage :name="customComponentId" class="small mt-1"></ErrorMessage>
|
<ErrorMessage :name="customComponentId" class="small mt-1"></ErrorMessage>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -742,15 +740,6 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.date-picker-loader {
|
|
||||||
opacity: 0;
|
|
||||||
max-height: 0;
|
|
||||||
|
|
||||||
&.show-loader {
|
|
||||||
opacity: 1;
|
|
||||||
max-height: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.date-picker {
|
.date-picker {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
|
@ -769,13 +758,20 @@ export default {
|
||||||
font-family: UrbanistSemiBold;
|
font-family: UrbanistSemiBold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.loader {
|
.date-picker-loader {
|
||||||
height: 2rem;
|
display: flex;
|
||||||
width: calc(100% - 1.5rem);
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
&::after {
|
:deep(.loader) {
|
||||||
width: 1.5rem;
|
height: 2rem;
|
||||||
height: 1.5rem;
|
width: calc(100% - 1.5rem);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.month {
|
.month {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
:id="modalId + '-modalbtn'"
|
:id="modalId + '-modalbtn'"
|
||||||
ref="modalButtonMain"
|
ref="modalButtonMain"
|
||||||
loaderColor="white"
|
loaderColor="white"
|
||||||
|
loaderPosition="center"
|
||||||
:isDisabled="footerButtonDisabled"
|
:isDisabled="footerButtonDisabled"
|
||||||
:buttonText="footerButtonText"
|
:buttonText="footerButtonText"
|
||||||
@click-event="validateAndEmit"
|
@click-event="validateAndEmit"
|
||||||
|
|
|
||||||
|
|
@ -36,48 +36,6 @@ describe("modal-button-main.vue", () => {
|
||||||
expect(button.attributes()["aria-disabled"]).toEqual("true");
|
expect(button.attributes()["aria-disabled"]).toEqual("true");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return loader color", async () => {
|
|
||||||
// Arrange
|
|
||||||
const wrapper = shallowMount(
|
|
||||||
modalButtonMain,
|
|
||||||
setupMocks({
|
|
||||||
propsData: {
|
|
||||||
loaderColor: "blue",
|
|
||||||
loaderEnabled: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
wrapper.vm.clicked();
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const loader = wrapper.find("loader-stub");
|
|
||||||
expect(loader.attributes("class")).toContain("blue");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return loader position", async () => {
|
|
||||||
// Arrange
|
|
||||||
const wrapper = shallowMount(
|
|
||||||
modalButtonMain,
|
|
||||||
setupMocks({
|
|
||||||
propsData: {
|
|
||||||
loaderPosition: "right",
|
|
||||||
loaderEnabled: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
wrapper.vm.clicked();
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const loader = wrapper.find("loader-stub");
|
|
||||||
expect(loader.attributes("class")).toContain("right");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should set 'isLoaderDisplayed' to false when calling 'removeLoader'", async () => {
|
it("Should set 'isLoaderDisplayed' to false when calling 'removeLoader'", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const wrapper = shallowMount(
|
const wrapper = shallowMount(
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@
|
||||||
@click="clicked">
|
@click="clicked">
|
||||||
<span class="m-0">{{ this.buttonText }}</span>
|
<span class="m-0">{{ this.buttonText }}</span>
|
||||||
<loader
|
<loader
|
||||||
class="ms-2"
|
|
||||||
v-if="isLoaderDisplayed && !suppressLoader"
|
v-if="isLoaderDisplayed && !suppressLoader"
|
||||||
v-bind:class="[this.loaderColor, this.loaderPosition]" />
|
:loaderColor="loaderColor"
|
||||||
|
:loaderPosition="loaderPosition" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,10 +65,9 @@
|
||||||
data-test="image-upload"
|
data-test="image-upload"
|
||||||
@change="imageChanged" />
|
@change="imageChanged" />
|
||||||
</label>
|
</label>
|
||||||
<loader
|
<span v-if="isImageProcessing" class="loading-icon">
|
||||||
v-show="isImageProcessing"
|
<loader loaderColor="blue" />
|
||||||
loaderColor="blue"
|
</span>
|
||||||
class="loading-icon"></loader>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@
|
||||||
ref="buttonMain"
|
ref="buttonMain"
|
||||||
:buttonText="DonationAddButtonCopy"
|
:buttonText="DonationAddButtonCopy"
|
||||||
loaderColor="blue"
|
loaderColor="blue"
|
||||||
|
loaderPosition="center"
|
||||||
class="w-100 mb-2 custom-secondary"
|
class="w-100 mb-2 custom-secondary"
|
||||||
@click-event="handleDonationAction" />
|
@click-event="handleDonationAction" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
isPrimary
|
isPrimary
|
||||||
:buttonText="overrideButtonText || buttonText"
|
:buttonText="overrideButtonText || buttonText"
|
||||||
loaderColor="white"
|
loaderColor="white"
|
||||||
|
loaderPosition="center"
|
||||||
:class="isForwardActionDisabled && 'form-test-invalid'"
|
:class="isForwardActionDisabled && 'form-test-invalid'"
|
||||||
:aria-disabled="isForwardActionDisabled"
|
:aria-disabled="isForwardActionDisabled"
|
||||||
:isDisabled="isForwardActionDisabled"
|
:isDisabled="isForwardActionDisabled"
|
||||||
|
|
@ -68,10 +69,6 @@ export default {
|
||||||
customButtontext: "",
|
customButtontext: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
unmounted() {
|
|
||||||
document.onkeydown = null;
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
backLink() {
|
backLink() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -91,15 +88,8 @@ export default {
|
||||||
},
|
},
|
||||||
removeLoader() {
|
removeLoader() {
|
||||||
this.$refs.buttonMain.removeLoader();
|
this.$refs.buttonMain.removeLoader();
|
||||||
document.onkeydown = function (e) {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
buttonClick() {
|
buttonClick() {
|
||||||
//prevent keyboard input after button click
|
|
||||||
document.onkeydown = function (e) {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
// check session expired and initSession to recreate cookies
|
// check session expired and initSession to recreate cookies
|
||||||
if (analyticsMixin.methods.sessionExpired()) {
|
if (analyticsMixin.methods.sessionExpired()) {
|
||||||
this.routeReturnUser();
|
this.routeReturnUser();
|
||||||
|
|
|
||||||
|
|
@ -33,21 +33,15 @@ describe("nav-bar.vue", () => {
|
||||||
expect(wrapper.componentVM.customButtontext).toBe("newText");
|
expect(wrapper.componentVM.customButtontext).toBe("newText");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should run removeLoader fn on buttonMain and return false for onkeydown fn", async () => {
|
test("should run removeLoader on buttonMain", async () => {
|
||||||
// Arrange
|
|
||||||
const wrapper = mount(navbar, {
|
const wrapper = mount(navbar, {
|
||||||
mixins: [mockMixin],
|
mixins: [mockMixin],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
|
||||||
wrapper.vm.$refs.buttonMain.removeLoader = jest.fn();
|
wrapper.vm.$refs.buttonMain.removeLoader = jest.fn();
|
||||||
wrapper.vm.removeLoader();
|
wrapper.vm.removeLoader();
|
||||||
const spy = jest.spyOn(document, "onkeydown");
|
|
||||||
document.onkeydown();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(wrapper.vm.$refs.buttonMain.removeLoader).toHaveBeenCalled();
|
expect(wrapper.vm.$refs.buttonMain.removeLoader).toHaveBeenCalled();
|
||||||
expect(spy).toReturnWith(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
isPrimary
|
isPrimary
|
||||||
:buttonText="overrideButtonText || buttonText"
|
:buttonText="overrideButtonText || buttonText"
|
||||||
loaderColor="white"
|
loaderColor="white"
|
||||||
|
loaderPosition="center"
|
||||||
:class="isForwardActionDisabled && 'form-test-invalid'"
|
:class="isForwardActionDisabled && 'form-test-invalid'"
|
||||||
:aria-disabled="isForwardActionDisabled"
|
:aria-disabled="isForwardActionDisabled"
|
||||||
:isDisabled="isForwardActionDisabled"
|
:isDisabled="isForwardActionDisabled"
|
||||||
|
|
@ -69,9 +70,6 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
unmounted() {
|
|
||||||
document.onkeydown = null;
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
backLink() {
|
backLink() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -91,15 +89,8 @@ export default {
|
||||||
},
|
},
|
||||||
removeLoader() {
|
removeLoader() {
|
||||||
this.$refs.buttonMain.removeLoader();
|
this.$refs.buttonMain.removeLoader();
|
||||||
document.onkeydown = function (e) {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
buttonClick() {
|
buttonClick() {
|
||||||
//prevent keyboard input after button click
|
|
||||||
document.onkeydown = function (e) {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
// check session expired and initSession to recreate cookies
|
// check session expired and initSession to recreate cookies
|
||||||
if (analyticsMixin.methods.sessionExpired()) {
|
if (analyticsMixin.methods.sessionExpired()) {
|
||||||
this.routeReturnUser();
|
this.routeReturnUser();
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,66 @@ describe("save-progress-popup-question ", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("footer button suppression", () => {
|
||||||
|
test("should suppress the modal footer button on the phone tab", async () => {
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
props: {
|
||||||
|
modalWidgetName: "SaveProgressPopupWidget",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.vm.selectContactMethod("PhoneAnswer");
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
expect(wrapper.vm.isFooterButtonSuppressed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should show the modal footer button on the email tab before save", () => {
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
props: {
|
||||||
|
modalWidgetName: "SaveProgressPopupWidget",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.vm.isFooterButtonSuppressed).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should suppress the modal footer button after a successful email save", async () => {
|
||||||
|
const dispatchStoreAction = jest.fn().mockResolvedValue(undefined);
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
props: {
|
||||||
|
modalWidgetName: "SaveProgressPopupWidget",
|
||||||
|
pageName: "quote",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.vm.dispatchStoreAction = dispatchStoreAction;
|
||||||
|
wrapper.vm.userInput = "test@example.com";
|
||||||
|
|
||||||
|
await wrapper.vm.saveProgress();
|
||||||
|
|
||||||
|
expect(wrapper.vm.isFooterButtonSuppressed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should reset the modal footer button loader before suppressing it after save", async () => {
|
||||||
|
const dispatchStoreAction = jest.fn().mockResolvedValue(undefined);
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
props: {
|
||||||
|
modalWidgetName: "SaveProgressPopupWidget",
|
||||||
|
pageName: "quote",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.vm.dispatchStoreAction = dispatchStoreAction;
|
||||||
|
wrapper.vm.userInput = "test@example.com";
|
||||||
|
wrapper.vm.modal.resetButtonStyle = jest.fn();
|
||||||
|
|
||||||
|
await wrapper.vm.saveProgress();
|
||||||
|
|
||||||
|
expect(wrapper.vm.modal.resetButtonStyle).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("saveProgress", () => {
|
describe("saveProgress", () => {
|
||||||
test("should save phone number and sms consent when phone tab is selected", async () => {
|
test("should save phone number and sms consent when phone tab is selected", async () => {
|
||||||
const dispatchStoreAction = jest.fn().mockResolvedValue(undefined);
|
const dispatchStoreAction = jest.fn().mockResolvedValue(undefined);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
:onModalClosedCallback="onModalClosed"
|
:onModalClosedCallback="onModalClosed"
|
||||||
:footerButtonText="modalButtonText"
|
:footerButtonText="modalButtonText"
|
||||||
:isFooterButtonPrimary="true"
|
:isFooterButtonPrimary="true"
|
||||||
:isFooterButtonSuppressed="isPhoneTabSelected && !isProgressSaved"
|
:isFooterButtonSuppressed="isFooterButtonSuppressed"
|
||||||
@footer-button-event="saveProgress">
|
@footer-button-event="saveProgress">
|
||||||
<p class="modal-body-inner">{{ modalBodyText }}</p>
|
<p class="modal-body-inner">{{ modalBodyText }}</p>
|
||||||
<fieldset class="save-progress-popup-question__tabs">
|
<fieldset class="save-progress-popup-question__tabs">
|
||||||
|
|
@ -71,9 +71,14 @@
|
||||||
ref="phoneSendButton"
|
ref="phoneSendButton"
|
||||||
:isPrimary="true"
|
:isPrimary="true"
|
||||||
class="w-100 modal-footer-button"
|
class="w-100 modal-footer-button"
|
||||||
|
loaderColor="white"
|
||||||
|
loaderPosition="center"
|
||||||
:buttonText="modalButtonText"
|
:buttonText="modalButtonText"
|
||||||
@click-event="validatePhoneAndSave" />
|
@click-event="validatePhoneAndSave" />
|
||||||
<p class="modal-disclaimer" v-html="modalDisclaimerText"></p>
|
<p
|
||||||
|
v-if="!isProgressSaved"
|
||||||
|
class="modal-disclaimer"
|
||||||
|
v-html="modalDisclaimerText"></p>
|
||||||
</template>
|
</template>
|
||||||
<alert
|
<alert
|
||||||
class="my-4"
|
class="my-4"
|
||||||
|
|
@ -193,6 +198,9 @@ export default {
|
||||||
isPhoneTabSelected() {
|
isPhoneTabSelected() {
|
||||||
return this.contactMethod === saveProgressPopupContactMethods.PHONE;
|
return this.contactMethod === saveProgressPopupContactMethods.PHONE;
|
||||||
},
|
},
|
||||||
|
isFooterButtonSuppressed() {
|
||||||
|
return this.isProgressSaved || this.isPhoneTabSelected;
|
||||||
|
},
|
||||||
phoneQuestionWidgetName() {
|
phoneQuestionWidgetName() {
|
||||||
return PHONE_QUESTION_WIDGET;
|
return PHONE_QUESTION_WIDGET;
|
||||||
},
|
},
|
||||||
|
|
@ -272,6 +280,9 @@ export default {
|
||||||
// send store call to send to new API (that triggers an email/SMS send)
|
// send store call to send to new API (that triggers an email/SMS send)
|
||||||
await saveQuote({ pageNameToLog: this.pageName });
|
await saveQuote({ pageNameToLog: this.pageName });
|
||||||
|
|
||||||
|
this.modal?.resetButtonStyle();
|
||||||
|
this.resetPhoneSendButtonStyle();
|
||||||
|
|
||||||
// hide save progress button; show success message alert
|
// hide save progress button; show success message alert
|
||||||
this.savedContactMethod = this.contactMethod;
|
this.savedContactMethod = this.contactMethod;
|
||||||
this.isProgressSaved = true;
|
this.isProgressSaved = true;
|
||||||
|
|
@ -528,10 +539,6 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.progress-saved {
|
&.progress-saved {
|
||||||
.modal-footer-button,
|
|
||||||
.modal-disclaimer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.skip-button {
|
.skip-button {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
</span>
|
</span>
|
||||||
<loader
|
<loader
|
||||||
v-if="isLoaderDisplayed"
|
v-if="isLoaderDisplayed"
|
||||||
:loaderPosition="left"
|
loaderPosition="left"
|
||||||
:allowPageInteraction="true" />
|
:allowPageInteraction="true" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,10 @@
|
||||||
:isRequired="false"
|
:isRequired="false"
|
||||||
:validationRules="''" />
|
:validationRules="''" />
|
||||||
</form>
|
</form>
|
||||||
<div v-show="isLoading" class="loader-wrapper">
|
<div v-if="isLoading" class="loader-wrapper">
|
||||||
<loader loaderColor="blue" loaderPosition="center" />
|
<loader loaderColor="blue" loaderPosition="center" />
|
||||||
</div>
|
</div>
|
||||||
<div v-show="!isLoading">
|
<div v-if="!isLoading">
|
||||||
<alert
|
<alert
|
||||||
ref="alertInvalidZip"
|
ref="alertInvalidZip"
|
||||||
v-if="displayInvalidZipAlert"
|
v-if="displayInvalidZipAlert"
|
||||||
|
|
|
||||||
|
|
@ -36,48 +36,6 @@ describe("buttonMain.vue", () => {
|
||||||
expect(button.attributes()["aria-disabled"]).toEqual("true");
|
expect(button.attributes()["aria-disabled"]).toEqual("true");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return loader color", async () => {
|
|
||||||
// Arrange
|
|
||||||
const wrapper = shallowMount(
|
|
||||||
buttonMain,
|
|
||||||
setupMocks({
|
|
||||||
propsData: {
|
|
||||||
loaderColor: "blue",
|
|
||||||
loaderEnabled: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
wrapper.vm.clicked();
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const loader = wrapper.find("loader-stub");
|
|
||||||
expect(loader.attributes("class")).toContain("blue");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return loader position", async () => {
|
|
||||||
// Arrange
|
|
||||||
const wrapper = shallowMount(
|
|
||||||
buttonMain,
|
|
||||||
setupMocks({
|
|
||||||
propsData: {
|
|
||||||
loaderPosition: "right",
|
|
||||||
loaderEnabled: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
wrapper.vm.clicked();
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const loader = wrapper.find("loader-stub");
|
|
||||||
expect(loader.attributes("class")).toContain("right");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should set 'isLoaderDisplayed' to false when calling 'removeLoader'", async () => {
|
it("Should set 'isLoaderDisplayed' to false when calling 'removeLoader'", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const wrapper = shallowMount(
|
const wrapper = shallowMount(
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
<loader
|
<loader
|
||||||
class="ms-2"
|
class="ms-2"
|
||||||
v-if="isLoaderDisplayed && !suppressLoader"
|
v-if="isLoaderDisplayed && !suppressLoader"
|
||||||
v-bind:class="[this.loaderColor, this.loaderPosition]" />
|
:loaderColor="loaderColor"
|
||||||
|
:loaderPosition="loaderPosition" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -64,6 +65,7 @@ export default {
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.btn {
|
.btn {
|
||||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||||
|
|
||||||
&.btn-primary {
|
&.btn-primary {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: $red;
|
background: $red;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { shallowMount } from "@vue/test-utils";
|
import { mount, shallowMount } from "@vue/test-utils";
|
||||||
import interceptOverlay from "./intercept-overlay";
|
import interceptOverlay from "./intercept-overlay";
|
||||||
|
|
||||||
describe("intercept-overlay.vue", () => {
|
describe("intercept-overlay.vue", () => {
|
||||||
|
|
@ -43,4 +43,30 @@ describe("intercept-overlay.vue", () => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("click interception", () => {
|
||||||
|
test("captureClick prevents default and stops propagation", () => {
|
||||||
|
const wrapper = shallowMount(interceptOverlay);
|
||||||
|
const event = { preventDefault: jest.fn(), stopPropagation: jest.fn() };
|
||||||
|
wrapper.vm.captureClick(event);
|
||||||
|
expect(event.preventDefault).toHaveBeenCalled();
|
||||||
|
expect(event.stopPropagation).toHaveBeenCalled();
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clicking the overlay does not propagate to document listeners", async () => {
|
||||||
|
const documentClickFn = jest.fn();
|
||||||
|
document.addEventListener("click", documentClickFn);
|
||||||
|
|
||||||
|
const wrapper = mount(interceptOverlay, { attachTo: document.body });
|
||||||
|
|
||||||
|
const overlay = wrapper.find(".intercept-overlay");
|
||||||
|
await overlay.trigger("click");
|
||||||
|
|
||||||
|
expect(documentClickFn).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
document.removeEventListener("click", documentClickFn);
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="intercept-overlay" aria-hidden="true" @keydown.capture="blockKey"></div>
|
<div
|
||||||
|
class="intercept-overlay"
|
||||||
|
aria-hidden="true"
|
||||||
|
@keydown.capture="blockKey"
|
||||||
|
@click="captureClick"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -16,6 +20,10 @@ export default {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
},
|
},
|
||||||
|
captureClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -30,60 +30,6 @@ describe("list-button.vue", () => {
|
||||||
const loader = wrapper.findComponent({ name: "loader" });
|
const loader = wrapper.findComponent({ name: "loader" });
|
||||||
expect(loader.exists()).toBe(true);
|
expect(loader.exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should return loader color", async () => {
|
|
||||||
// Arrange
|
|
||||||
const { wrapper } = setupMocks({
|
|
||||||
mockData: {
|
|
||||||
global: {
|
|
||||||
mocks: {
|
|
||||||
$route: { query: { fmgPage: "page-name" } },
|
|
||||||
GaActions: GaActions,
|
|
||||||
pushEventToGA: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
propsData: {
|
|
||||||
loaderColor: "blue",
|
|
||||||
selectingInitiatesLoad: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act
|
|
||||||
wrapper.vm.selectedValue = "something";
|
|
||||||
await wrapper.vm.$nextTick();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const loader = wrapper.findComponent({ name: "loader" });
|
|
||||||
expect(loader.attributes("class")).toContain("blue");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return loader position", async () => {
|
|
||||||
// Act
|
|
||||||
const { wrapper } = setupMocks({
|
|
||||||
mockData: {
|
|
||||||
global: {
|
|
||||||
mocks: {
|
|
||||||
$route: { query: { fmgPage: "page-name" } },
|
|
||||||
GaActions: GaActions,
|
|
||||||
pushEventToGA: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
propsData: {
|
|
||||||
loaderPosition: "right",
|
|
||||||
selectingInitiatesLoad: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act
|
|
||||||
wrapper.vm.selectedValue = "something";
|
|
||||||
await wrapper.vm.$nextTick();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const loader = wrapper.findComponent({ name: "loader" });
|
|
||||||
expect(loader.attributes("class")).toContain("right");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("baseInputButton checks", () => {
|
describe("baseInputButton checks", () => {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@
|
||||||
</span>
|
</span>
|
||||||
<loader
|
<loader
|
||||||
v-if="isLoaderDisplayed && selectingInitiatesLoad"
|
v-if="isLoaderDisplayed && selectingInitiatesLoad"
|
||||||
:class="[this.loaderColor, this.loaderPosition]" />
|
:loaderColor="loaderColor"
|
||||||
|
:loaderPosition="loaderPosition" />
|
||||||
</div>
|
</div>
|
||||||
</baseInputButton>
|
</baseInputButton>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,16 @@ import loader from "./loader";
|
||||||
|
|
||||||
describe("loader.vue", () => {
|
describe("loader.vue", () => {
|
||||||
test("if it rendered the HTML element with class", () => {
|
test("if it rendered the HTML element with class", () => {
|
||||||
// Arrange/Act
|
|
||||||
const wrapper = shallowMount(loader, {
|
const wrapper = shallowMount(loader, {
|
||||||
props: {},
|
props: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(wrapper.find("div").exists()).toBeTruthy();
|
expect(wrapper.find("div").exists()).toBeTruthy();
|
||||||
expect(wrapper.find(".loader").exists()).toBeTruthy();
|
expect(wrapper.find(".loader").exists()).toBeTruthy();
|
||||||
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("if it correctly passed props", () => {
|
test("applies loaderColor and loaderPosition as CSS classes on the loader element", () => {
|
||||||
// Arrange/Act
|
|
||||||
const wrapper = shallowMount(loader, {
|
const wrapper = shallowMount(loader, {
|
||||||
props: {
|
props: {
|
||||||
loaderColor: "blue",
|
loaderColor: "blue",
|
||||||
|
|
@ -22,64 +20,95 @@ describe("loader.vue", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
const loaderElement = wrapper.find(".loader");
|
||||||
expect(wrapper.props()).toMatchObject({
|
expect(loaderElement.classes()).toContain("blue");
|
||||||
loaderColor: "blue",
|
expect(loaderElement.classes()).toContain("left");
|
||||||
loaderPosition: "left",
|
wrapper.unmount();
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Blocking interaction on page", () => {
|
describe("Blocking interaction on page", () => {
|
||||||
test("Does capture clicks if enabled (default)", async () => {
|
let parent;
|
||||||
// Arrange
|
|
||||||
const div = document.createElement("div");
|
|
||||||
div.id = "parent";
|
|
||||||
document.body.appendChild(div);
|
|
||||||
|
|
||||||
const parentClickFn = jest.fn();
|
beforeEach(() => {
|
||||||
|
parent = document.createElement("div");
|
||||||
div.addEventListener("click", parentClickFn);
|
parent.id = "loader-test-parent";
|
||||||
|
document.body.appendChild(parent);
|
||||||
const wrapper = shallowMount(loader, {
|
|
||||||
props: {},
|
|
||||||
attachTo: "#parent",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await wrapper.trigger("click");
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(parentClickFn).not.toBeCalled();
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
document.body.removeChild(div);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Does not capture clicks if disabled", async () => {
|
afterEach(() => {
|
||||||
// Arrange
|
document.body.querySelectorAll(".intercept-overlay").forEach((el) => el.remove());
|
||||||
const div = document.createElement("div");
|
if (parent?.parentNode) {
|
||||||
div.id = "parent";
|
parent.parentNode.removeChild(parent);
|
||||||
document.body.appendChild(div);
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const parentClickFn = jest.fn();
|
test("teleports intercept overlay to document.body when blocking is enabled", () => {
|
||||||
|
const wrapper = mount(loader, {
|
||||||
|
props: {},
|
||||||
|
attachTo: "#loader-test-parent",
|
||||||
|
});
|
||||||
|
|
||||||
div.addEventListener("click", parentClickFn);
|
const overlayInBody = document.body.querySelector(".intercept-overlay");
|
||||||
|
const overlayInParent = parent.querySelector(".intercept-overlay");
|
||||||
|
|
||||||
const wrapper = shallowMount(loader, {
|
expect(overlayInBody).toBeTruthy();
|
||||||
|
expect(overlayInParent).toBeNull();
|
||||||
|
expect(wrapper.find(".intercept-overlay").exists()).toBe(false);
|
||||||
|
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not render intercept overlay when allowPageInteraction is true", () => {
|
||||||
|
const wrapper = mount(loader, {
|
||||||
props: {
|
props: {
|
||||||
allowPageInteraction: true,
|
allowPageInteraction: true,
|
||||||
},
|
},
|
||||||
attachTo: "#parent",
|
attachTo: "#loader-test-parent",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
expect(document.body.querySelector(".intercept-overlay")).toBeNull();
|
||||||
await wrapper.trigger("click");
|
expect(wrapper.find(".loader").exists()).toBe(true);
|
||||||
|
|
||||||
// Assert
|
wrapper.unmount();
|
||||||
expect(parentClickFn).toBeCalled();
|
});
|
||||||
|
|
||||||
// Cleanup
|
test("clicking the teleported overlay does not propagate to document listeners", async () => {
|
||||||
document.body.removeChild(div);
|
const documentClickFn = jest.fn();
|
||||||
|
document.addEventListener("click", documentClickFn);
|
||||||
|
|
||||||
|
const wrapper = mount(loader, {
|
||||||
|
props: {},
|
||||||
|
attachTo: "#loader-test-parent",
|
||||||
|
});
|
||||||
|
|
||||||
|
const overlay = document.body.querySelector(".intercept-overlay");
|
||||||
|
expect(overlay).toBeTruthy();
|
||||||
|
|
||||||
|
overlay.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
||||||
|
|
||||||
|
expect(documentClickFn).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
document.removeEventListener("click", documentClickFn);
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("teleports overlay to body even when loader is mounted inside a button", async () => {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
parent.appendChild(button);
|
||||||
|
|
||||||
|
const wrapper = mount(loader, {
|
||||||
|
props: {},
|
||||||
|
attachTo: button,
|
||||||
|
});
|
||||||
|
|
||||||
|
const overlayInBody = document.body.querySelector(".intercept-overlay");
|
||||||
|
const overlayInButton = button.querySelector(".intercept-overlay");
|
||||||
|
|
||||||
|
expect(overlayInBody).toBeTruthy();
|
||||||
|
expect(overlayInButton).toBeNull();
|
||||||
|
|
||||||
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<interceptOverlay v-if="!allowPageInteraction" />
|
||||||
|
</Teleport>
|
||||||
<div
|
<div
|
||||||
class="loader"
|
class="loader"
|
||||||
role="alert"
|
role="alert"
|
||||||
aria-label="Loading new page"
|
aria-label="Loading new page"
|
||||||
@click="captureClick"
|
v-bind:class="[this.loaderColor, this.loaderPosition]"></div>
|
||||||
v-bind:class="[
|
|
||||||
this.loaderColor,
|
|
||||||
this.loaderPosition,
|
|
||||||
this.allowPageInteraction ? 'allow-ui-interaction' : '',
|
|
||||||
]"></div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import interceptOverlay from "../intercept-overlay/intercept-overlay.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "loader",
|
name: "loader",
|
||||||
/* Specify size in number value which translates to rem value. For example, 1.5 = 1.5rem = 24px */
|
/* Specify size in number value which translates to rem value. For example, 1.5 = 1.5rem = 24px */
|
||||||
|
|
@ -31,12 +31,9 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {},
|
||||||
captureClick(event) {
|
components: {
|
||||||
if (!this.allowPageInteraction) {
|
interceptOverlay,
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -46,18 +43,6 @@ export default {
|
||||||
display: flex;
|
display: flex;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
|
||||||
//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
|
//Spinner basics
|
||||||
&:after {
|
&:after {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -101,9 +86,5 @@ export default {
|
||||||
&.black:after {
|
&.black:after {
|
||||||
background-color: $black;
|
background-color: $black;
|
||||||
}
|
}
|
||||||
&.allow-ui-interaction::before {
|
|
||||||
//no-block to enable clicking on certain buttons with loaders while the loader is actives
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue