INSR-8489: Update cart dropdown to add tooltip for advanced mobile fee
Also includes a modal when trying to remove the item from the cart.
This commit is contained in:
parent
76ebbf335a
commit
f46e584d6b
8 changed files with 517 additions and 13 deletions
5
src/assets/img/icons/info-circle-blue.svg
Normal file
5
src/assets/img/icons/info-circle-blue.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 0C6.41775 0 4.87104 0.469192 3.55544 1.34824C2.23985 2.22729 1.21447 3.47672 0.608967 4.93853C0.00346626 6.40034 -0.15496 8.00887 0.153721 9.56072C0.462403 11.1126 1.22433 12.538 2.34315 13.6569C3.46197 14.7757 4.88743 15.5376 6.43928 15.8463C7.99113 16.155 9.59966 15.9965 11.0615 15.391C12.5233 14.7855 13.7727 13.7602 14.6518 12.4446C15.5308 11.129 16 9.58225 16 8C16 5.87827 15.1571 3.84344 13.6569 2.34315C12.1566 0.842855 10.1217 0 8 0V0ZM8.4672 11.4C8.4672 11.5485 8.4082 11.691 8.30318 11.796C8.19816 11.901 8.05572 11.96 7.9072 11.96C7.75868 11.96 7.61624 11.901 7.51122 11.796C7.4062 11.691 7.3472 11.5485 7.3472 11.4V7.08C7.3472 6.93148 7.4062 6.78904 7.51122 6.68402C7.61624 6.579 7.75868 6.52 7.9072 6.52C8.05572 6.52 8.19816 6.579 8.30318 6.68402C8.4082 6.78904 8.4672 6.93148 8.4672 7.08V11.4ZM7.9072 5.4C7.79645 5.4 7.68818 5.36716 7.59608 5.30562C7.50399 5.24409 7.43222 5.15663 7.38983 5.0543C7.34745 4.95198 7.33636 4.83938 7.35796 4.73075C7.37957 4.62212 7.43291 4.52234 7.51122 4.44402C7.58954 4.3657 7.68932 4.31237 7.79795 4.29076C7.90658 4.26915 8.01918 4.28024 8.12151 4.32263C8.22383 4.36501 8.31129 4.43679 8.37283 4.52888C8.43436 4.62097 8.4672 4.72924 8.4672 4.84C8.46741 4.9136 8.45307 4.98651 8.42501 5.05455C8.39694 5.12259 8.3557 5.18441 8.30365 5.23645C8.25161 5.28849 8.18979 5.32973 8.12176 5.3578C8.05372 5.38587 7.9808 5.40021 7.9072 5.4Z" fill="#0070D1"/>
|
||||
</svg>
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
exports[`cart-dropdown component initial data rendered as expected 1`] = `
|
||||
Object {
|
||||
"ADVANCED_MOBILE_MODAL_REF_NAME": "advancedMobileModal",
|
||||
"cartItemType": Object {
|
||||
"MOBILE_FEE": "MOBILE FEE",
|
||||
"RECYCLE_FEE": "RECYCLE FEE",
|
||||
|
|
@ -9,6 +10,8 @@ Object {
|
|||
"WARRANTY": "WARRANTY",
|
||||
},
|
||||
"widget": Object {
|
||||
"advancedMobileModal": "AdvancedMobileModal",
|
||||
"advancedMobileTooltip": "AdvancedMobileTooltip",
|
||||
"amountDue": "AmountDueTextWidget",
|
||||
"amountPaid": "AmountPaidTextWidget",
|
||||
"basePrice": "BasePriceWidget",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
<template>
|
||||
<div class="advanced-mobile-modal-container">
|
||||
<modal
|
||||
ref="advancedMobileModal"
|
||||
:onModalClosedCallback="onModalClosed">
|
||||
<div>
|
||||
<div class="header">{{ modalHeaderText }}</div>
|
||||
<div class="subheader">{{ modalHeaderSubText }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="message"
|
||||
v-html="modalBodyText">
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-container">
|
||||
<div>
|
||||
<button type="button" aria-live="polite" aria-label="Confirm Appointment" class="btn btn-danger change-appointment"
|
||||
@click="changeAppointmentClicked">
|
||||
{{ modalCloseButtonText }}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" aria-live="polite" aria-label="See more options" class="btn btn-link cancel"
|
||||
@click="footerButtonClick">
|
||||
{{ modalFooterSubText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import modal from '@/digital-components/modal/modal.vue';
|
||||
|
||||
export default {
|
||||
name: 'advanced-mobile-modal',
|
||||
components: {
|
||||
modal
|
||||
},
|
||||
emits: ['change-appointment-clicked'],
|
||||
props: {
|
||||
cmsWidgetName: {
|
||||
type: String,
|
||||
default: 'SuggestTimeslotModal'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
modalBodyText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, 'BodyText');
|
||||
},
|
||||
modalCloseButtonText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, 'FooterText');
|
||||
},
|
||||
modalFooterSubText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, 'BodyText2');
|
||||
},
|
||||
modalHeaderSubText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, 'SubheaderText');
|
||||
},
|
||||
modalHeaderText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, 'HeaderText');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
this.$refs.advancedMobileModal.openModal();
|
||||
},
|
||||
footerButtonClick() {
|
||||
this.$refs.advancedMobileModal.closeModal();
|
||||
},
|
||||
changeAppointmentClicked() {
|
||||
this.$emit('change-appointment-clicked');
|
||||
this.$refs.advancedMobileModal.closeModal();
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.advanced-mobile-modal-container {
|
||||
.modal {
|
||||
padding-right: 0;
|
||||
}
|
||||
:deep(.modal) {
|
||||
.modal-dialog {
|
||||
@include media-breakpoint-up(xs) {
|
||||
width: auto;
|
||||
max-width: unset;
|
||||
}
|
||||
@include media-breakpoint-up(xl) {
|
||||
width: 37.5rem;
|
||||
max-width: 37.5rem;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 22rem;
|
||||
max-width: 22rem;
|
||||
padding: 1.5rem 0;
|
||||
border-radius: 0.5rem;
|
||||
line-height: 1.5rem;
|
||||
|
||||
.modal-header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-top: 1rem;
|
||||
color: #db0020;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.subheader {
|
||||
color: black;
|
||||
text-align: center;
|
||||
letter-spacing: 0.03em;
|
||||
font-weight: 400;
|
||||
font-size: 1.25rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 1.5rem;
|
||||
color: #525656;
|
||||
letter-spacing: 0.03em;
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
line-height: 1.625rem;
|
||||
|
||||
.emphasize {
|
||||
color: black;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-container {
|
||||
margin-top: 1.5rem;
|
||||
padding: 0 1.5rem;
|
||||
border: none;
|
||||
|
||||
.change-appointment {
|
||||
width: 100%;
|
||||
padding: .75rem 0;
|
||||
border-radius: 4.5rem;
|
||||
letter-spacing: 0.03em;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin-top: 1.5rem;
|
||||
letter-spacing: 0.03em;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
line-height: 1.625rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -33,7 +33,13 @@
|
|||
:key="i"
|
||||
class="cart-item packaged">
|
||||
<div class="price-row">
|
||||
<span>{{ item?.name ?? '' }}</span>
|
||||
<span>
|
||||
{{ item?.name ?? '' }}
|
||||
<toolTip
|
||||
v-if="item.cartItemType === cartItemType.MOBILE_FEE"
|
||||
:cmsWidgetName="widget.advancedMobileTooltip"
|
||||
:customValueMap="advancedMobileTooltipCustomValueMap" />
|
||||
</span>
|
||||
<span>{{ formatAmountInDollars(item?.subTotal ?? 0) }}</span>
|
||||
</div>
|
||||
<textLink
|
||||
|
|
@ -47,6 +53,17 @@
|
|||
<span class="sr-only">{{ item?.name ?? '' }}</span>
|
||||
</template>
|
||||
</textLink>
|
||||
<textLink
|
||||
v-if="item.cartItemType === cartItemType.MOBILE_FEE
|
||||
&& !readOnly"
|
||||
linkType="text"
|
||||
text="Remove"
|
||||
href="javascript:void(0)"
|
||||
@clickEvent="openModal(ADVANCED_MOBILE_MODAL_REF_NAME)">
|
||||
<template #after-text>
|
||||
<span class="sr-only">{{ item?.name ?? '' }}</span>
|
||||
</template>
|
||||
</textLink>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -99,6 +116,10 @@
|
|||
<span class="deductible-label">{{ deductibleLabel }}:</span>
|
||||
<span class="deductible-value">{{ getDisplayed(deductible) }}</span>
|
||||
</div>
|
||||
<advancedMobileModal
|
||||
:ref="ADVANCED_MOBILE_MODAL_REF_NAME"
|
||||
:cmsWidgetName="widget.advancedMobileModal"
|
||||
@changeAppointmentClicked="switchToInShop" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
@ -107,6 +128,8 @@ import contentGroupModal from '@/iss-components/content-group-modal/content-grou
|
|||
import textBlock from '@/digital-components/text-block/text-block.vue';
|
||||
import textLink from '@/ux-components/text-link/text-link.vue';
|
||||
import { formatAmountInDollars } from '@/helpers/text-helper.js';
|
||||
import advancedMobileModal from '@/iss-components/cart-dropdown/advanced-mobile-modal/advanced-mobile-modal.vue';
|
||||
import toolTip from '@/ux-components/tool-tip/tool-tip.vue';
|
||||
|
||||
// Constants
|
||||
import { experimentSettings } from '@/constants/experiments';
|
||||
|
|
@ -126,17 +149,21 @@ import { getPriceOfLineItem, getPriceOfLineItems, getTaxOfLineItems } from '@/he
|
|||
import { processIfStatements } from '@/helpers/cms-content-helper';
|
||||
|
||||
const VERIFYING_COVERAGE = 'Verifying coverage';
|
||||
const ADVANCED_MOBILE_MODAL_REF_NAME = 'advancedMobileModal';
|
||||
|
||||
export default {
|
||||
name: 'confirmation-cart',
|
||||
components: {
|
||||
textLink
|
||||
textLink,
|
||||
advancedMobileModal,
|
||||
toolTip
|
||||
},
|
||||
props: {
|
||||
readOnly: Boolean,
|
||||
showAsPaid: Boolean,
|
||||
submittedOrder: Object
|
||||
},
|
||||
emits: ['switch-to-in-shop'],
|
||||
data() {
|
||||
return {
|
||||
widget: {
|
||||
|
|
@ -153,9 +180,12 @@ export default {
|
|||
vapsItemDescriptions: 'VapsItemDescriptions',
|
||||
warrantyText: 'WarrantyCartItemTextWidget',
|
||||
guaranteeText: 'GuaranteeCartItemTextWidget',
|
||||
total: 'TotalWidget'
|
||||
total: 'TotalWidget',
|
||||
advancedMobileTooltip: 'AdvancedMobileTooltip',
|
||||
advancedMobileModal: 'AdvancedMobileModal'
|
||||
},
|
||||
cartItemType
|
||||
cartItemType,
|
||||
ADVANCED_MOBILE_MODAL_REF_NAME
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -237,9 +267,6 @@ export default {
|
|||
if (this.recycleFeeCartItem && !isRecycleFeeHidden) {
|
||||
items.push(this.recycleFeeCartItem);
|
||||
}
|
||||
if (this.mobileFeeCartItem && !isMobileFeeHidden && !this.mobileFeeCartItem.isInsurable) {
|
||||
items.push(this.mobileFeeCartItem);
|
||||
}
|
||||
const vapPartTypesInOrder = Array.from(new Set(this.lineItems.vaps?.map((vap) => vap.partType) ?? []));
|
||||
const vapsCartItems = vapPartTypesInOrder.map((partType) => {
|
||||
switch (partType) {
|
||||
|
|
@ -254,6 +281,9 @@ export default {
|
|||
}
|
||||
});
|
||||
items.push(...vapsCartItems.filter((item) => item != null));
|
||||
if (this.mobileFeeCartItem && !isMobileFeeHidden && !this.mobileFeeCartItem.isInsurable) {
|
||||
items.push(this.mobileFeeCartItem);
|
||||
}
|
||||
return items;
|
||||
},
|
||||
frontWipersCartItem() {
|
||||
|
|
@ -308,7 +338,7 @@ export default {
|
|||
const mobileFeeLineItem = getMobileFeeLineItem(this.cartOrder);
|
||||
const cartItem = mobileFeeLineItem
|
||||
? this.getCartItem(
|
||||
this.getCmsContent(this.widget.mobileFee, widgetFields.TEXT_BLOCK_WIDGET.TEXT),
|
||||
mobileFeeLineItem.displayName ?? this.getCmsContent(this.widget.mobileFee, widgetFields.TEXT_BLOCK_WIDGET.TEXT),
|
||||
[mobileFeeLineItem],
|
||||
cartItemType.MOBILE_FEE,
|
||||
partTypeStrings.MOBILE_FEE
|
||||
|
|
@ -335,6 +365,11 @@ export default {
|
|||
return this.cartOrder.lineItems.glassParts?.some((glassPart) => {
|
||||
return glassPart.childParts?.some((childPart) => childPart.partType === partTypeStrings.MOLDING);
|
||||
});
|
||||
},
|
||||
advancedMobileTooltipCustomValueMap() {
|
||||
return {
|
||||
feeAmount: this.mobileFeeCartItem ? formatAmountInDollars(this.mobileFeeCartItem.subTotal) : ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -402,6 +437,9 @@ export default {
|
|||
return null;
|
||||
}
|
||||
},
|
||||
switchToInShop() {
|
||||
this.$emit('switch-to-in-shop');
|
||||
},
|
||||
formatAmountInDollars
|
||||
}
|
||||
};
|
||||
|
|
@ -418,7 +456,7 @@ export default {
|
|||
.cart-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: .5rem 0rem;
|
||||
padding: .5rem 1rem;
|
||||
width: 100%;
|
||||
min-width: fit-content;
|
||||
&:nth-child(odd) {
|
||||
|
|
@ -431,12 +469,14 @@ export default {
|
|||
|
||||
.price-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
span, a {
|
||||
white-space: nowrap;
|
||||
padding: 0rem 1rem;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@ const addToCalendarStub = {
|
|||
render: () => {}
|
||||
};
|
||||
|
||||
const cartDropdownStub = {
|
||||
template: '<div></div>'
|
||||
};
|
||||
|
||||
const initialStore = {
|
||||
order: {
|
||||
schedule: {
|
||||
|
|
@ -171,7 +175,8 @@ function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRu
|
|||
mountOptions.global.stubs = {
|
||||
siteFooter: footerStub,
|
||||
siteHeader: headerStub,
|
||||
addToCalendar: addToCalendarStub
|
||||
addToCalendar: addToCalendarStub,
|
||||
cartDropdown: cartDropdownStub
|
||||
};
|
||||
|
||||
const testingPinia = createTestingPinia({
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@
|
|||
:showAsPaid="false"
|
||||
:readOnly="false"
|
||||
recyclingModalCmsWidgetName="RecycleModal"
|
||||
servicePackageTitleWidgetName="ServicePackageTitle" />
|
||||
servicePackageTitleWidgetName="ServicePackageTitle"
|
||||
@switchToInShop="handleSwitchToInShop" />
|
||||
</div>
|
||||
<alert
|
||||
v-if="displayPayInAdvanceAlert"
|
||||
|
|
@ -500,6 +501,14 @@ export default {
|
|||
break;
|
||||
}
|
||||
},
|
||||
handleSwitchToInShop() {
|
||||
this.mainStore.resetServiceLocationAndDependencies();
|
||||
this.mainStore.updateAppointmentType(AppointmentTypeStrings.IN_SHOP);
|
||||
this.$router.navigateWithSpinner(
|
||||
this.navigationScenarios.EDIT_SCHEDULE,
|
||||
this.$route
|
||||
);
|
||||
},
|
||||
openContactDetailsModal() {
|
||||
this.$refs.contactDetailsDrawer.openModal();
|
||||
},
|
||||
|
|
|
|||
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 tooltipHorizontalOffset 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.tooltipHorizontalOffset).toBe(-290);
|
||||
});
|
||||
});
|
||||
144
src/ux-components/tool-tip/tool-tip.vue
Normal file
144
src/ux-components/tool-tip/tool-tip.vue
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<template>
|
||||
<div 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: tooltipHorizontalOffset + 'px', top: '1rem' }">
|
||||
<slot>
|
||||
<h3 v-html="toolTipHeaderText"></h3>
|
||||
<p v-html="toolTipBodyText"></p>
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getStringWithCustomValues } from '@/helpers/cms-content-helper';
|
||||
|
||||
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";
|
||||
},
|
||||
},
|
||||
customValueMap: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isVisible: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener("click", this.handleClickOutside);
|
||||
},
|
||||
unmounted() {
|
||||
document.removeEventListener("click", this.handleClickOutside);
|
||||
},
|
||||
computed: {
|
||||
toolTipHeaderText() {
|
||||
return getStringWithCustomValues(
|
||||
this.getCmsContent(this.cmsWidgetName, "HeaderText") || "",
|
||||
this.customValueMap
|
||||
);
|
||||
},
|
||||
toolTipBodyText() {
|
||||
return getStringWithCustomValues(
|
||||
this.getCmsContent(this.cmsWidgetName, "BodyText") || "",
|
||||
this.customValueMap
|
||||
);
|
||||
},
|
||||
tooltipHorizontalOffset() {
|
||||
// Get the width of the parent container - used to position the tooltip to the left
|
||||
if (this.$refs.container && this.$refs.container.parentElement) {
|
||||
return 10 - 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.5rem;
|
||||
padding: 0rem;
|
||||
|
||||
.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: 18.75rem;
|
||||
background-color: $white;
|
||||
box-shadow: $box-shadow;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: $black;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: .875rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in a new issue