Merge pull request #3135 from Safelite/feature/CASH-2514
CASH-2514: Create tooltip component for MSR and Recycle fee
This commit is contained in:
commit
6992a5b0b9
3 changed files with 273 additions and 21 deletions
|
|
@ -87,20 +87,15 @@
|
|||
@click-event="removeItem(cartItem.cartItemType, cartItem.category)" />
|
||||
<span v-else-if="cartItem == recycleFeeCartItem">
|
||||
{{ recycleFeeCartItem.name }}
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
@click="openModal(recyclingModalCmsWidgetName)">
|
||||
<img :src="infoIcon" alt="Info Icon" class="info-icon" />
|
||||
</a>
|
||||
<toolTip
|
||||
:cmsWidgetName="recyclingModalCmsWidgetName"
|
||||
:customBodyText="recyclingToolTipBodyText" />
|
||||
</span>
|
||||
<span v-if="cartItem == msrFeeCartItem">
|
||||
{{ msrFeeCartItem.name }}
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
@click="openModal(msrModalCmsWidgetName)"
|
||||
id="msr-fee-cartItem">
|
||||
<img :src="infoIcon" alt="Info Icon" class="info-icon" />
|
||||
</a>
|
||||
<toolTip
|
||||
:cmsWidgetName="msrModalCmsWidgetName"
|
||||
:customBodyText="msrToolTipBodyText" />
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
|
|
@ -163,13 +158,6 @@
|
|||
applied
|
||||
</span>
|
||||
</div>
|
||||
<contentGroupModal ref="RecycleModal" cmsWidgetName="RecycleModal">
|
||||
<textBlock cmsWidgetName="RecycleTextBlock" />
|
||||
</contentGroupModal>
|
||||
<contentGroupModal
|
||||
ref="MSRModal"
|
||||
cmsWidgetName="MSRModal"
|
||||
:customBodyText="msrModalTextBlock"></contentGroupModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -177,9 +165,9 @@
|
|||
// Components
|
||||
import textLink from "@/ux-components/text-link/text-link";
|
||||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
|
||||
import promoModalQuestion from "@/fmg-components/promo-modal-question/promo-modal-question";
|
||||
import removeMsrFeeModal from "./remove-msr-fee-modal/remove-msr-fee-modal.vue";
|
||||
import toolTip from "@/ux-components/tool-tip/tool-tip.vue";
|
||||
|
||||
// Mixins
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
|
|
@ -1015,7 +1003,10 @@ export default {
|
|||
msrFeeLineItem.laborAmount + msrFeeLineItem.sellingPrice + msrFeeLineItem.kitPrice
|
||||
);
|
||||
},
|
||||
msrModalTextBlock() {
|
||||
recyclingToolTipBodyText() {
|
||||
return this.getCmsContent("RecycleTextBlock", "Text");
|
||||
},
|
||||
msrToolTipBodyText() {
|
||||
let cmsContentText = this.getCmsContent("MSRModal", "BodyText");
|
||||
if (cmsContentText) {
|
||||
cmsContentText = cmsContentText.replaceAll(
|
||||
|
|
@ -1287,9 +1278,9 @@ export default {
|
|||
components: {
|
||||
textBlock,
|
||||
textLink,
|
||||
contentGroupModal,
|
||||
promoModalQuestion,
|
||||
removeMsrFeeModal,
|
||||
toolTip,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
121
src/ux-components/tool-tip/tool-tip.spec.js
Normal file
121
src/ux-components/tool-tip/tool-tip.spec.js
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { shallowMount, mount } from "@vue/test-utils";
|
||||
import toolTip from "@/ux-components/tool-tip/tool-tip.vue";
|
||||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn(() => "Mock CMS Content"),
|
||||
},
|
||||
};
|
||||
|
||||
function setupMocks(mountOptionsMockData = {}) {
|
||||
const defaultMountOptions = {
|
||||
propsData: {
|
||||
cmsWidgetName: "testWidget",
|
||||
},
|
||||
mixins: [mockMixin],
|
||||
};
|
||||
return { ...defaultMountOptions, ...mountOptionsMockData };
|
||||
}
|
||||
|
||||
describe("toolTip.vue", () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should not show tooltip initially", () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(toolTip, setupMocks());
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.isVisible).toBe(false);
|
||||
expect(wrapper.find(".tooltip").exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("should show tooltip when icon is clicked", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(toolTip, setupMocks());
|
||||
const icon = wrapper.find(".tooltip-icon");
|
||||
|
||||
// Act
|
||||
await icon.trigger("click");
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.isVisible).toBe(true);
|
||||
expect(wrapper.find(".tooltip").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("should hide tooltip when clicked outside", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(toolTip, setupMocks());
|
||||
const icon = wrapper.find(".tooltip-icon");
|
||||
|
||||
// Show tooltip first
|
||||
await icon.trigger("click");
|
||||
expect(wrapper.vm.isVisible).toBe(true);
|
||||
|
||||
// Act - simulate click outside
|
||||
const clickEvent = new Event("click");
|
||||
Object.defineProperty(clickEvent, "target", { value: document.body });
|
||||
document.dispatchEvent(clickEvent);
|
||||
|
||||
// Wait for next tick to allow event handling
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.isVisible).toBe(false);
|
||||
expect(wrapper.find(".tooltip").exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("should toggle tooltip visibility when icon is clicked multiple times", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(toolTip, setupMocks());
|
||||
const icon = wrapper.find(".tooltip-icon");
|
||||
|
||||
// Act & Assert - First click shows
|
||||
await icon.trigger("click");
|
||||
expect(wrapper.vm.isVisible).toBe(true);
|
||||
|
||||
// Second click hides
|
||||
await icon.trigger("click");
|
||||
expect(wrapper.vm.isVisible).toBe(false);
|
||||
|
||||
// Third click shows again
|
||||
await icon.trigger("click");
|
||||
expect(wrapper.vm.isVisible).toBe(true);
|
||||
});
|
||||
|
||||
it("should not hide tooltip when clicking inside the tooltip container", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(toolTip, setupMocks());
|
||||
const icon = wrapper.find(".tooltip-icon");
|
||||
|
||||
// Show tooltip first
|
||||
await icon.trigger("click");
|
||||
expect(wrapper.vm.isVisible).toBe(true);
|
||||
|
||||
// Act - simulate click inside container
|
||||
const container = wrapper.find(".tooltip-container").element;
|
||||
const clickEvent = new Event("click");
|
||||
Object.defineProperty(clickEvent, "target", { value: container });
|
||||
document.dispatchEvent(clickEvent);
|
||||
|
||||
// Wait for next tick
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// Assert - tooltip should still be visible
|
||||
expect(wrapper.vm.isVisible).toBe(true);
|
||||
});
|
||||
|
||||
it("should calculate lineItemContainerLength based on parent element width", () => {
|
||||
// Arrange
|
||||
const wrapper = mount(toolTip, setupMocks());
|
||||
const parentDiv = document.createElement("div");
|
||||
Object.defineProperty(parentDiv, "offsetWidth", { value: 300, writable: true });
|
||||
|
||||
const container = wrapper.vm.$refs.container;
|
||||
Object.defineProperty(container, "parentElement", { value: parentDiv, writable: true });
|
||||
|
||||
// Act & Assert
|
||||
expect(wrapper.vm.lineItemContainerLength).toBe(-300);
|
||||
});
|
||||
});
|
||||
140
src/ux-components/tool-tip/tool-tip.vue
Normal file
140
src/ux-components/tool-tip/tool-tip.vue
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
<template>
|
||||
<span class="tooltip-container" ref="container">
|
||||
<span @click="toggleTooltip" class="tooltip-icon">
|
||||
<slot name="icon">
|
||||
<img src="@/assets/img/icons/info-circle-blue.svg" />
|
||||
</slot>
|
||||
</span>
|
||||
<span
|
||||
v-if="isVisible"
|
||||
class="tooltip"
|
||||
:class="{ 'tooltip-visible': isVisible }"
|
||||
:style="{ left: lineItemContainerLength + 'px' }">
|
||||
<slot>
|
||||
<h3 v-html="toolTipHeaderText"></h3>
|
||||
<p v-html="toolTipBodyText"></p>
|
||||
</slot>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "toolTip",
|
||||
props: {
|
||||
cmsWidgetName: {
|
||||
type: String,
|
||||
default(rawProps) {
|
||||
if (!rawProps.cmsWidgetName) {
|
||||
console.log("Error: Missing a CMS Widget Name (required field)");
|
||||
}
|
||||
return "widgetUndefined";
|
||||
},
|
||||
},
|
||||
customHeaderText: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
customBodyText: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isVisible: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener("click", this.handleClickOutside);
|
||||
},
|
||||
unmounted() {
|
||||
document.removeEventListener("click", this.handleClickOutside);
|
||||
},
|
||||
computed: {
|
||||
toolTipHeaderText() {
|
||||
return (
|
||||
this.customHeaderText || this.getCmsContent(this.cmsWidgetName, "HeaderText") || ""
|
||||
);
|
||||
},
|
||||
toolTipBodyText() {
|
||||
return this.customBodyText || this.getCmsContent(this.cmsWidgetName, "BodyText") || "";
|
||||
},
|
||||
lineItemContainerLength() {
|
||||
// Get the width of the parent container - used to position the tooltip to the left
|
||||
if (this.$refs.container && this.$refs.container.parentElement) {
|
||||
return 0 - this.$refs.container.parentElement.offsetWidth;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showTooltip() {
|
||||
this.isVisible = true;
|
||||
},
|
||||
hideTooltip() {
|
||||
this.isVisible = false;
|
||||
},
|
||||
toggleTooltip() {
|
||||
this.isVisible = !this.isVisible;
|
||||
},
|
||||
handleClickOutside(event) {
|
||||
if (this.$refs.container && !this.$refs.container.contains(event.target)) {
|
||||
this.hideTooltip();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tooltip-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-left: 0.2rem;
|
||||
|
||||
.tooltip-icon {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
|
||||
img {
|
||||
width: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 1rem;
|
||||
padding: 1rem;
|
||||
border-radius: $border-radius;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition:
|
||||
opacity 0.2s,
|
||||
visibility 0.2s;
|
||||
z-index: 500;
|
||||
width: 16.5rem;
|
||||
background-color: $white;
|
||||
box-shadow: $box-shadow;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: $font-weight-600;
|
||||
color: $black;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in a new issue