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>
|
||||
</div>
|
||||
</div>
|
||||
<loader
|
||||
class="date-picker-loader"
|
||||
:class="[loadingStatus !== 'none' ? 'show-loader' : '']"
|
||||
loaderColor="blue"
|
||||
loaderPosition="center" />
|
||||
<div v-if="loadingStatus !== 'none'" class="date-picker-loader">
|
||||
<loader loaderColor="blue" loaderPosition="center" />
|
||||
</div>
|
||||
<div class="row form-test-error" id="date-of-month-error">
|
||||
<ErrorMessage :name="customComponentId" class="small mt-1"></ErrorMessage>
|
||||
</div>
|
||||
|
|
@ -742,15 +740,6 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.date-picker-loader {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
|
||||
&.show-loader {
|
||||
opacity: 1;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
.date-picker {
|
||||
position: relative;
|
||||
flex-grow: 0;
|
||||
|
|
@ -769,13 +758,20 @@ export default {
|
|||
font-family: UrbanistSemiBold;
|
||||
}
|
||||
}
|
||||
.loader {
|
||||
height: 2rem;
|
||||
width: calc(100% - 1.5rem);
|
||||
.date-picker-loader {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
&::after {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
:deep(.loader) {
|
||||
height: 2rem;
|
||||
width: calc(100% - 1.5rem);
|
||||
|
||||
&::after {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.month {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
:id="modalId + '-modalbtn'"
|
||||
ref="modalButtonMain"
|
||||
loaderColor="white"
|
||||
loaderPosition="center"
|
||||
:isDisabled="footerButtonDisabled"
|
||||
:buttonText="footerButtonText"
|
||||
@click-event="validateAndEmit"
|
||||
|
|
|
|||
|
|
@ -36,48 +36,6 @@ describe("modal-button-main.vue", () => {
|
|||
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 () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@
|
|||
@click="clicked">
|
||||
<span class="m-0">{{ this.buttonText }}</span>
|
||||
<loader
|
||||
class="ms-2"
|
||||
v-if="isLoaderDisplayed && !suppressLoader"
|
||||
v-bind:class="[this.loaderColor, this.loaderPosition]" />
|
||||
:loaderColor="loaderColor"
|
||||
:loaderPosition="loaderPosition" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -65,10 +65,9 @@
|
|||
data-test="image-upload"
|
||||
@change="imageChanged" />
|
||||
</label>
|
||||
<loader
|
||||
v-show="isImageProcessing"
|
||||
loaderColor="blue"
|
||||
class="loading-icon"></loader>
|
||||
<span v-if="isImageProcessing" class="loading-icon">
|
||||
<loader loaderColor="blue" />
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@
|
|||
ref="buttonMain"
|
||||
:buttonText="DonationAddButtonCopy"
|
||||
loaderColor="blue"
|
||||
loaderPosition="center"
|
||||
class="w-100 mb-2 custom-secondary"
|
||||
@click-event="handleDonationAction" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
isPrimary
|
||||
:buttonText="overrideButtonText || buttonText"
|
||||
loaderColor="white"
|
||||
loaderPosition="center"
|
||||
:class="isForwardActionDisabled && 'form-test-invalid'"
|
||||
:aria-disabled="isForwardActionDisabled"
|
||||
:isDisabled="isForwardActionDisabled"
|
||||
|
|
@ -68,10 +69,6 @@ export default {
|
|||
customButtontext: "",
|
||||
};
|
||||
},
|
||||
|
||||
unmounted() {
|
||||
document.onkeydown = null;
|
||||
},
|
||||
computed: {
|
||||
backLink() {
|
||||
return (
|
||||
|
|
@ -91,15 +88,8 @@ export default {
|
|||
},
|
||||
removeLoader() {
|
||||
this.$refs.buttonMain.removeLoader();
|
||||
document.onkeydown = function (e) {
|
||||
return true;
|
||||
};
|
||||
},
|
||||
buttonClick() {
|
||||
//prevent keyboard input after button click
|
||||
document.onkeydown = function (e) {
|
||||
return false;
|
||||
};
|
||||
// check session expired and initSession to recreate cookies
|
||||
if (analyticsMixin.methods.sessionExpired()) {
|
||||
this.routeReturnUser();
|
||||
|
|
|
|||
|
|
@ -33,21 +33,15 @@ describe("nav-bar.vue", () => {
|
|||
expect(wrapper.componentVM.customButtontext).toBe("newText");
|
||||
});
|
||||
|
||||
test("should run removeLoader fn on buttonMain and return false for onkeydown fn", async () => {
|
||||
// Arrange
|
||||
test("should run removeLoader on buttonMain", async () => {
|
||||
const wrapper = mount(navbar, {
|
||||
mixins: [mockMixin],
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.$refs.buttonMain.removeLoader = jest.fn();
|
||||
wrapper.vm.removeLoader();
|
||||
const spy = jest.spyOn(document, "onkeydown");
|
||||
document.onkeydown();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.$refs.buttonMain.removeLoader).toHaveBeenCalled();
|
||||
expect(spy).toReturnWith(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
isPrimary
|
||||
:buttonText="overrideButtonText || buttonText"
|
||||
loaderColor="white"
|
||||
loaderPosition="center"
|
||||
:class="isForwardActionDisabled && 'form-test-invalid'"
|
||||
:aria-disabled="isForwardActionDisabled"
|
||||
:isDisabled="isForwardActionDisabled"
|
||||
|
|
@ -69,9 +70,6 @@ export default {
|
|||
};
|
||||
},
|
||||
|
||||
unmounted() {
|
||||
document.onkeydown = null;
|
||||
},
|
||||
computed: {
|
||||
backLink() {
|
||||
return (
|
||||
|
|
@ -91,15 +89,8 @@ export default {
|
|||
},
|
||||
removeLoader() {
|
||||
this.$refs.buttonMain.removeLoader();
|
||||
document.onkeydown = function (e) {
|
||||
return true;
|
||||
};
|
||||
},
|
||||
buttonClick() {
|
||||
//prevent keyboard input after button click
|
||||
document.onkeydown = function (e) {
|
||||
return false;
|
||||
};
|
||||
// check session expired and initSession to recreate cookies
|
||||
if (analyticsMixin.methods.sessionExpired()) {
|
||||
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", () => {
|
||||
test("should save phone number and sms consent when phone tab is selected", async () => {
|
||||
const dispatchStoreAction = jest.fn().mockResolvedValue(undefined);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
:onModalClosedCallback="onModalClosed"
|
||||
:footerButtonText="modalButtonText"
|
||||
:isFooterButtonPrimary="true"
|
||||
:isFooterButtonSuppressed="isPhoneTabSelected && !isProgressSaved"
|
||||
:isFooterButtonSuppressed="isFooterButtonSuppressed"
|
||||
@footer-button-event="saveProgress">
|
||||
<p class="modal-body-inner">{{ modalBodyText }}</p>
|
||||
<fieldset class="save-progress-popup-question__tabs">
|
||||
|
|
@ -71,9 +71,14 @@
|
|||
ref="phoneSendButton"
|
||||
:isPrimary="true"
|
||||
class="w-100 modal-footer-button"
|
||||
loaderColor="white"
|
||||
loaderPosition="center"
|
||||
:buttonText="modalButtonText"
|
||||
@click-event="validatePhoneAndSave" />
|
||||
<p class="modal-disclaimer" v-html="modalDisclaimerText"></p>
|
||||
<p
|
||||
v-if="!isProgressSaved"
|
||||
class="modal-disclaimer"
|
||||
v-html="modalDisclaimerText"></p>
|
||||
</template>
|
||||
<alert
|
||||
class="my-4"
|
||||
|
|
@ -193,6 +198,9 @@ export default {
|
|||
isPhoneTabSelected() {
|
||||
return this.contactMethod === saveProgressPopupContactMethods.PHONE;
|
||||
},
|
||||
isFooterButtonSuppressed() {
|
||||
return this.isProgressSaved || this.isPhoneTabSelected;
|
||||
},
|
||||
phoneQuestionWidgetName() {
|
||||
return PHONE_QUESTION_WIDGET;
|
||||
},
|
||||
|
|
@ -272,6 +280,9 @@ export default {
|
|||
// send store call to send to new API (that triggers an email/SMS send)
|
||||
await saveQuote({ pageNameToLog: this.pageName });
|
||||
|
||||
this.modal?.resetButtonStyle();
|
||||
this.resetPhoneSendButtonStyle();
|
||||
|
||||
// hide save progress button; show success message alert
|
||||
this.savedContactMethod = this.contactMethod;
|
||||
this.isProgressSaved = true;
|
||||
|
|
@ -528,10 +539,6 @@ export default {
|
|||
}
|
||||
|
||||
&.progress-saved {
|
||||
.modal-footer-button,
|
||||
.modal-disclaimer {
|
||||
display: none;
|
||||
}
|
||||
.skip-button {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
</span>
|
||||
<loader
|
||||
v-if="isLoaderDisplayed"
|
||||
:loaderPosition="left"
|
||||
loaderPosition="left"
|
||||
:allowPageInteraction="true" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@
|
|||
:isRequired="false"
|
||||
:validationRules="''" />
|
||||
</form>
|
||||
<div v-show="isLoading" class="loader-wrapper">
|
||||
<div v-if="isLoading" class="loader-wrapper">
|
||||
<loader loaderColor="blue" loaderPosition="center" />
|
||||
</div>
|
||||
<div v-show="!isLoading">
|
||||
<div v-if="!isLoading">
|
||||
<alert
|
||||
ref="alertInvalidZip"
|
||||
v-if="displayInvalidZipAlert"
|
||||
|
|
|
|||
|
|
@ -36,48 +36,6 @@ describe("buttonMain.vue", () => {
|
|||
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 () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@
|
|||
<loader
|
||||
class="ms-2"
|
||||
v-if="isLoaderDisplayed && !suppressLoader"
|
||||
v-bind:class="[this.loaderColor, this.loaderPosition]" />
|
||||
:loaderColor="loaderColor"
|
||||
:loaderPosition="loaderPosition" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
|
@ -64,6 +65,7 @@ export default {
|
|||
<style lang="scss">
|
||||
.btn {
|
||||
font-family: UrbanistSemibold, Arial, Helvetica, sans-serif;
|
||||
|
||||
&.btn-primary {
|
||||
position: relative;
|
||||
background: $red;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import { mount, shallowMount } from "@vue/test-utils";
|
||||
import interceptOverlay from "./intercept-overlay";
|
||||
|
||||
describe("intercept-overlay.vue", () => {
|
||||
|
|
@ -43,4 +43,30 @@ describe("intercept-overlay.vue", () => {
|
|||
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>
|
||||
<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>
|
||||
|
||||
<script>
|
||||
|
|
@ -16,6 +20,10 @@ export default {
|
|||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
},
|
||||
captureClick(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -30,60 +30,6 @@ describe("list-button.vue", () => {
|
|||
const loader = wrapper.findComponent({ name: "loader" });
|
||||
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", () => {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@
|
|||
</span>
|
||||
<loader
|
||||
v-if="isLoaderDisplayed && selectingInitiatesLoad"
|
||||
:class="[this.loaderColor, this.loaderPosition]" />
|
||||
:loaderColor="loaderColor"
|
||||
:loaderPosition="loaderPosition" />
|
||||
</div>
|
||||
</baseInputButton>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -3,18 +3,16 @@ import loader from "./loader";
|
|||
|
||||
describe("loader.vue", () => {
|
||||
test("if it rendered the HTML element with class", () => {
|
||||
// Arrange/Act
|
||||
const wrapper = shallowMount(loader, {
|
||||
props: {},
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(wrapper.find("div").exists()).toBeTruthy();
|
||||
expect(wrapper.find(".loader").exists()).toBeTruthy();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test("if it correctly passed props", () => {
|
||||
// Arrange/Act
|
||||
test("applies loaderColor and loaderPosition as CSS classes on the loader element", () => {
|
||||
const wrapper = shallowMount(loader, {
|
||||
props: {
|
||||
loaderColor: "blue",
|
||||
|
|
@ -22,64 +20,95 @@ describe("loader.vue", () => {
|
|||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(wrapper.props()).toMatchObject({
|
||||
loaderColor: "blue",
|
||||
loaderPosition: "left",
|
||||
});
|
||||
const loaderElement = wrapper.find(".loader");
|
||||
expect(loaderElement.classes()).toContain("blue");
|
||||
expect(loaderElement.classes()).toContain("left");
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
describe("Blocking interaction on page", () => {
|
||||
test("Does capture clicks if enabled (default)", async () => {
|
||||
// Arrange
|
||||
const div = document.createElement("div");
|
||||
div.id = "parent";
|
||||
document.body.appendChild(div);
|
||||
let parent;
|
||||
|
||||
const parentClickFn = jest.fn();
|
||||
|
||||
div.addEventListener("click", parentClickFn);
|
||||
|
||||
const wrapper = shallowMount(loader, {
|
||||
props: {},
|
||||
attachTo: "#parent",
|
||||
});
|
||||
|
||||
// Act
|
||||
await wrapper.trigger("click");
|
||||
|
||||
// Assert
|
||||
expect(parentClickFn).not.toBeCalled();
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(div);
|
||||
beforeEach(() => {
|
||||
parent = document.createElement("div");
|
||||
parent.id = "loader-test-parent";
|
||||
document.body.appendChild(parent);
|
||||
});
|
||||
|
||||
test("Does not capture clicks if disabled", async () => {
|
||||
// Arrange
|
||||
const div = document.createElement("div");
|
||||
div.id = "parent";
|
||||
document.body.appendChild(div);
|
||||
afterEach(() => {
|
||||
document.body.querySelectorAll(".intercept-overlay").forEach((el) => el.remove());
|
||||
if (parent?.parentNode) {
|
||||
parent.parentNode.removeChild(parent);
|
||||
}
|
||||
});
|
||||
|
||||
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: {
|
||||
allowPageInteraction: true,
|
||||
},
|
||||
attachTo: "#parent",
|
||||
attachTo: "#loader-test-parent",
|
||||
});
|
||||
|
||||
// Act
|
||||
await wrapper.trigger("click");
|
||||
expect(document.body.querySelector(".intercept-overlay")).toBeNull();
|
||||
expect(wrapper.find(".loader").exists()).toBe(true);
|
||||
|
||||
// Assert
|
||||
expect(parentClickFn).toBeCalled();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(div);
|
||||
test("clicking the teleported overlay does not propagate to document listeners", async () => {
|
||||
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>
|
||||
<Teleport to="body">
|
||||
<interceptOverlay v-if="!allowPageInteraction" />
|
||||
</Teleport>
|
||||
<div
|
||||
class="loader"
|
||||
role="alert"
|
||||
aria-label="Loading new page"
|
||||
@click="captureClick"
|
||||
v-bind:class="[
|
||||
this.loaderColor,
|
||||
this.loaderPosition,
|
||||
this.allowPageInteraction ? 'allow-ui-interaction' : '',
|
||||
]"></div>
|
||||
v-bind:class="[this.loaderColor, this.loaderPosition]"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import interceptOverlay from "../intercept-overlay/intercept-overlay.vue";
|
||||
|
||||
export default {
|
||||
name: "loader",
|
||||
/* 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,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
captureClick(event) {
|
||||
if (!this.allowPageInteraction) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
components: {
|
||||
interceptOverlay,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -46,18 +43,6 @@ export default {
|
|||
display: flex;
|
||||
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
|
||||
&:after {
|
||||
content: "";
|
||||
|
|
@ -101,9 +86,5 @@ export default {
|
|||
&.black:after {
|
||||
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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue