Skip to content

Commit

Permalink
feat: Add support for calendar event indicator. (#5002)
Browse files Browse the repository at this point in the history
  • Loading branch information
dbatiste authored Sep 25, 2024
1 parent 32651db commit 5d5a2d5
Show file tree
Hide file tree
Showing 27 changed files with 167 additions and 16 deletions.
88 changes: 75 additions & 13 deletions components/calendar/calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,26 @@ function getCalendarData() {
return calendarData;
}

function getInitialFocusDate(selectedValue, minValue, maxValue) {
if (selectedValue) return getDateFromISODate(selectedValue);
else return getDateFromISODate(getClosestValidDate(minValue, maxValue, false));
}

export function checkIfDatesEqual(date1, date2) {
if (!date1 || !date2) return false;
return date1.getTime() === date2.getTime();
}

export function getMinMaxDatesInView(selectedValue, minValue, maxValue) {
getCalendarData();
const date = getInitialFocusDate(selectedValue, minValue, maxValue);
const dates = getDatesInMonthArray(date.getMonth(), date.getFullYear());
return {
maxValue: dates[dates.length - 1][6],
minValue: dates[0][0]
};
}

export function getDatesInMonthArray(shownMonth, shownYear) {
const dates = [];
const numDays = getNumberOfDaysInMonth(shownMonth, shownYear);
Expand Down Expand Up @@ -135,6 +150,11 @@ class Calendar extends LocalizeCoreElement(RtlMixin(LitElement)) {

static get properties() {
return {
/**
* Additional info for each day (ex. events on [{"date": "2024-09-19"}])
* @type {Array}
*/
dayInfos: { type: Array, attribute: 'day-infos' },
/**
* Unique label text for calendar (necessary if multiple calendars on page)
* @type {string}
Expand All @@ -145,19 +165,16 @@ class Calendar extends LocalizeCoreElement(RtlMixin(LitElement)) {
* @type {string}
*/
maxValue: { attribute: 'max-value', reflect: true, type: String },

/**
* Minimum valid date that could be selected by a user
* @type {string}
*/
minValue: { attribute: 'min-value', reflect: true, type: String },

/**
* Currently selected date
* @type {string}
*/
selectedValue: { type: String, attribute: 'selected-value' },

/**
* ACCESSIBILITY: Summary of the calendar used by screen reader users for identifying the calendar and/or summarizing its purpose
* @type {string}
Expand Down Expand Up @@ -395,6 +412,24 @@ class Calendar extends LocalizeCoreElement(RtlMixin(LitElement)) {
font-size: 1rem;
font-weight: 700;
}
.d2l-calendar-date-day-info::after {
background-color: var(--d2l-color-celestine);
border-radius: 3px;
bottom: 4px;
content: "";
display: inline-block;
height: 6px;
position: absolute;
width: 6px;
}
.d2l-calendar-date-selected.d2l-calendar-date-day-info::after {
bottom: 2px;
}
td:focus .d2l-calendar-date-day-info::after {
bottom: 0;
}
`];
}

Expand Down Expand Up @@ -435,9 +470,7 @@ class Calendar extends LocalizeCoreElement(RtlMixin(LitElement)) {
}

render() {
if (this._shownMonth === undefined || !this._shownYear) {
return nothing;
}
if (this._shownMonth === undefined || !this._shownYear) return nothing;

const weekdayHeaders = calendarData.daysOfWeekIndex.map((index) => html`
<th>
Expand All @@ -450,20 +483,28 @@ class Calendar extends LocalizeCoreElement(RtlMixin(LitElement)) {
const dates = getDatesInMonthArray(this._shownMonth, this._shownYear);
const dayRows = dates.map((week) => {
const weekHtml = week.map((day, index) => {

const disabled = !isDateInRange(day, getDateFromISODate(this.minValue), getDateFromISODate(this.maxValue));
const focused = checkIfDatesEqual(day, this._focusDate);
const selected = this.selectedValue ? checkIfDatesEqual(day, getDateFromISODate(this.selectedValue)) : false;

const year = day.getFullYear();
const month = day.getMonth();
const date = day.getDate();

const hasDayInfo = !!this.dayInfos?.find(dayInfo => checkIfDatesEqual(day, getDateFromISODate(dayInfo.date)));

const classes = {
'd2l-calendar-date': true,
'd2l-calendar-date-day-info': hasDayInfo,
'd2l-calendar-date-initial': this._isInitialFocusDate,
'd2l-calendar-date-selected': selected,
'd2l-calendar-date-today': checkIfDatesEqual(day, this._today)
};
const year = day.getFullYear();
const month = day.getMonth();
const date = day.getDate();

const weekday = calendarData.descriptor.calendar.days.long[calendarData.daysOfWeekIndex[index]];
const description = `${weekday} ${date} ${formatDate(day, { format: 'monthYear' })}`;
const dayInfoText = hasDayInfo ? `${this.localize('components.calendar.hasEvents')} ` : '';
const description = `${dayInfoText}${weekday} ${date} ${formatDate(day, { format: 'monthYear' })}`;
return html`
<td
aria-selected="${selected ? 'true' : 'false'}"
Expand All @@ -487,6 +528,7 @@ class Calendar extends LocalizeCoreElement(RtlMixin(LitElement)) {

return html`<tr>${weekHtml}</tr>`;
});

const summary = this.summary ? html`<caption class="d2l-offscreen">${this.summary}</caption>` : '';
const calendarClasses = {
'd2l-calendar': true,
Expand All @@ -501,6 +543,7 @@ class Calendar extends LocalizeCoreElement(RtlMixin(LitElement)) {
const heading = formatDate(new Date(this._shownYear, this._shownMonth, 1), { format: 'monthYear' });
const regionLabel = this.label ? `${this.label}. ${heading}` : heading;
const role = this._dialog ? 'dialog' : undefined;

return html`
<div role="region" aria-label="${regionLabel}">
<div class="${classMap(calendarClasses)}" role="${ifDefined(role)}">
Expand Down Expand Up @@ -532,6 +575,7 @@ class Calendar extends LocalizeCoreElement(RtlMixin(LitElement)) {
</div>
</div>
`;

}

updated(changedProperties) {
Expand All @@ -551,6 +595,26 @@ class Calendar extends LocalizeCoreElement(RtlMixin(LitElement)) {
});
}

willUpdate(changedProperties) {
super.willUpdate(changedProperties);

// don't dispatch d2l-calendar-view-change when _shownYear and _shownMonth are being initialized
if (changedProperties.get('_shownYear') === undefined && changedProperties.get('_shownMonth') === undefined) return;

const dates = getDatesInMonthArray(this._shownMonth, this._shownYear);

/** Dispatched when the calender view changes. "e.detail" provides the year and month in view. */
this.dispatchEvent(new CustomEvent('d2l-calendar-view-change', {
bubbles: false,
composed: false,
detail: {
maxValue: dates[dates.length - 1][6],
minValue: dates[0][0]
}
}));

}

async focus() {
if (this._dialog) {
await this.updateComplete;
Expand Down Expand Up @@ -585,9 +649,7 @@ class Calendar extends LocalizeCoreElement(RtlMixin(LitElement)) {
}

_getInitialFocusDate() {
let date;
if (this.selectedValue) date = getDateFromISODate(this.selectedValue);
else date = getDateFromISODate(getClosestValidDate(this.minValue, this.maxValue, false));
const date = getInitialFocusDate(this.selectedValue, this.minValue, this.maxValue);
this._shownMonth = date.getMonth();
this._shownYear = date.getFullYear();
return date;
Expand Down
35 changes: 35 additions & 0 deletions components/calendar/demo/calendar.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@
<script type="module">
import '../../demo/demo-page.js';
import '../calendar.js';

window._allEvents = [
{ date: '2024-08-21' },
{ date: '2024-08-22' },
{ date: '2024-09-04' },
{ date: '2024-09-19' },
{ date: '2024-09-20' },
{ date: '2024-09-20' },
{ date: '2024-09-28' },
{ date: '2024-09-30' },
{ date: '2024-10-02' },
{ date: '2024-10-30' },
{ date: '2024-10-31' },
{ date: '2024-11-01' },
{ date: '2024-11-11' }
];
</script>
</head>
<body unresolved>
Expand All @@ -33,6 +49,25 @@ <h2>Calendar (min and max value)</h2>
</template>
</d2l-demo-snippet>

<h2>Calendar (with events)</h2>
<d2l-demo-snippet>
<template>
<d2l-calendar id="eventsCalendar" selected-value="2024-09-16"></d2l-calendar>
<script type="module">
import { getDateFromISODate, isDateInRange } from '../../../helpers/dateTime.js';
import { getMinMaxDatesInView } from '../calendar.js';

const getEvents = datesInView => {
return window._allEvents.filter(event => isDateInRange(getDateFromISODate(event.date), datesInView.minValue, datesInView.maxValue));
};

const calendar = document.querySelector('#eventsCalendar');
calendar.dayInfos = getEvents(getMinMaxDatesInView(calendar.selectedValue));
calendar.addEventListener('d2l-calendar-view-change', e => calendar.dayInfos = getEvents(e.detail));
</script>
</template>
</d2l-demo-snippet>

</d2l-demo-page>
</body>
</html>
31 changes: 30 additions & 1 deletion components/calendar/test/calendar.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { aTimeout, expect, fixture, html, oneEvent, runConstructor, waitUntil } from '@brightspace-ui/testing';
import { aTimeout, clickElem, expect, fixture, html, oneEvent, runConstructor, waitUntil } from '@brightspace-ui/testing';
import { checkIfDatesEqual,
getDatesInMonthArray,
getNextMonth,
Expand All @@ -23,6 +23,7 @@ describe('d2l-calendar', () => {
});

describe('events', () => {

it('dispatches event when date clicked', async() => {
const calendar = await fixture(normalFixture);
const el = calendar.shadowRoot.querySelector('td[data-date="1"] button');
Expand Down Expand Up @@ -92,6 +93,34 @@ describe('d2l-calendar', () => {
const expectedFocusDate = new Date(2015, 8, 2);
expect(calendar._focusDate).to.deep.equal(expectedFocusDate);
});

it('dispatches d2l-calendar-view-change event when user changes to previous month', async() => {
const calendar = await fixture(normalFixture);
const el = calendar.shadowRoot.querySelectorAll('d2l-button-icon')[0];
clickElem(el);
const { detail } = await oneEvent(calendar, 'd2l-calendar-view-change');
expect(detail.minValue).to.deep.equal(new Date(2015, 6, 26));
expect(detail.maxValue).to.deep.equal(new Date(2015, 8, 5));
});

it('dispatches d2l-calendar-view-change event when user changes to next month', async() => {
const calendar = await fixture(normalFixture);
const el = calendar.shadowRoot.querySelectorAll('d2l-button-icon')[1];
clickElem(el);
const { detail } = await oneEvent(calendar, 'd2l-calendar-view-change');
expect(detail.minValue).to.deep.equal(new Date(2015, 8, 27));
expect(detail.maxValue).to.deep.equal(new Date(2015, 9, 31));
});

it('does not dispatch d2l-calendar-view-change event initially', async() => {
let dispatched = false;
const calendar = document.createElement('d2l-calendar');
calendar.addEventListener('d2l-calendar-view-change', () => dispatched = true);
document.body.appendChild(calendar);
await calendar.updateComplete;
expect(dispatched).to.equal(false);
});

});

describe('focus date', () => {
Expand Down
10 changes: 8 additions & 2 deletions components/calendar/test/calendar.vdiff.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ sinon.useFakeTimers({ now: newToday.getTime(), toFake: ['Date'] });

const simpleTemplate = html`<d2l-calendar selected-value="2018-02-14"></d2l-calendar>`;
const minMaxTemplate = html`<d2l-calendar min-value="2018-01-31" max-value="2018-02-27" selected-value="2018-02-14"></d2l-calendar>`;
const eventsTemplate = html`<d2l-calendar selected-value="2018-02-14" day-infos="[{&quot;date&quot;:&quot;2018-01-29&quot;},{&quot;date&quot;:&quot;2018-02-12&quot;},{&quot;date&quot;:&quot;2018-02-13&quot;},{&quot;date&quot;:&quot;2018-02-14&quot;},{&quot;date&quot;:&quot;2018-02-15&quot;},{&quot;date&quot;:&quot;2018-03-03&quot;}]"></d2l-calendar>`;

describe('calendar', () => {
let elem;
Expand All @@ -21,10 +22,15 @@ describe('calendar', () => {
{ name: 'min-max', template: minMaxTemplate },
{ name: 'min-max-no-selected', template: html`<d2l-calendar min-value="2017-08-31" max-value="2017-10-27" ></d2l-calendar>` },
{ name: 'no-selected', template: html`<d2l-calendar></d2l-calendar>` },
{ name: 'today-selected', template: html`<d2l-calendar selected-value="2018-02-12"></d2l-calendar>` }
].forEach(({ name, template }) => {
{ name: 'today-selected', template: html`<d2l-calendar selected-value="2018-02-12"></d2l-calendar>` },
{ name: 'day-infos', template: eventsTemplate },
{ name: 'day-infos-focus', template: eventsTemplate, action: () => focusElem(elem.shadowRoot.querySelector('td[data-date="29"]')) },
{ name: 'day-infos-today-focus', template: eventsTemplate, action: () => focusElem(elem.shadowRoot.querySelector('td[data-date="12"]')) },
{ name: 'day-infos-selected-focus', template: eventsTemplate, action: () => focusElem(elem.shadowRoot.querySelector('td[data-date="14"]')) }
].forEach(({ name, template, action }) => {
it(name, async() => {
await setupFixture(template);
if (action) await action();
await expect(elem).to.be.golden();
});
});
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions lang/ar.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default {
"components.alert.close": "إغلاق التنبيه",
"components.breadcrumbs.breadcrumb": "شريط التنقل",
"components.button-add.addItem": "إضافة عنصر",
"components.calendar.hasEvents": "Has Events.",
"components.calendar.notSelected": "لم يتم التحديد.",
"components.calendar.selected": "تم التحديد.",
"components.calendar.show": "إظهار {month}",
Expand Down
1 change: 1 addition & 0 deletions lang/cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default {
"components.alert.close": "Cau Hysbysiad",
"components.breadcrumbs.breadcrumb": "Briwsionyn Bara",
"components.button-add.addItem": "Ychwanegu Eitem",
"components.calendar.hasEvents": "Has Events.",
"components.calendar.notSelected": "Heb ei Ddewis.",
"components.calendar.selected": "Wedi'i Ddewis.",
"components.calendar.show": "Dangos {month}",
Expand Down
1 change: 1 addition & 0 deletions lang/da.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default {
"components.alert.close": "Luk besked",
"components.breadcrumbs.breadcrumb": "Brødkrumme",
"components.button-add.addItem": "Tilføj element",
"components.calendar.hasEvents": "Has Events.",
"components.calendar.notSelected": "Ikke valgt.",
"components.calendar.selected": "Valgt.",
"components.calendar.show": "Vis {month}",
Expand Down
1 change: 1 addition & 0 deletions lang/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default {
"components.alert.close": "Benachrichtigung schließen",
"components.breadcrumbs.breadcrumb": "Brotkrümelnavigation",
"components.button-add.addItem": "Element hinzufügen",
"components.calendar.hasEvents": "Has Events.",
"components.calendar.notSelected": "Nicht ausgewählt.",
"components.calendar.selected": "Ausgewählt.",
"components.calendar.show": "{month} anzeigen",
Expand Down
1 change: 1 addition & 0 deletions lang/en-gb.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default {
"components.alert.close": "Close Alert",
"components.breadcrumbs.breadcrumb": "Breadcrumb",
"components.button-add.addItem": "Add Item",
"components.calendar.hasEvents": "Has Events.",
"components.calendar.notSelected": "Not Selected.",
"components.calendar.selected": "Selected.",
"components.calendar.show": "Show {month}",
Expand Down
1 change: 1 addition & 0 deletions lang/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default {
"components.alert.close": "Close Alert",
"components.breadcrumbs.breadcrumb": "Breadcrumb",
"components.button-add.addItem": "Add Item",
"components.calendar.hasEvents": "Has Events.",
"components.calendar.notSelected": "Not Selected.",
"components.calendar.selected": "Selected.",
"components.calendar.show": "Show {month}",
Expand Down
1 change: 1 addition & 0 deletions lang/es-es.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default {
"components.alert.close": "Cerrar alerta",
"components.breadcrumbs.breadcrumb": "Ruta de navegación",
"components.button-add.addItem": "Agregar elemento",
"components.calendar.hasEvents": "Has Events.",
"components.calendar.notSelected": "No seleccionado.",
"components.calendar.selected": "Seleccionado.",
"components.calendar.show": "Mostrar {month}",
Expand Down
1 change: 1 addition & 0 deletions lang/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default {
"components.alert.close": "Cerrar alerta",
"components.breadcrumbs.breadcrumb": "Ruta de navegación",
"components.button-add.addItem": "Agregar elemento",
"components.calendar.hasEvents": "Has Events.",
"components.calendar.notSelected": "No seleccionado.",
"components.calendar.selected": "Seleccionado.",
"components.calendar.show": "Mostrar {month}",
Expand Down
1 change: 1 addition & 0 deletions lang/fr-fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default {
"components.alert.close": "Fermer l'alerte",
"components.breadcrumbs.breadcrumb": "Chemin de navigation",
"components.button-add.addItem": "Ajouter un élément",
"components.calendar.hasEvents": "Has Events.",
"components.calendar.notSelected": "Non sélectionné.",
"components.calendar.selected": "Sélectionné.",
"components.calendar.show": "Afficher {month}",
Expand Down
1 change: 1 addition & 0 deletions lang/fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default {
"components.alert.close": "Fermer l'alerte",
"components.breadcrumbs.breadcrumb": "Chemin de navigation",
"components.button-add.addItem": "Ajouter un élément",
"components.calendar.hasEvents": "Has Events.",
"components.calendar.notSelected": "Non sélectionné(e)",
"components.calendar.selected": "Sélectionné(e).",
"components.calendar.show": "Afficher {month}",
Expand Down
Loading

0 comments on commit 5d5a2d5

Please sign in to comment.