Merge pull request #3135 from Safelite/feature/CASH-2514

CASH-2514: Create tooltip component for MSR and Recycle fee
This commit is contained in:
Chris 2026-04-07 11:11:29 -04:00 committed by GitHub
commit 6992a5b0b9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 273 additions and 21 deletions

View file

@ -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>

View 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);
});
});

View 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>