Visual tweaks + combine selection between components

This commit is contained in:
Chloe Herd 2026-06-11 13:56:12 -04:00
parent 85b0b65ca2
commit 725b504d67
6 changed files with 107 additions and 61 deletions

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00161 3.1177C7.76595 3.11605 7.53923 3.2078 7.37109 3.37286L0.264342 10.4688C0.0950866 10.638 -1.78339e-09 10.8674 0 11.1067C1.78339e-09 11.3459 0.0950866 11.5754 0.264342 11.7446C0.433597 11.9137 0.663157 12.0088 0.90252 12.0088C1.14188 12.0088 1.37144 11.9137 1.5407 11.7446L8.00161 5.27378L14.4702 11.7446C14.6394 11.9121 14.8683 12.0055 15.1065 12.0043C15.3447 12.0031 15.5726 11.9074 15.7402 11.7382C15.9077 11.569 16.0012 11.3402 16 11.1022C15.9988 10.8641 15.903 10.6363 15.7338 10.4688L8.63468 3.37796C8.46644 3.21088 8.23878 3.11729 8.00161 3.1177Z" fill="#0070D1"/>
</svg>

After

Width:  |  Height:  |  Size: 691 B

View file

@ -1,6 +1,6 @@
import { shallowMount } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
import inshopSchedulingCard from "./inshop-scheduling-card"; import inshopSchedulingCard from "./inshop-scheduling-card";
import { RouteCodeFlags } from "@/constants/schedule-constants"; import { RouteCodeFlags, AppointmentTypeStrings } from "@/constants/schedule-constants";
import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { getMountOptions } from "@/helpers/unit-test-helper.js";
const ALL_DAY_DROPOFF_ROUTE_CODE = `03357-001820-I*21346*${RouteCodeFlags.ALL_DAY_DROP_OFF}`; const ALL_DAY_DROPOFF_ROUTE_CODE = `03357-001820-I*21346*${RouteCodeFlags.ALL_DAY_DROP_OFF}`;
@ -142,11 +142,10 @@ describe("inshop-scheduling-card.vue", () => {
await dropOffInput.setValue(true); await dropOffInput.setValue(true);
await wrapper.setProps({ await wrapper.setProps({
modelValue: { modelValue: {
selectedInshopTimeSlot: { appointmentType: AppointmentTypeStrings.IN_SHOP,
providerNumber: "001820", providerNumber: "001820",
routeCodeId: ALL_DAY_DROPOFF_ROUTE_CODE, routeCodeId: ALL_DAY_DROPOFF_ROUTE_CODE,
}, },
},
}); });
expect(wrapper.text()).toContain("Park in any open spot"); expect(wrapper.text()).toContain("Park in any open spot");
@ -158,10 +157,9 @@ describe("inshop-scheduling-card.vue", () => {
const dropOffInput = wrapper.find(`input[value="${ALL_DAY_DROPOFF_ROUTE_CODE}"]`); const dropOffInput = wrapper.find(`input[value="${ALL_DAY_DROPOFF_ROUTE_CODE}"]`);
await dropOffInput.setValue(true); await dropOffInput.setValue(true);
expect(wrapper.emitted("update:modelValue")[0][0]).toEqual({ expect(wrapper.emitted("update:modelValue")[0][0]).toEqual({
selectedInshopTimeSlot: { appointmentType: AppointmentTypeStrings.IN_SHOP,
providerNumber: "001820", providerNumber: "001820",
routeCodeId: ALL_DAY_DROPOFF_ROUTE_CODE, routeCodeId: ALL_DAY_DROPOFF_ROUTE_CODE,
},
}); });
wrapper.unmount(); wrapper.unmount();
}); });
@ -202,10 +200,9 @@ describe("inshop-scheduling-card.vue", () => {
const morningInput = wrapper.find(`input[value="${MORNING_INSHOP_ROUTE_CODE}"]`); const morningInput = wrapper.find(`input[value="${MORNING_INSHOP_ROUTE_CODE}"]`);
await morningInput.setValue(true); await morningInput.setValue(true);
expect(wrapper.emitted("update:modelValue")[0][0]).toEqual({ expect(wrapper.emitted("update:modelValue")[0][0]).toEqual({
selectedInshopTimeSlot: { appointmentType: AppointmentTypeStrings.IN_SHOP,
providerNumber: "001820", providerNumber: "001820",
routeCodeId: MORNING_INSHOP_ROUTE_CODE, routeCodeId: MORNING_INSHOP_ROUTE_CODE,
},
}); });
wrapper.unmount(); wrapper.unmount();
}); });
@ -213,11 +210,10 @@ describe("inshop-scheduling-card.vue", () => {
test("highlights selected time slot", async () => { test("highlights selected time slot", async () => {
const wrapper = mountComponent({ const wrapper = mountComponent({
modelValue: { modelValue: {
selectedInshopTimeSlot: { appointmentType: AppointmentTypeStrings.IN_SHOP,
providerNumber: "001820", providerNumber: "001820",
routeCodeId: MORNING_INSHOP_ROUTE_CODE, routeCodeId: MORNING_INSHOP_ROUTE_CODE,
}, },
},
}); });
const selectedSlot = wrapper.find(".inshop-scheduling-card__slot--selected"); const selectedSlot = wrapper.find(".inshop-scheduling-card__slot--selected");
expect(selectedSlot.exists()).toBe(true); expect(selectedSlot.exists()).toBe(true);
@ -225,6 +221,18 @@ describe("inshop-scheduling-card.vue", () => {
wrapper.unmount(); wrapper.unmount();
}); });
test("does not highlight selection from another provider", () => {
const wrapper = mountComponent({
modelValue: {
appointmentType: AppointmentTypeStrings.IN_SHOP,
providerNumber: "999999",
routeCodeId: MORNING_INSHOP_ROUTE_CODE,
},
});
expect(wrapper.find(".inshop-scheduling-card__slot--selected").exists()).toBe(false);
wrapper.unmount();
});
test("collapses and expands body when header is clicked", async () => { test("collapses and expands body when header is clicked", async () => {
const wrapper = mountComponent(); const wrapper = mountComponent();
expect(wrapper.find(".inshop-scheduling-card__body").isVisible()).toBe(false); expect(wrapper.find(".inshop-scheduling-card__body").isVisible()).toBe(false);

View file

@ -23,7 +23,7 @@
<img <img
class="inshop-scheduling-card__chevron" class="inshop-scheduling-card__chevron"
:class="{ 'inshop-scheduling-card__chevron--expanded': isExpanded }" :class="{ 'inshop-scheduling-card__chevron--expanded': isExpanded }"
src="@/assets/img/icons/chevron-right-blue.svg" src="@/assets/img/icons/chevron-no-background.svg"
alt="" alt=""
aria-hidden="true" /> aria-hidden="true" />
</button> </button>
@ -107,7 +107,7 @@
</template> </template>
<script> <script>
import { RouteCodeFlags } from "@/constants/schedule-constants"; import { RouteCodeFlags, AppointmentTypeStrings } from "@/constants/schedule-constants";
import { import {
groupInshopTimeSlotsByTimeOfDay, groupInshopTimeSlotsByTimeOfDay,
INSHOP_INITIAL_VISIBLE_TIME_SLOTS, INSHOP_INITIAL_VISIBLE_TIME_SLOTS,
@ -145,7 +145,7 @@ export default {
}, },
radioGroupName: { radioGroupName: {
type: String, type: String,
required: true, default: "schedulingTimeSlot",
}, },
cmsWidgetName: { cmsWidgetName: {
type: String, type: String,
@ -182,7 +182,7 @@ export default {
if (miles == null) { if (miles == null) {
return ""; return "";
} }
const rounded = Math.round(miles * 2) / 2; const rounded = Math.round(miles * 10) / 10;
return `${rounded} mi`; return `${rounded} mi`;
}, },
formattedAddress() { formattedAddress() {
@ -265,7 +265,13 @@ export default {
return `${this.appointmentCount} appt${this.appointmentCount === 1 ? "" : "s"}`; return `${this.appointmentCount} appt${this.appointmentCount === 1 ? "" : "s"}`;
}, },
selectedRouteCodeValue() { selectedRouteCodeValue() {
return this.modelValue?.selectedInshopTimeSlot?.routeCodeId ?? null; if (
this.modelValue?.appointmentType !== AppointmentTypeStrings.IN_SHOP ||
this.modelValue?.providerNumber !== this.provider.providerNumber
) {
return null;
}
return this.modelValue?.routeCodeId ?? null;
}, },
}, },
methods: { methods: {
@ -305,10 +311,9 @@ export default {
}, },
onTimeSlotSelected(slot) { onTimeSlotSelected(slot) {
this.$emit("update:modelValue", { this.$emit("update:modelValue", {
selectedInshopTimeSlot: { appointmentType: AppointmentTypeStrings.IN_SHOP,
providerNumber: this.provider.providerNumber, providerNumber: this.provider.providerNumber,
routeCodeId: slot.routeCodeId, routeCodeId: slot.routeCodeId,
},
}); });
}, },
}, },
@ -382,11 +387,11 @@ export default {
width: 16px; width: 16px;
height: 16px; height: 16px;
flex-shrink: 0; flex-shrink: 0;
transform: rotate(90deg); transform: rotate(180deg);
transition: transform 150ms linear; transition: transform 150ms linear;
&--expanded { &--expanded {
transform: rotate(-90deg); transform: rotate(0deg);
} }
} }

View file

@ -1,6 +1,6 @@
import { shallowMount } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
import mobileSchedulingCard from "./mobile-scheduling-card"; import mobileSchedulingCard from "./mobile-scheduling-card";
import { PREMIUM_TIME_SLOT_ID_FLAG } from "@/constants/schedule-constants"; import { PREMIUM_TIME_SLOT_ID_FLAG, AppointmentTypeStrings } from "@/constants/schedule-constants";
import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { getMountOptions } from "@/helpers/unit-test-helper.js";
const MOCK_TIME_SLOTS = [ const MOCK_TIME_SLOTS = [
@ -54,6 +54,7 @@ function mountComponent(props = {}, cmsContent = {}) {
return shallowMount(mobileSchedulingCard, { return shallowMount(mobileSchedulingCard, {
props: { props: {
zipCode: "43235", zipCode: "43235",
providerNumber: "03357",
timeSlots: MOCK_TIME_SLOTS, timeSlots: MOCK_TIME_SLOTS,
premiumTimeSlotPrice: 29.99, premiumTimeSlotPrice: 29.99,
...props, ...props,
@ -102,10 +103,10 @@ describe("mobile-scheduling-card.vue", () => {
); );
await premiumInput.setValue(true); await premiumInput.setValue(true);
expect(wrapper.emitted("update:modelValue")[0][0]).toEqual({ expect(wrapper.emitted("update:modelValue")[0][0]).toEqual({
selectedMobileTimeSlot: { appointmentType: AppointmentTypeStrings.MOBILE,
providerNumber: "03357",
routeCodeId: MOCK_TIME_SLOTS[0].id, routeCodeId: MOCK_TIME_SLOTS[0].id,
isPremiumAppointment: true, isPremiumAppointment: true,
},
}); });
wrapper.unmount(); wrapper.unmount();
}); });
@ -120,11 +121,11 @@ describe("mobile-scheduling-card.vue", () => {
await premiumInput.setValue(true); await premiumInput.setValue(true);
await wrapper.setProps({ await wrapper.setProps({
modelValue: { modelValue: {
selectedMobileTimeSlot: { appointmentType: AppointmentTypeStrings.MOBILE,
providerNumber: "03357",
routeCodeId: MOCK_TIME_SLOTS[0].id, routeCodeId: MOCK_TIME_SLOTS[0].id,
isPremiumAppointment: true, isPremiumAppointment: true,
}, },
},
}); });
expect(wrapper.text()).toContain("8:00 am - 12:00 pm arrival window"); expect(wrapper.text()).toContain("8:00 am - 12:00 pm arrival window");
@ -136,11 +137,23 @@ describe("mobile-scheduling-card.vue", () => {
const pmInput = wrapper.find(`input[value="${MOCK_TIME_SLOTS[1].id}"]`); const pmInput = wrapper.find(`input[value="${MOCK_TIME_SLOTS[1].id}"]`);
await pmInput.setValue(true); await pmInput.setValue(true);
expect(wrapper.emitted("update:modelValue")[0][0]).toEqual({ expect(wrapper.emitted("update:modelValue")[0][0]).toEqual({
selectedMobileTimeSlot: { appointmentType: AppointmentTypeStrings.MOBILE,
providerNumber: "03357",
routeCodeId: MOCK_TIME_SLOTS[1].id, routeCodeId: MOCK_TIME_SLOTS[1].id,
isPremiumAppointment: false, isPremiumAppointment: false,
});
wrapper.unmount();
});
test("does not highlight selection from in-shop appointment", async () => {
const wrapper = mountComponent({
modelValue: {
appointmentType: AppointmentTypeStrings.IN_SHOP,
providerNumber: "001820",
routeCodeId: MOCK_TIME_SLOTS[0].id,
}, },
}); });
expect(wrapper.find(".mobile-scheduling-card__slot--selected").exists()).toBe(false);
wrapper.unmount(); wrapper.unmount();
}); });

View file

@ -32,7 +32,7 @@
<img <img
class="mobile-scheduling-card__chevron" class="mobile-scheduling-card__chevron"
:class="{ 'mobile-scheduling-card__chevron--expanded': isExpanded }" :class="{ 'mobile-scheduling-card__chevron--expanded': isExpanded }"
src="@/assets/img/icons/chevron-right-blue.svg" src="@/assets/img/icons/chevron-no-background.svg"
alt="" alt=""
aria-hidden="true" /> aria-hidden="true" />
</button> </button>
@ -59,7 +59,7 @@
<input <input
type="radio" type="radio"
class="mobile-scheduling-card__slot-input" class="mobile-scheduling-card__slot-input"
name="mobileSchedulingTimeSlot" :name="radioGroupName"
:value="slot.value" :value="slot.value"
:checked="isSlotSelected(slot)" :checked="isSlotSelected(slot)"
@change="onTimeSlotSelected(slot)" /> @change="onTimeSlotSelected(slot)" />
@ -84,7 +84,7 @@
</template> </template>
<script> <script>
import { PREMIUM_TIME_SLOT_ID_FLAG } from "@/constants/schedule-constants"; import { PREMIUM_TIME_SLOT_ID_FLAG, AppointmentTypeStrings } from "@/constants/schedule-constants";
import { militaryToTwelveHourTime } from "@/layouts/schedule/helpers/schedule-helper"; import { militaryToTwelveHourTime } from "@/layouts/schedule/helpers/schedule-helper";
export default { export default {
@ -107,6 +107,14 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
providerNumber: {
type: String,
required: true,
},
radioGroupName: {
type: String,
default: "schedulingTimeSlot",
},
showFreeFlag: { showFreeFlag: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -179,13 +187,18 @@ export default {
return `+$${this.premiumTimeSlotPrice.toFixed(2)}`; return `+$${this.premiumTimeSlotPrice.toFixed(2)}`;
}, },
selectedRouteCodeValue() { selectedRouteCodeValue() {
const selected = this.modelValue?.selectedMobileTimeSlot; if (
if (!selected?.routeCodeId) { this.modelValue?.appointmentType !== AppointmentTypeStrings.MOBILE ||
this.modelValue?.providerNumber !== this.providerNumber
) {
return null; return null;
} }
return selected.isPremiumAppointment if (!this.modelValue?.routeCodeId) {
? this.addPremiumFlagToInput(selected.routeCodeId) return null;
: selected.routeCodeId; }
return this.modelValue.isPremiumAppointment
? this.addPremiumFlagToInput(this.modelValue.routeCodeId)
: this.modelValue.routeCodeId;
}, },
}, },
methods: { methods: {
@ -205,10 +218,10 @@ export default {
}, },
onTimeSlotSelected(slot) { onTimeSlotSelected(slot) {
this.$emit("update:modelValue", { this.$emit("update:modelValue", {
selectedMobileTimeSlot: { appointmentType: AppointmentTypeStrings.MOBILE,
providerNumber: this.providerNumber,
routeCodeId: slot.routeCodeId, routeCodeId: slot.routeCodeId,
isPremiumAppointment: slot.isPremiumAppointment, isPremiumAppointment: slot.isPremiumAppointment,
},
}); });
}, },
addPremiumFlagToInput(routeCode) { addPremiumFlagToInput(routeCode) {
@ -300,11 +313,11 @@ export default {
width: 16px; width: 16px;
height: 16px; height: 16px;
flex-shrink: 0; flex-shrink: 0;
transform: rotate(90deg); transform: rotate(180deg);
transition: transform 150ms linear; transition: transform 150ms linear;
&--expanded { &--expanded {
transform: rotate(-90deg); transform: rotate(0deg);
} }
} }

View file

@ -22,21 +22,23 @@
<mobileSchedulingCard <mobileSchedulingCard
v-if="showMobileSchedulingCard" v-if="showMobileSchedulingCard"
class="mt-4" class="mt-4"
v-model="selectedMobileScheduling" v-model="selectedScheduling"
:providerNumber="mobileProviderAndTimeSlot.providerNumber"
:timeSlots="mobileTimeSlotsForSelectedDate" :timeSlots="mobileTimeSlotsForSelectedDate"
:premiumTimeSlotPrice="premiumTimeSlotPrice" :premiumTimeSlotPrice="premiumTimeSlotPrice"
:zipCode="serviceZipCode" :zipCode="serviceZipCode"
:showFreeFlag="showMobileFreeFlag" :showFreeFlag="showMobileFreeFlag"
:radioGroupName="schedulingRadioGroupName"
@zip-code-clicked="onMobileZipCodeClicked" /> @zip-code-clicked="onMobileZipCodeClicked" />
<inshopSchedulingCard <inshopSchedulingCard
v-for="{ provider } in inShopProvidersAndTimeslots" v-for="{ provider } in inShopProvidersAndTimeslots"
v-show="selectedDate" v-show="selectedDate"
:key="provider.providerNumber" :key="provider.providerNumber"
class="mt-4" class="mt-4"
v-model="selectedInshopSchedulingByProvider[provider.providerNumber]" v-model="selectedScheduling"
:provider="provider" :provider="provider"
:timeSlots="getInshopTimeSlotsForSelectedDate(provider.providerNumber)" :timeSlots="getInshopTimeSlotsForSelectedDate(provider.providerNumber)"
:radioGroupName="`inshopSchedulingTimeSlot-${provider.providerNumber}`" :radioGroupName="schedulingRadioGroupName"
@address-clicked="onInshopAddressClicked(provider)" /> @address-clicked="onInshopAddressClicked(provider)" />
<navbar <navbar
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
@ -83,6 +85,7 @@ function toDateString(offsetDays, base = new Date()) {
} }
const SCHEDULE_FETCH_DAYS = 15; const SCHEDULE_FETCH_DAYS = 15;
const SCHEDULING_RADIO_GROUP_NAME = "schedulingTimeSlot";
export default { export default {
name: "scheduling", name: "scheduling",
@ -155,11 +158,13 @@ export default {
}, },
watch: { watch: {
selectedDate() { selectedDate() {
this.selectedMobileScheduling = null; this.selectedScheduling = null;
this.selectedInshopSchedulingByProvider = {};
}, },
}, },
computed: { computed: {
schedulingRadioGroupName() {
return SCHEDULING_RADIO_GROUP_NAME;
},
serviceLocationText() { serviceLocationText() {
return this.getCmsContent("ServiceLocationText", "Text"); return this.getCmsContent("ServiceLocationText", "Text");
}, },
@ -208,8 +213,7 @@ export default {
inShopProvidersAndTimeslots: [], inShopProvidersAndTimeslots: [],
mobileProviderAndTimeSlot: null, mobileProviderAndTimeSlot: null,
mobilePremiumAppointmentFee: null, mobilePremiumAppointmentFee: null,
selectedMobileScheduling: null, selectedScheduling: null,
selectedInshopSchedulingByProvider: {},
}; };
}, },
methods: { methods: {