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 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";
const ALL_DAY_DROPOFF_ROUTE_CODE = `03357-001820-I*21346*${RouteCodeFlags.ALL_DAY_DROP_OFF}`;
@ -142,10 +142,9 @@ describe("inshop-scheduling-card.vue", () => {
await dropOffInput.setValue(true);
await wrapper.setProps({
modelValue: {
selectedInshopTimeSlot: {
providerNumber: "001820",
routeCodeId: ALL_DAY_DROPOFF_ROUTE_CODE,
},
appointmentType: AppointmentTypeStrings.IN_SHOP,
providerNumber: "001820",
routeCodeId: ALL_DAY_DROPOFF_ROUTE_CODE,
},
});
@ -158,10 +157,9 @@ describe("inshop-scheduling-card.vue", () => {
const dropOffInput = wrapper.find(`input[value="${ALL_DAY_DROPOFF_ROUTE_CODE}"]`);
await dropOffInput.setValue(true);
expect(wrapper.emitted("update:modelValue")[0][0]).toEqual({
selectedInshopTimeSlot: {
providerNumber: "001820",
routeCodeId: ALL_DAY_DROPOFF_ROUTE_CODE,
},
appointmentType: AppointmentTypeStrings.IN_SHOP,
providerNumber: "001820",
routeCodeId: ALL_DAY_DROPOFF_ROUTE_CODE,
});
wrapper.unmount();
});
@ -202,10 +200,9 @@ describe("inshop-scheduling-card.vue", () => {
const morningInput = wrapper.find(`input[value="${MORNING_INSHOP_ROUTE_CODE}"]`);
await morningInput.setValue(true);
expect(wrapper.emitted("update:modelValue")[0][0]).toEqual({
selectedInshopTimeSlot: {
providerNumber: "001820",
routeCodeId: MORNING_INSHOP_ROUTE_CODE,
},
appointmentType: AppointmentTypeStrings.IN_SHOP,
providerNumber: "001820",
routeCodeId: MORNING_INSHOP_ROUTE_CODE,
});
wrapper.unmount();
});
@ -213,10 +210,9 @@ describe("inshop-scheduling-card.vue", () => {
test("highlights selected time slot", async () => {
const wrapper = mountComponent({
modelValue: {
selectedInshopTimeSlot: {
providerNumber: "001820",
routeCodeId: MORNING_INSHOP_ROUTE_CODE,
},
appointmentType: AppointmentTypeStrings.IN_SHOP,
providerNumber: "001820",
routeCodeId: MORNING_INSHOP_ROUTE_CODE,
},
});
const selectedSlot = wrapper.find(".inshop-scheduling-card__slot--selected");
@ -225,6 +221,18 @@ describe("inshop-scheduling-card.vue", () => {
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 () => {
const wrapper = mountComponent();
expect(wrapper.find(".inshop-scheduling-card__body").isVisible()).toBe(false);

View file

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

View file

@ -1,6 +1,6 @@
import { shallowMount } from "@vue/test-utils";
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";
const MOCK_TIME_SLOTS = [
@ -54,6 +54,7 @@ function mountComponent(props = {}, cmsContent = {}) {
return shallowMount(mobileSchedulingCard, {
props: {
zipCode: "43235",
providerNumber: "03357",
timeSlots: MOCK_TIME_SLOTS,
premiumTimeSlotPrice: 29.99,
...props,
@ -102,10 +103,10 @@ describe("mobile-scheduling-card.vue", () => {
);
await premiumInput.setValue(true);
expect(wrapper.emitted("update:modelValue")[0][0]).toEqual({
selectedMobileTimeSlot: {
routeCodeId: MOCK_TIME_SLOTS[0].id,
isPremiumAppointment: true,
},
appointmentType: AppointmentTypeStrings.MOBILE,
providerNumber: "03357",
routeCodeId: MOCK_TIME_SLOTS[0].id,
isPremiumAppointment: true,
});
wrapper.unmount();
});
@ -120,10 +121,10 @@ describe("mobile-scheduling-card.vue", () => {
await premiumInput.setValue(true);
await wrapper.setProps({
modelValue: {
selectedMobileTimeSlot: {
routeCodeId: MOCK_TIME_SLOTS[0].id,
isPremiumAppointment: true,
},
appointmentType: AppointmentTypeStrings.MOBILE,
providerNumber: "03357",
routeCodeId: MOCK_TIME_SLOTS[0].id,
isPremiumAppointment: true,
},
});
@ -136,11 +137,23 @@ describe("mobile-scheduling-card.vue", () => {
const pmInput = wrapper.find(`input[value="${MOCK_TIME_SLOTS[1].id}"]`);
await pmInput.setValue(true);
expect(wrapper.emitted("update:modelValue")[0][0]).toEqual({
selectedMobileTimeSlot: {
routeCodeId: MOCK_TIME_SLOTS[1].id,
isPremiumAppointment: false,
appointmentType: AppointmentTypeStrings.MOBILE,
providerNumber: "03357",
routeCodeId: MOCK_TIME_SLOTS[1].id,
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();
});

View file

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

View file

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