DigitalConsumer.ISS/src/digital-components/date-picker/date-picker.vue
2025-12-10 15:05:33 -05:00

1122 lines
40 KiB
Vue

<template>
<div
class="date-picker text-center"
:class="[calendarViewDirection, { 'has-error': errors.length > 0 }]">
<fieldset
id="date-picker-fieldset"
ref="datePickerFieldset">
<legend class="sr-only">
Select a day and time
</legend>
<div
v-for="month in months"
:id="`${month.monthLabel}-${month.yearNum?.toString()}`"
:key="`${month.monthLabel}-${month.yearNum?.toString()}`"
class="calendar-grid-container"
:class="[
hideSomeDaysForInitialView
? 'partial-month-initial-view'
: '',
month.monthClass
]">
<div
class="month-year body-small d-flex align-items-center small">
{{ month.monthLabel }} {{ month.yearNum?.toString() }}
</div>
<div
v-if="calendarViewDirection === 'future'"
class="legend caption d-flex align-items-center justify-content-end">
<span class="legend-circle me-1"></span> &equals; Available
</div>
<div class="separator-line"></div>
<div class="nav-back ps-3">
<button></button>
</div>
<div class="nav-forward pe-3">
<button></button>
</div>
<div class="grid-item caption">
<span class="sr-only">Sunday</span>S
</div>
<div class="grid-item caption">
<span class="sr-only">Monday</span>M
</div>
<div class="grid-item caption">
<span class="sr-only">Tuesday</span>T
</div>
<div class="grid-item caption">
<span class="sr-only">Wednesday</span>W
</div>
<div class="grid-item caption">
<span class="sr-only">Thursday</span>T
</div>
<div class="grid-item caption">
<span class="sr-only">Friday</span>F
</div>
<div class="grid-item caption">
<span class="sr-only">Saturday</span>S
</div>
<div
v-for="date in month.dates"
:id="date.inputValue"
:key="date.inputValue"
class="grid-item radio-wrapper"
:class="[
date.dateNum === 1
? 'first-day-' + month.startDateDayIndex
: '',
date.dayClasses,
date.isSelectable ? 'selectable-day' : ''
]">
<input
:id="`${month.monthLabel}-${date.dateNum.toString()}`"
v-model="selectedDate"
:disabled="!date.isSelectable"
type="radio"
name="day-of-month"
:value="date.inputValue"
@click="fireDateSelectedEvent"
@keypress.enter="fireDateSelectedEvent" />
<label
:aria-label="getDateLabel(date, month)"
:for="`${month.monthLabel}-${date.dateNum.toString()}`">
<span aria-hidden="true">{{ date.dateNum.toString() }}</span>
</label>
</div>
</div>
<div
id="date-of-month-error"
class="row form-test-error">
<ErrorMessage
:name="customComponentId"
class="small mt-1">
</ErrorMessage>
</div>
<button
v-if="
calendarViewDirection === 'future' &&
!disableViewMoreDatesButton
"
id="viewMoreDates"
type="button"
class="btn btn-link"
:disabled="isLoading"
@click="showAnotherMonth">
View more dates
</button>
</fieldset>
</div>
</template>
<script>
// Supporting files
import { useMainStore } from '@/store';
import { useField, ErrorMessage } from 'vee-validate';
import { deepClone } from '@/helpers/object-helper';
import {
convertDateStringToDate,
convertDateToDateString
} from '@/helpers/date-helper';
import {
TIMINGFUNC_MAP,
BUFFER_OFFSET,
MONTHS_OF_YEAR
} from './mixins/constants';
import { selectableDaysOptions, requiredParameter } from './mixins/helpers';
export default {
name: 'date-picker',
components: {
ErrorMessage
},
props: {
customComponentId: String,
selectableDatesSetting: {
type: String,
validator(value) {
return Object.values(selectableDaysOptions).includes(value);
},
default: selectableDaysOptions.PAST
},
modelValue: {
type: Object
},
todayOverrideDateString: {
// keep for use in unit tests to override today's date
type: String,
default: null
},
customSelectableDatesCallback: {
type: Function,
default() {
return [];
}
},
validationRules: {
type: String,
default: ''
}
},
emits: ['update:modelValue', 'date-clicked'],
setup(props) {
const uuid = crypto.randomUUID();
const componentId = !props.customComponentId
? `component-${uuid}`
: props.customComponentId;
const mainStore = useMainStore();
const { modelValue } = deepClone(props);
const initialValue = modelValue;
const fieldOptions = {
value: modelValue,
initialValue
};
const {
errorMessage,
handleBlur,
handleChange,
meta,
validate,
errors,
resetField
} = useField(componentId, props.validationRules, fieldOptions);
return {
componentId,
mainStore,
errorMessage,
handleBlur,
handleChange,
validate,
meta,
errors,
resetField
};
},
data() {
return {
isLoading: true,
months: null,
disableViewMoreDatesButton: false,
selectableDatesData: [], // NOTE: uses monthNum (1-based), NOT monthIndex (0-based)
hideSomeDaysForInitialView: null
};
},
computed: {
todayString() {
return (
this.todayOverrideDateString
|| convertDateToDateString(new Date())
);
},
todayDayIndex() {
return convertDateStringToDate(this.todayString).getDay();
},
todayDateNum() {
return convertDateStringToDate(this.todayString).getDate();
},
currentWeekStartDateNum() {
return this.todayDayIndex >= this.todayDateNum
? 1
: this.todayDateNum - this.todayDayIndex;
},
currentWeekEndDateNum() {
return this.todayDateNum + (6 - this.todayDayIndex);
},
calendarViewDirection() {
if (this.selectableDatesSetting === 'custom') return 'future';
return 'past';
},
selectedDate: {
get() {
return this.modelValue;
},
set(newSelectedDate) {
this.$emit('update:modelValue', newSelectedDate);
}
}
},
watch: {
modelValue(newValue, oldValue) {
this.resetField({
value: newValue
});
},
errorMessage(newValue, oldValue) {
// Check for changing from no error message to some error message.
if (newValue && !oldValue) {
this.scrollToElement('date-of-month-error');
}
}
},
methods: {
initializeComponent(initialData) {
this.setCalendarData(initialData);
},
fireDateSelectedEvent(event) {
// Ignore if arrow key selected radioButton
if (event.screenX === 0 && event.screenY === 0) {
return;
}
this.$emit('date-clicked');
},
getWeekStartDate(dateString) {
const date = convertDateStringToDate(dateString);
const dayOfWeek = date.getDay();
// Subtract the day of the week from date to get the date of Sunday
const sunday = new Date(date);
sunday.setDate(sunday.getDate() - dayOfWeek);
return convertDateToDateString(sunday);
},
getWeekEndDate(dateString) {
const date = convertDateStringToDate(dateString);
const dayOfWeek = date.getDay();
const daysUntilSaturday = 6 - dayOfWeek; // Calculate the number of days until Saturday
// Clone the given date and add the remaining days until Saturday
const saturday = new Date(date);
saturday.setDate(date.getDate() + daysUntilSaturday);
return convertDateToDateString(saturday);
},
getNextWeekSunday(dateString) {
const date = convertDateStringToDate(dateString);
const dayOfWeek = date.getDay();
const daysUntilNextSunday = 7 - dayOfWeek; // Calculate the number of days until the next Sunday
// Clone the given date and add the remaining days until Sunday
const nextSunday = new Date(date);
nextSunday.setDate(date.getDate() + daysUntilNextSunday);
return convertDateToDateString(nextSunday);
},
getMonthEnd(dateStr) {
// convert string to date, do date calc, then return a string back
const date = new Date(
dateStr.split('-')[0],
parseInt(dateStr.split('-')[1], 10),
0
);
return convertDateToDateString(date);
},
getInitialViewWeeks(
todayString,
initialViewRowsToShow,
preSelectedDateString
) {
// TODO: this only is for future direction; need to create logic for past direction
const weeks = [];
let weekStartDate = this.getWeekStartDate(todayString);
let weekEndDate = this.getWeekEndDate(todayString);
if (preSelectedDateString) {
const preSelectedDateMonthEnd = this.getMonthEnd(preSelectedDateString);
let weekIncludesPreSelectedMonthEnd = false;
let i = 0;
while (!weekIncludesPreSelectedMonthEnd) {
if (i > 0) {
weekStartDate = this.getNextWeekSunday(weekEndDate);
weekEndDate = this.getWeekEndDate(weekStartDate);
if (
(preSelectedDateMonthEnd > weekStartDate
&& preSelectedDateMonthEnd < weekEndDate)
|| preSelectedDateMonthEnd === weekStartDate
|| preSelectedDateMonthEnd === weekEndDate
) {
weekEndDate = preSelectedDateMonthEnd;
weekIncludesPreSelectedMonthEnd = true;
}
}
weeks.push({
weekNum: i + 1,
weekStartDate,
weekEndDate
});
i++;
}
} else {
for (let i = 0; i < initialViewRowsToShow; i++) {
if (i > 0) {
weekStartDate = this.getNextWeekSunday(weekEndDate);
weekEndDate = this.getWeekEndDate(weekStartDate);
}
weeks.push({
weekNum: i + 1,
weekStartDate,
weekEndDate
});
}
// If any of these weeks is split between two months, then make them 2 separate "weeks"
// (a week split between two months is considered 2 weeks per business requirements)
const hasSplitWeek = (week) =>
week.weekStartDate.split('-')[1]
!== week.weekEndDate.split('-')[1];
const splitWeekIndex = weeks.findIndex(hasSplitWeek);
if (splitWeekIndex > -1) {
const week1 = [];
const week2 = [];
let switchToWeek2 = false;
for (let j = 0; j < 7; j++) {
const newDate = convertDateStringToDate(weeks[splitWeekIndex].weekStartDate);
newDate.setDate(newDate.getDate() + j);
if (newDate.getDate() === 1) switchToWeek2 = true;
if (switchToWeek2) {
week2.push(convertDateToDateString(newDate));
} else {
week1.push(convertDateToDateString(newDate));
}
}
const week1EndDate = week1[week1.length - 1];
const week2StartDate = week2[0];
if (week1EndDate < todayString) {
// replace week 1 with week 2
weeks[splitWeekIndex].weekStartDate = week2StartDate;
} else {
const newWeek = {
weekNum: weeks[splitWeekIndex].weekNum,
weekStartDate: week2StartDate,
weekEndDate: weeks[splitWeekIndex].weekEndDate
};
weeks[splitWeekIndex].weekEndDate = week1EndDate;
weeks.splice(splitWeekIndex + 1, 0, newWeek);
weeks.pop();
weeks.forEach((item, index) => {
if (index > splitWeekIndex) {
item.weekNum += 1;
}
});
}
}
}
return weeks;
},
async loadInitialData(config) {
/*
** NOTE: this _could_ be called by a parent before fully loaded, so data or computeds might not be available
*/
let todayDateString;
let calendarViewDirection = 'none';
let hideSomeDaysForInitialView = false;
let hideSecondMonth = false;
if (this.todayString) {
todayDateString = this.todayString;
} else if (config.todayOverrideDateString) {
todayDateString = config.todayOverrideDateString;
} else {
todayDateString = convertDateToDateString(new Date());
}
if (config.selectableDatesSetting === 'past') calendarViewDirection = 'past';
if (config.selectableDatesSetting === 'custom') calendarViewDirection = 'future';
const currentMonthEnd = this.getMonthEnd(todayDateString);
// TODO - set up currentMonthStart if direction is PAST:
// let currentMonthStart = new Date(todayYearNum, todayMonthIndex - 1, 1);
const initialViewWeeks = this.getInitialViewWeeks(
todayDateString,
config.initialViewRowsToShow,
config.preSelectedDate
);
const initialViewStartDate = todayDateString;
const initialViewEndDate =
initialViewWeeks[initialViewWeeks.length - 1].weekEndDate;
if (!config.preSelectedDate) {
const firstSaturdayMonth =
initialViewWeeks[0].weekEndDate.split('-')[1];
const lastSundayMonth =
initialViewWeeks[
initialViewWeeks.length - 1
].weekStartDate.split('-')[1];
if (calendarViewDirection === 'future') {
if (firstSaturdayMonth !== lastSundayMonth) {
hideSomeDaysForInitialView = true;
}
if (
initialViewStartDate.split('-')[1] === lastSundayMonth
) {
hideSecondMonth = true;
if (currentMonthEnd > initialViewEndDate) {
// should part of 1st month be hidden?
hideSomeDaysForInitialView = true;
}
}
}
// TODO - NEED TO CREATE OPPOSITE LOGIC FOR "past" DIRECTION LIKE THE ABOVE ^ ^ ^
}
const loadInitialDataPromise = new Promise((resolve, reject) => {
const response = config.customSelectableDatesCallback(
initialViewStartDate,
initialViewEndDate,
useMainStore().order.serviceLocation.appointmentType,
useMainStore().order.serviceLocation.provider.providerNumber
);
resolve(response);
});
return loadInitialDataPromise.then((response) => {
const initialData = {
todayDate: todayDateString,
initialViewStartDate,
initialViewEndDate,
calendarViewDirection,
initialShopTimeSlotsResponse: response,
hideSomeDaysForInitialView,
hideSecondMonth,
preSelectedDate: config.preSelectedDate
};
return initialData;
});
},
async setCalendarData(config = {}) {
this.hideSomeDaysForInitialView = config.hideSomeDaysForInitialView;
const { hideSecondMonth } = config;
const direction = config.calendarViewDirection;
const monthsAfterToLoadOffset = 6;
const monthsBeforeToLoadOffset = 36;
config.initialShopTimeSlotsResponse.days.forEach((selectableDate) => {
this.selectableDatesData.push(selectableDate);
});
// GENERATE MONTHS AND PUSH THEM INTO ARRAY
const months = [];
const options = {
calendarViewDirection: direction,
monthsBeforeToLoadOffset,
monthsAfterToLoadOffset,
initialViewStartDate: config.initialViewStartDate,
initialViewEndDate: config.initialViewEndDate,
hideSecondMonth,
preSelectedDate: config.preSelectedDate
};
if (direction === 'future') {
// first 0, then 1
for (let i = 0; i <= monthsAfterToLoadOffset; i++) {
months.push(await this.getMonthData(i, options));
}
} else if (direction === 'past') {
// first 0, then -1
for (let i = 0; i >= 0 - monthsBeforeToLoadOffset; i--) {
months.unshift(await this.getMonthData(i, options));
}
} else {
// TODO - IF A CALENDAR WITH BOTH PAST AND FUTURE WAS EVER NEEDED
// for (let i = monthsAfterToLoadOffset; i >= monthsBeforeToLoadOffset; i--) {
// months.push(this.getMonthDataPAST(i));
// }
}
this.months = months;
this.isLoading = false;
if (config.preSelectedDate) {
this.$nextTick(() => {
// Advance to month
const monthToShow = this.months.find((month) =>
month.monthClass.includes('month-preselected'));
if (
monthToShow.monthClass.includes('month-preselected')
&& monthToShow.monthClass.includes('last-available-month')
) {
// disable View More dates button if the preSelectedDate in the last available month
this.disableViewMoreDatesButton = true;
}
this.scrollToElement(monthToShow.monthString);
});
}
},
async getMonthData(offset = requiredParameter(), options) {
/* options will contain:
calendarViewDirection (string)
initialViewStartDateNum (number)
initialViewEndDateNum (number)
monthsBeforeToLoadOffset (number),
monthsAfterToLoadOffset (number),
hideSecondMonth (boolean),
preSelectedDate (string)
data used:
todayDate (date object)
this.hideSomeDaysForInitialView (string)
*/
let monthNum =
convertDateStringToDate(this.todayString).getMonth()
+ offset
+ 1;
let yearNum = convertDateStringToDate(this.todayString).getFullYear();
const { calendarViewDirection } = options;
if (calendarViewDirection === 'future' && offset > 0) {
while (monthNum > 12) {
monthNum -= 12;
yearNum++;
}
} else if (calendarViewDirection === 'past' && offset < 0) {
while (monthNum < 1) {
monthNum = 12 + monthNum;
yearNum--;
}
}
const { initialViewEndDate } = options;
// const initialViewStartDate = options.initialViewStartDate; // TODO: to be used for past calendarViewDirection
const { hideSecondMonth } = options;
const preSelectedDateObj = options.preSelectedDate
? convertDateStringToDate(options.preSelectedDate)
: null;
const dates = [];
const monthEndDate = new Date(yearNum, monthNum, 0);
const monthStartDateNum =
offset === 0 && calendarViewDirection === 'future'
? this.currentWeekStartDateNum
: 1;
const monthStartDate = new Date(
yearNum,
monthNum - 1,
monthStartDateNum
);
const startDateDayIndex = monthStartDate.getDay();
let monthClass = '';
let isMonthThatHidesSomeDaysForInitialView;
let monthEndDateNum = monthEndDate.getDate();
if (
offset === 0
&& calendarViewDirection === 'past'
&& monthEndDateNum > this.currentWeekEndDateNum
) {
monthEndDateNum = this.currentWeekEndDateNum;
}
if (options.preSelectedDate) {
if (
monthStartDate.getFullYear()
=== preSelectedDateObj.getFullYear()
&& monthStartDate.getMonth() === preSelectedDateObj.getMonth()
) {
monthClass += ' month-preselected';
} else if (monthStartDate > preSelectedDateObj) {
monthClass += ' month-hidden';
}
} else if (Math.abs(offset) === 1 && hideSecondMonth) {
monthClass = `${monthClass} month-hidden`;
} else if (Math.abs(offset) > 1) {
monthClass = `${monthClass} month-hidden`;
}
if (
Math.abs(offset) === options.monthsAfterToLoadOffset
&& calendarViewDirection === 'future'
) {
monthClass += ' last-available-month';
}
if (
Math.abs(offset) === options.monthsBeforeToLoadOffset
&& calendarViewDirection === 'past'
) {
// TODO - re-check this logic if past direction
monthClass += ' last-available-month';
}
// populate dates array
for (let i = monthStartDateNum; i <= monthEndDateNum; i++) {
let dayClasses = '';
const dateString = `${yearNum.toString()}-${`0${monthNum}`.slice(-2)}-${`0${i}`.slice(-2)}`;
if (offset === 0 && i === this.todayDateNum) {
dayClasses += ' current-day';
}
if (
offset === 0
&& i < this.todayDateNum
&& calendarViewDirection === 'future'
) {
dayClasses += ' unavailable-day';
}
if (
offset === 0
&& i > this.todayDateNum
&& calendarViewDirection === 'past'
) {
dayClasses += ' unavailable-day';
}
if (convertDateStringToDate(dateString).getDay() === 0) {
dayClasses += ' sunday';
}
if (
this.hideSomeDaysForInitialView
&& convertDateStringToDate(initialViewEndDate).getMonth()
+ 1
=== monthNum
&& convertDateStringToDate(initialViewEndDate).getDate() < i
) {
dayClasses += ' day-hidden';
isMonthThatHidesSomeDaysForInitialView = true;
}
const dateObject = {
dateNum: i,
dayClasses,
inputValue: dateString,
isSelectable:
this.selectableDatesData.findIndex((date) => date.date === dateString) > -1
};
dates.push(dateObject);
}
const monthToAdd = {
monthLabel: MONTHS_OF_YEAR[monthNum - 1],
monthIndex: monthNum,
monthString: `${
MONTHS_OF_YEAR[monthNum - 1]
}-${yearNum?.toString()}`,
yearNum,
dates,
startDateDayIndex,
monthClass,
isMonthThatHidesSomeDaysForInitialView
};
return monthToAdd;
},
async showAnotherMonth() {
this.isLoading = true;
let monthToShow;
let monthStartDateNum = 0;
if (this.hideSomeDaysForInitialView) {
monthToShow = this.months.find(({ isMonthThatHidesSomeDaysForInitialView }) =>
isMonthThatHidesSomeDaysForInitialView);
// find the first day-hidden to become the next api call start date
monthStartDateNum =
monthToShow.dates.find(({ dayClasses }) =>
dayClasses.includes('day-hidden')).dateNum - 1;
} else {
if (this.calendarViewDirection === 'future') {
monthToShow = this.months.find((month) =>
month.monthClass.includes('month-hidden'));
}
if (this.calendarViewDirection === 'past') {
// TODO: UPDATE THIS WITH CORRECT PAST LOOKING LOGIC
monthToShow = this.months.find((month) =>
month.monthClass.includes('month-hidden'));
}
}
if (monthToShow) {
// make new API call with this month's start and end dates
await this.updateSelectableDates(
monthToShow.dates[monthStartDateNum].inputValue,
monthToShow.dates[monthToShow.dates.length - 1].inputValue
);
this.isLoading = false;
this.hideSomeDaysForInitialView = false; // if hid days on initial partial view, this will reveal those days
monthToShow.monthClass = monthToShow.monthClass.replace(
' month-hidden',
''
);
this.scrollToElement(monthToShow.monthString);
if (monthToShow.monthClass.includes('last-available-month')) this.disableViewMoreDatesButton = true;
}
},
async updateSelectableDates(monthStart, monthEnd) {
const moreSelectableDates =
await this.customSelectableDatesCallback(
monthStart,
monthEnd,
this.mainStore.order.serviceLocation.appointmentType,
this.mainStore.order.serviceLocation.provider.providerNumber
);
moreSelectableDates.days.forEach((selectableDate) => {
const index = this.selectableDatesData.findIndex((dateObj) => dateObj.date === selectableDate.date);
if (index === -1) this.selectableDatesData.push(selectableDate);
this.months.forEach((month) => {
// TODO: avoid checking all calendar dates; maybe only ones between monthStart and monthEnd as defined above?
month.dates.forEach((date) => {
if (date.inputValue === selectableDate.date) {
date.isSelectable = true;
}
});
});
});
},
scrollToElement(elementId, duration = 800, easing = 'ease-in-out') {
const wrapper = document.querySelector('.fade-on-route-transition');
const target = document.getElementById(elementId);
const initY = wrapper.scrollTop;
const wrapperRect = wrapper.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
const targetY = targetRect.top - wrapperRect.top - BUFFER_OFFSET;
const timingFunc = TIMINGFUNC_MAP[easing];
let start = null;
const step = (timestamp) => {
start = start || timestamp;
const time = Math.min(1, (timestamp - start) / duration);
const percentageNew = timingFunc(time);
const distanceToGo = targetY;
const thisDistance = percentageNew * distanceToGo;
wrapper.scrollTo(0, initY + thisDistance);
if (percentageNew < 1) {
window.requestAnimationFrame(step);
}
};
window.requestAnimationFrame(step);
},
getDateLabel(date, month) {
const dateStr = `${month.yearNum?.toString()}-${month.monthIndex?.toString()}-${date.dateNum.toString()}`;
const dateObj = convertDateStringToDate(dateStr);
const labelOptions = {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric'
};
let label = dateObj.toLocaleDateString('en-US', labelOptions);
date.isSelectable ? label += ', available' : label += ', unavailable';
return label;
}
}
};
</script>
<style lang="scss" scoped>
@import '@/styles/ux-variables-svg-strings.scss';
.date-picker-hidden {
opacity: 0;
max-height: 0;
}
.date-picker {
position: relative;
flex-grow: 0;
display: flex;
flex-direction: column;
fieldset {
flex-grow: 1;
position: relative;
}
.calendar-grid-container {
margin: 0 auto 2rem auto;
max-width: 414px;
position: relative;
transition: height ease 2s, opacity ease 2s;
display: grid;
grid-template-columns: repeat(7, 1fr);
justify-content: center;
align-items: center;
padding: 0 0.75rem;
opacity: 1;
.grid-item {
text-align: center;
margin: 10px 3px;
font-size: 0.875rem;
line-height: 1.5;
&.first-day-,
&.first-day-0 {
grid-column-start: 1;
}
&.first-day-1 {
grid-column-start: 2;
}
&.first-day-2 {
grid-column-start: 3;
}
&.first-day-3 {
grid-column-start: 4;
}
&.first-day-4 {
grid-column-start: 5;
}
&.first-day-5 {
grid-column-start: 6;
}
&.first-day-6 {
grid-column-start: 7;
}
}
.separator-line {
grid-area: 2/1/2/8;
border-top: 1px solid $gray-500;
margin: 0.5rem 0;
}
.month-year {
grid-area: 1 / 1 / 2 / 5;
text-transform: uppercase;
font-weight: 300;
}
.legend {
grid-area: 1 / 5 / 2 / 8;
.legend-circle {
border-radius: 50%;
width: 16px;
height: 16px;
background-color: $blue-100;
border: 1px solid $blue;
}
}
.nav-back,
.nav-forward {
display: none;
}
.radio-wrapper {
position: relative;
display: flex;
justify-content: center;
align-items: center;
outline: none;
height: 1.5rem;
opacity: 1;
transition: height ease 250ms, opacity ease 250ms;
input[type='radio'] {
position: absolute; //override bootstrap
height: 0;
opacity: 0;
&:focus-visible + label {
box-shadow: 0 0 0 2.5px $blue;
}
&:focus + label,
&:checked:focus + label {
box-shadow: 0 0 0 3px #fff, 0 0 0 5.5px #1574a1;
background-color: $blue;
color: $white;
&.current-day {
&:after {
background-color: $white;
}
.first-day {
color: $white;
}
}
}
&:checked + label {
color: $white;
background: $blue;
&:after {
background-color: $white;
}
.first-day {
color: $white;
}
}
}
.first-day {
position: absolute;
font-size: 9px;
font-weight: 500;
color: $gray-600;
top: 0;
z-index: 1;
}
label {
position: relative;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
min-width: 2.5rem;
width: 2.5rem;
height: 2.5rem;
span {
&.small {
font-size: 0.75rem;
color: $gray-550;
}
}
&:hover,
&:checked {
@include media-breakpoint-up(sm) {
box-shadow: 0 0 0 4px transparent;
background-color: $blue;
color: $white;
&:after {
background-color: $white;
}
}
cursor: pointer;
&.current-day {
+ .first-day {
color: $white;
}
}
.first-day {
color: $white;
}
}
+ p {
display: none;
}
}
&.selectable-day {
label {
color: $blue;
background-color: $blue-100;
border: 1px solid $blue;
min-width: 2.5rem;
width: 2.5rem;
border-radius: 50%;
}
}
&.unavailable-day {
label {
color: $gray-500;
background-color: $gray-100;
border: none;
pointer-events: none;
}
}
&.unavailable-day:not(&.sunday) {
label {
&::before {
content: '';
display: inline-block;
position: absolute;
left: -1rem;
width: 1rem;
height: 2.5rem;
background: $gray-100;
}
}
}
&.current-day {
label {
font-weight: 500;
color: $black;
&:after {
content: '';
width: 0.25rem;
height: 0.25rem;
border-radius: 50%;
background-color: $black;
position: absolute;
top: 28px;
}
.first-day {
color: $blue;
}
}
}
}
&.partial-month-initial-view {
.day-hidden {
opacity: 0;
overflow: hidden;
display: flex;
margin: 0;
max-height: 0;
}
}
&.month-hidden {
opacity: 0;
max-height: 0;
margin-bottom: 0;
overflow: hidden;
}
}
.btn-link {
font-weight: 500;
text-underline-offset: 4px;
flex-grow: 0;
&:focus {
box-shadow: none;
}
}
.past {
.calendar-grid-container {
.month-year {
grid-area: 1 / 1 / 2 / 6;
}
.nav-forward {
grid-area: 1 / 7 / 2 / 8;
display: flex;
justify-content: flex-end;
button {
&:after {
content: '';
position: absolute;
width: 7px;
height: 12px;
background-image: url($svg-date-picker-forward-button);
}
}
}
.nav-back {
grid-area: 1 / 6 / 2 / 7;
display: flex;
justify-content: flex-end;
button {
&:after {
content: '';
position: absolute;
width: 7px;
height: 12px;
background-image: url($svg-date-picker-nav-back-button);
}
}
}
.nav-back,
.nav-forward {
button {
position: relative;
border-radius: 50%;
width: 28px;
height: 28px;
background-color: $blue-100;
border: 1px solid $blue;
display: flex;
justify-content: center;
align-items: center;
&:focus,
&:focus-visible {
border: 2.5px solid $blue;
box-shadow: none;
background-color: $blue-100;
color: $white;
}
}
}
}
}
}
.btn-link {
display: block;
position: relative;
height: 2rem;
width: 100%;
justify-content: center;
background: transparent;
border: none;
font-weight: 500;
letter-spacing: inherit;
text-underline-offset: 4px;
z-index: 2;
}
.form-test-error {
margin: 0 auto;
max-width: 414px;
text-align: left;
}
</style>