From a10490a21890b9a389c7a24b292e3caa9091f2e9 Mon Sep 17 00:00:00 2001 From: Thomas Allmer Date: Wed, 12 May 2021 17:37:56 +0200 Subject: [PATCH] fix(calendar): focusable disabled dates --- docs/components/inputs/calendar/features.md | 39 ++ packages/calendar/src/LionCalendar.js | 150 ++++---- packages/calendar/src/calendarStyle.js | 26 +- packages/calendar/src/utils/dayTemplate.js | 11 +- packages/calendar/test-helpers/DayObject.js | 4 +- packages/calendar/test/lion-calendar.test.js | 241 ++++++------- .../calendar/test/utils/dayTemplate.test.js | 10 +- .../monthTemplate_en-GB_Sunday_2018-12.js | 336 +++++++++++------- 8 files changed, 492 insertions(+), 325 deletions(-) diff --git a/docs/components/inputs/calendar/features.md b/docs/components/inputs/calendar/features.md index 470c2edf4a..ef3e7db5e2 100644 --- a/docs/components/inputs/calendar/features.md +++ b/docs/components/inputs/calendar/features.md @@ -190,3 +190,42 @@ export const combinedDisabledDates = () => { `; }; ``` + +### Finding enabled dates + +The next available date may be multiple days/month in the future/past. +For that we offer convenient helpers as + +- `findNextEnabledDate()` +- `findPreviousEnabledDate()` +- `findNearestEnabledDate()` + +```js preview-story +export const findingEnabledDates = () => { + function getCalendar(ev) { + return ev.target.parentElement.querySelector('.js-calendar'); + } + return html` + + day.getDay() === 6 || day.getDay() === 0} + > + + + + `; +}; +``` diff --git a/packages/calendar/src/LionCalendar.js b/packages/calendar/src/LionCalendar.js index 5d724d43d2..4758f17852 100644 --- a/packages/calendar/src/LionCalendar.js +++ b/packages/calendar/src/LionCalendar.js @@ -23,6 +23,17 @@ import { isSameDate } from './utils/isSameDate.js'; * @typedef {import('../types/day').Month} Month */ +const isDayButton = /** @param {HTMLElement} el */ el => + el.classList.contains('calendar__day-button'); + +/** + * @param {HTMLElement} el + * @returns {boolean} + */ +function isDisabledDayButton(el) { + return el.getAttribute('aria-disabled') === 'true'; +} + /** * @customElement lion-calendar */ @@ -199,19 +210,19 @@ export class LionCalendar extends LocalizeMixin(LitElement) { } goToNextMonth() { - this.__modifyDate(1, { dateType: 'centralDate', type: 'Month', mode: 'both' }); + this.__modifyDate(1, { dateType: 'centralDate', type: 'Month' }); } goToPreviousMonth() { - this.__modifyDate(-1, { dateType: 'centralDate', type: 'Month', mode: 'both' }); + this.__modifyDate(-1, { dateType: 'centralDate', type: 'Month' }); } goToNextYear() { - this.__modifyDate(1, { dateType: 'centralDate', type: 'FullYear', mode: 'both' }); + this.__modifyDate(1, { dateType: 'centralDate', type: 'FullYear' }); } goToPreviousYear() { - this.__modifyDate(-1, { dateType: 'centralDate', type: 'FullYear', mode: 'both' }); + this.__modifyDate(-1, { dateType: 'centralDate', type: 'FullYear' }); } /** @@ -224,9 +235,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) { } focusCentralDate() { - const button = /** @type {HTMLElement} */ (this.shadowRoot?.querySelector( - 'button[tabindex="0"]', - )); + const button = /** @type {HTMLElement} */ (this.shadowRoot?.querySelector('[tabindex="0"]')); button.focus(); this.__focusedDate = this.centralDate; } @@ -308,13 +317,8 @@ export class LionCalendar extends LocalizeMixin(LitElement) { requestUpdateInternal(name, oldValue) { super.requestUpdateInternal(name, oldValue); - const map = { - disableDates: () => this.__disableDatesChanged(), - centralDate: () => this.__centralDateChanged(), - __focusedDate: () => this.__focusedDateChanged(), - }; - if (map[name]) { - map[name](); + if (name === '__focusedDate') { + this.__focusedDateChanged(); } const updateDataOn = ['centralDate', 'minDate', 'maxDate', 'selectedDate', 'disableDates']; @@ -342,10 +346,8 @@ export class LionCalendar extends LocalizeMixin(LitElement) { */ __calculateInitialCentralDate() { if (this.centralDate === this.__today && this.selectedDate) { - // initialised with selectedDate only if user didn't provide another one + // initialized with selectedDate only if user didn't provide another one this.centralDate = this.selectedDate; - } else { - this.__ensureValidCentralDate(); } /** @type {Date} */ this.__initialCentralDate = this.centralDate; @@ -611,15 +613,6 @@ export class LionCalendar extends LocalizeMixin(LitElement) { return data; } - /** - * @private - */ - __disableDatesChanged() { - if (this.__connectedCallbackDone) { - this.__ensureValidCentralDate(); - } - } - /** * @param {Date} selectedDate * @private @@ -636,15 +629,6 @@ export class LionCalendar extends LocalizeMixin(LitElement) { ); } - /** - * @private - */ - __centralDateChanged() { - if (this.__connectedCallbackDone) { - this.__ensureValidCentralDate(); - } - } - /** * @private */ @@ -654,15 +638,6 @@ export class LionCalendar extends LocalizeMixin(LitElement) { } } - /** - * @private - */ - __ensureValidCentralDate() { - if (!this.__isEnabledDate(this.centralDate)) { - this.centralDate = this.__findBestEnabledDateFor(this.centralDate); - } - } - /** * @param {Date} date * @private @@ -715,16 +690,40 @@ export class LionCalendar extends LocalizeMixin(LitElement) { ); } + /** + * @param {Date} [date] + * @returns + */ + findNextEnabledDate(date) { + const _date = date || this.centralDate; + return this.__findBestEnabledDateFor(_date, { mode: 'future' }); + } + + /** + * @param {Date} [date] + * @returns + */ + findPreviousEnabledDate(date) { + const _date = date || this.centralDate; + return this.__findBestEnabledDateFor(_date, { mode: 'past' }); + } + + /** + * @param {Date} [date] + * @returns + */ + findNearestEnabledDate(date) { + const _date = date || this.centralDate; + return this.__findBestEnabledDateFor(_date, { mode: 'both' }); + } + /** * @param {Event} ev * @private */ __clickDateDelegation(ev) { - const isDayButton = /** @param {HTMLElement} el */ el => - el.classList.contains('calendar__day-button'); - const el = /** @type {HTMLElement & { date: Date }} */ (ev.target); - if (isDayButton(el)) { + if (isDayButton(el) && !isDisabledDayButton(el)) { this.__dateSelectedByUser(el.date); } } @@ -733,9 +732,6 @@ export class LionCalendar extends LocalizeMixin(LitElement) { * @private */ __focusDateDelegation() { - const isDayButton = /** @param {HTMLElement} el */ el => - el.classList.contains('calendar__day-button'); - if ( !this.__focusedDate && isDayButton(/** @type {HTMLElement} el */ (this.shadowRoot?.activeElement)) @@ -749,9 +745,6 @@ export class LionCalendar extends LocalizeMixin(LitElement) { * @private */ __blurDateDelegation() { - const isDayButton = /** @param {HTMLElement} el */ el => - el.classList.contains('calendar__day-button'); - setTimeout(() => { if ( this.shadowRoot?.activeElement && @@ -762,42 +755,65 @@ export class LionCalendar extends LocalizeMixin(LitElement) { }, 1); } + /** + * @param {HTMLElement & { date: Date }} el + * @private + */ + __dayButtonSelection(el) { + if (isDayButton(el)) { + this.__dateSelectedByUser(el.date); + } + } + /** * @param {KeyboardEvent} ev * @private */ __keyboardNavigationEvent(ev) { - const preventedKeys = ['ArrowUp', 'ArrowDown', 'PageDown', 'PageUp']; + const preventedKeys = [ + 'ArrowLeft', + 'ArrowUp', + 'ArrowRight', + 'ArrowDown', + 'PageDown', + 'PageUp', + ' ', + 'Enter', + ]; if (preventedKeys.includes(ev.key)) { ev.preventDefault(); } switch (ev.key) { + case ' ': + case 'Enter': + this.__dayButtonSelection(/** @type {HTMLElement & { date: Date }} */ (ev.target)); + break; case 'ArrowUp': - this.__modifyDate(-7, { dateType: '__focusedDate', type: 'Date', mode: 'past' }); + this.__modifyDate(-7, { dateType: '__focusedDate', type: 'Date' }); break; case 'ArrowDown': - this.__modifyDate(7, { dateType: '__focusedDate', type: 'Date', mode: 'future' }); + this.__modifyDate(7, { dateType: '__focusedDate', type: 'Date' }); break; case 'ArrowLeft': - this.__modifyDate(-1, { dateType: '__focusedDate', type: 'Date', mode: 'past' }); + this.__modifyDate(-1, { dateType: '__focusedDate', type: 'Date' }); break; case 'ArrowRight': - this.__modifyDate(1, { dateType: '__focusedDate', type: 'Date', mode: 'future' }); + this.__modifyDate(1, { dateType: '__focusedDate', type: 'Date' }); break; case 'PageDown': if (ev.altKey === true) { - this.__modifyDate(1, { dateType: '__focusedDate', type: 'FullYear', mode: 'future' }); + this.__modifyDate(1, { dateType: '__focusedDate', type: 'FullYear' }); } else { - this.__modifyDate(1, { dateType: '__focusedDate', type: 'Month', mode: 'future' }); + this.__modifyDate(1, { dateType: '__focusedDate', type: 'Month' }); } break; case 'PageUp': if (ev.altKey === true) { - this.__modifyDate(-1, { dateType: '__focusedDate', type: 'FullYear', mode: 'past' }); + this.__modifyDate(-1, { dateType: '__focusedDate', type: 'FullYear' }); } else { - this.__modifyDate(-1, { dateType: '__focusedDate', type: 'Month', mode: 'past' }); + this.__modifyDate(-1, { dateType: '__focusedDate', type: 'Month' }); } break; case 'Tab': @@ -813,11 +829,10 @@ export class LionCalendar extends LocalizeMixin(LitElement) { * @param {Object} opts * @param {string} opts.dateType * @param {string} opts.type - * @param {string} opts.mode * @private */ - __modifyDate(modify, { dateType, type, mode }) { - let tmpDate = new Date(this.centralDate); + __modifyDate(modify, { dateType, type }) { + const tmpDate = new Date(this.centralDate); // if we're not working with days, reset // day count to first day of the month if (type !== 'Date') { @@ -830,9 +845,6 @@ export class LionCalendar extends LocalizeMixin(LitElement) { const maxDays = new Date(tmpDate.getFullYear(), tmpDate.getMonth() + 1, 0).getDate(); tmpDate.setDate(Math.min(this.centralDate.getDate(), maxDays)); } - if (!this.__isEnabledDate(tmpDate)) { - tmpDate = this.__findBestEnabledDateFor(tmpDate, { mode }); - } this[dateType] = tmpDate; } diff --git a/packages/calendar/src/calendarStyle.js b/packages/calendar/src/calendarStyle.js index 7eb16cad8c..f1a981c0e9 100644 --- a/packages/calendar/src/calendarStyle.js +++ b/packages/calendar/src/calendarStyle.js @@ -54,6 +54,16 @@ export const calendarStyle = css` padding: 0; min-width: 40px; min-height: 40px; + /** give div[role=button][aria-disabled] same display type as native btn */ + display: inline-flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + } + + .calendar__day-button:focus { + border: 1px solid blue; + outline: none; } .calendar__day-button__text { @@ -77,9 +87,23 @@ export const calendarStyle = css` border: 1px solid green; } - .calendar__day-button[disabled] { + .calendar__day-button[aria-disabled='true'] { background-color: #fff; color: #eee; outline: none; } + + .u-sr-only { + position: absolute; + top: 0; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(100%); + clip: rect(1px, 1px, 1px, 1px); + white-space: nowrap; + border: 0; + margin: 0; + padding: 0; + } `; diff --git a/packages/calendar/src/utils/dayTemplate.js b/packages/calendar/src/utils/dayTemplate.js index d961c0f5bc..30aba965ab 100644 --- a/packages/calendar/src/utils/dayTemplate.js +++ b/packages/calendar/src/utils/dayTemplate.js @@ -56,14 +56,14 @@ export function dayTemplate(day, { weekdays, monthsLabels = defaultMonthLabels } ?start-of-last-week=${startOfLastWeek} ?last-day=${lastDay} > - + ${dayNumber} + ${`${monthName} ${year} ${weekdayName}`} + `; } diff --git a/packages/calendar/test-helpers/DayObject.js b/packages/calendar/test-helpers/DayObject.js index 7967d2f4d9..fe3b80f149 100644 --- a/packages/calendar/test-helpers/DayObject.js +++ b/packages/calendar/test-helpers/DayObject.js @@ -34,7 +34,7 @@ export class DayObject { */ get isDisabled() { - return this.buttonEl.hasAttribute('disabled'); + return this.buttonEl.getAttribute('aria-disabled') === 'true'; } get isSelected() { @@ -54,7 +54,7 @@ export class DayObject { } get monthday() { - return Number(this.buttonEl.textContent); + return Number(this.buttonEl.children[0].textContent); } /** diff --git a/packages/calendar/test/lion-calendar.test.js b/packages/calendar/test/lion-calendar.test.js index 300f604a91..67ea82a8a5 100644 --- a/packages/calendar/test/lion-calendar.test.js +++ b/packages/calendar/test/lion-calendar.test.js @@ -131,12 +131,6 @@ describe('', () => { /** @param {number} n */ n => n === 5, ), ).to.be.true; - expect( - elObj.checkForAllDayObjs( - /** @param {DayObject} o */ o => o.buttonEl.getAttribute('tabindex') === '-1', - /** @param {number} n */ n => n !== 5, - ), - ).to.be.true; }); it('has property "selectedDate" for the selected date', async () => { @@ -395,7 +389,7 @@ describe('', () => { clock.restore(); }); - it('should set centralDate to the unique valid value when minDate and maxDate are equal', async () => { + it('requires the user to set an appropriate centralDate even when minDate and maxDate are equal', async () => { const clock = sinon.useFakeTimers({ now: new Date('2019/06/03').getTime() }); const el = await fixture(html` @@ -404,9 +398,19 @@ describe('', () => { .maxDate="${new Date('2019/07/03')}" > `); - expect(isSameDate(el.centralDate, new Date('2019/07/03')), 'central date').to.be.true; + + const elSetting = await fixture(html` + + `); clock.restore(); + expect(isSameDate(el.centralDate, new Date('2019/06/03')), 'central date').to.be.true; + expect(isSameDate(elSetting.centralDate, new Date('2019/07/03')), 'central date').to.be + .true; }); describe('Normalization', () => { @@ -481,6 +485,98 @@ describe('', () => { }); describe('Navigation', () => { + describe('finding enabled dates', () => { + it('has helper for `findNextEnabledDate()`, `findPreviousEnabledDate()`, `findNearestEnabledDate()`', async () => { + const el = await fixture(html` + date.getDate() === 3 || date.getDate() === 4 + } + > + `); + const elObj = new CalendarObject(el); + + el.focusDate(el.findNextEnabledDate()); + await el.updateComplete; + expect(elObj.focusedDayObj?.monthday).to.equal(5); + + el.focusDate(el.findPreviousEnabledDate()); + await el.updateComplete; + expect(elObj.focusedDayObj?.monthday).to.equal(2); + + el.focusDate(el.findNearestEnabledDate()); + await el.updateComplete; + expect(elObj.focusedDayObj?.monthday).to.equal(1); + }); + + it('future dates take precedence over past dates when "distance" between dates is equal', async () => { + const clock = sinon.useFakeTimers({ now: new Date('2000/12/15').getTime() }); + + const el = await fixture(html` + + `); + el.focusDate(el.findNearestEnabledDate()); + await el.updateComplete; + + const elObj = new CalendarObject(el); + expect(elObj.centralDayObj?.monthday).to.equal(16); + + clock.restore(); + }); + + it('will search 750 days in the past', async () => { + const clock = sinon.useFakeTimers({ now: new Date('2000/12/15').getTime() }); + + const el = await fixture(html` + + `); + el.focusDate(el.findNearestEnabledDate()); + await el.updateComplete; + + expect(el.centralDate.getFullYear()).to.equal(1998); + expect(el.centralDate.getMonth()).to.equal(11); + expect(el.centralDate.getDate()).to.equal(31); + + clock.restore(); + }); + + it('will search 750 days in the future', async () => { + const clock = sinon.useFakeTimers({ now: new Date('2000/12/15').getTime() }); + + const el = await fixture(html` + + `); + + el.focusDate(el.findNearestEnabledDate()); + await el.updateComplete; + + expect(el.centralDate.getFullYear()).to.equal(2002); + expect(el.centralDate.getMonth()).to.equal(0); + expect(el.centralDate.getDate()).to.equal(1); + + clock.restore(); + }); + + it('throws if no available date can be found within +/- 750 days', async () => { + const el = await fixture(html` + + `); + + expect(() => { + el.findNextEnabledDate(new Date('1900/01/01')); + }).to.throw(Error, 'Could not find a selectable date within +/- 750 day for 1900/1/1'); + }); + }); + describe('Year', () => { it('has a button for navigation to previous year', async () => { const el = await fixture( @@ -654,7 +750,7 @@ describe('', () => { await el.updateComplete; expect(elObj.activeMonth).to.equal('November'); expect(elObj.activeYear).to.equal('2000'); - expect(isSameDate(el.centralDate, new Date('2000/11/20'))).to.be.true; + expect(isSameDate(el.centralDate, new Date('2000/11/15'))).to.be.true; clock.restore(); }); @@ -676,7 +772,7 @@ describe('', () => { await el.updateComplete; expect(elObj.activeMonth).to.equal('January'); expect(elObj.activeYear).to.equal('2001'); - expect(isSameDate(el.centralDate, new Date('2001/01/10'))).to.be.true; + expect(isSameDate(el.centralDate, new Date('2001/01/15'))).to.be.true; clock.restore(); }); @@ -695,17 +791,21 @@ describe('', () => { expect(remote.activeMonth).to.equal('September'); expect(remote.activeYear).to.equal('2019'); expect(remote.centralDayObj?.el).dom.to.equal(` - + + September 2019 Monday + + `); }); }); @@ -800,7 +900,7 @@ describe('', () => { ).to.equal(true); }); - it('adds "disabled" attribute to disabled dates', async () => { + it('adds aria-disabled="true" attribute to disabled dates', async () => { const clock = sinon.useFakeTimers({ now: new Date('2000/12/15').getTime() }); const el = await fixture(html` @@ -813,12 +913,10 @@ describe('', () => { `); const elObj = new CalendarObject(el); expect( - elObj.checkForAllDayObjs(/** @param {DayObject} d */ d => d.el.hasAttribute('disabled'), [ - 1, - 2, - 30, - 31, - ]), + elObj.checkForAllDayObjs( + /** @param {DayObject} d */ d => d.el.getAttribute('aria-disabled') === 'true', + [1, 2, 30, 31], + ), ).to.equal(true); clock.restore(); @@ -973,7 +1071,7 @@ describe('', () => { expect(elObj.focusedDayObj?.monthday).to.equal(12 + 1); }); - it('navigates (sets focus) to next selectable column item via [arrow right] key', async () => { + it('navigates (sets focus) to next column item via [arrow right] key', async () => { const el = await fixture(html` ', () => { new KeyboardEvent('keydown', { key: 'ArrowRight' }), ); await el.updateComplete; - expect(elObj.focusedDayObj?.monthday).to.equal(5); + expect(elObj.focusedDayObj?.monthday).to.equal(3); }); it('navigates (sets focus) to next row via [arrow right] key if last item in row', async () => { @@ -1109,77 +1207,6 @@ describe('', () => { clock.restore(); }); - - it('is on day closest to today, if today (and surrounding dates) is/are disabled', async () => { - const el = await fixture(html` - - `); - const elObj = new CalendarObject(el); - expect(elObj.centralDayObj?.monthday).to.equal(17); - - el.disableDates = d => d.getDate() >= 12; - await el.updateComplete; - expect(elObj.centralDayObj?.monthday).to.equal(11); - }); - - it('future dates take precedence over past dates when "distance" between dates is equal', async () => { - const clock = sinon.useFakeTimers({ now: new Date('2000/12/15').getTime() }); - - const el = await fixture(html` - - `); - const elObj = new CalendarObject(el); - expect(elObj.centralDayObj?.monthday).to.equal(16); - - clock.restore(); - }); - - it('will search 750 days in the past', async () => { - const clock = sinon.useFakeTimers({ now: new Date('2000/12/15').getTime() }); - - const el = await fixture(html` - - `); - expect(el.centralDate.getFullYear()).to.equal(1998); - expect(el.centralDate.getMonth()).to.equal(11); - expect(el.centralDate.getDate()).to.equal(31); - - clock.restore(); - }); - - it('will search 750 days in the future', async () => { - const clock = sinon.useFakeTimers({ now: new Date('2000/12/15').getTime() }); - - const el = await fixture(html` - - `); - expect(el.centralDate.getFullYear()).to.equal(2002); - expect(el.centralDate.getMonth()).to.equal(0); - expect(el.centralDate.getDate()).to.equal(1); - - clock.restore(); - }); - - it('throws if no available date can be found within +/- 750 days', async () => { - const el = await fixture(html` - - `); - - expect(() => { - el.centralDate = new Date('1900/01/01'); - }).to.throw(Error, 'Could not find a selectable date within +/- 750 day for 1900/1/1'); - }); }); /** @@ -1227,7 +1254,8 @@ describe('', () => { it('renders each day as a button inside a table cell', async () => { const elObj = new CalendarObject(await fixture(html``)); - const hasBtn = /** @param {DayObject} d */ d => d.el.tagName === 'BUTTON'; + const hasBtn = /** @param {DayObject} d */ d => + d.el.tagName === 'DIV' && d.el.getAttribute('role') === 'button'; expect(elObj.checkForAllDayObjs(hasBtn)).to.equal(true); }); @@ -1302,31 +1330,6 @@ describe('', () => { expect(elObj.checkForAllDayObjs(hasAriaPressed, [12])).to.equal(true); }); - // This implementation mentions "button" inbetween and doesn't mention table - // column and row. As an alternative, see Deque implementation below. - // it(`on focus on a day, the screen reader pronounces "day of the week", "day number" - // and "month" (in this order)', async () => { - // // implemented by labelelledby referencing row and column names - // const el = await fixture(''); - // }); - - // Alternative: Deque implementation - it(`sets aria-label on button, that consists of - "{day number} {month name} {year} {weekday name}"`, async () => { - const elObj = new CalendarObject( - await fixture(html` - - `), - ); - expect( - elObj.checkForAllDayObjs( - /** @param {DayObject} d */ d => - d.buttonEl.getAttribute('aria-label') === - `${d.monthday} November 2000 ${d.weekdayNameLong}`, - ), - ).to.equal(true); - }); - /** * Not in scope: * - reads the new focused day on month navigation" diff --git a/packages/calendar/test/utils/dayTemplate.test.js b/packages/calendar/test/utils/dayTemplate.test.js index 47387baabc..d8f1a03362 100644 --- a/packages/calendar/test/utils/dayTemplate.test.js +++ b/packages/calendar/test/utils/dayTemplate.test.js @@ -11,14 +11,18 @@ describe('dayTemplate', () => { const el = await fixture(dayTemplate(day, { weekdays })); expect(el).dom.to.equal(` - + + April 2019 Friday + + `); }); diff --git a/packages/calendar/test/utils/snapshots/monthTemplate_en-GB_Sunday_2018-12.js b/packages/calendar/test/utils/snapshots/monthTemplate_en-GB_Sunday_2018-12.js index 42ed39f0bc..5bf45b5f57 100644 --- a/packages/calendar/test/utils/snapshots/monthTemplate_en-GB_Sunday_2018-12.js +++ b/packages/calendar/test/utils/snapshots/monthTemplate_en-GB_Sunday_2018-12.js @@ -52,434 +52,518 @@ export default html` - + November 2018 Sunday + - + November 2018 Monday + - + November 2018 Tuesday + - + November 2018 Wednesday + - + November 2018 Thursday + - + November 2018 Friday + - + December 2018 Saturday + - + December 2018 Sunday + - + December 2018 Monday + - + December 2018 Tuesday + - + December 2018 Wednesday + - + December 2018 Thursday + - + December 2018 Friday + - + December 2018 Saturday + - + December 2018 Sunday + - + December 2018 Monday + - + December 2018 Tuesday + - + December 2018 Wednesday + - + December 2018 Thursday + - + December 2018 Friday + - + December 2018 Saturday + - + December 2018 Sunday + - + December 2018 Monday + - + December 2018 Tuesday + - + December 2018 Wednesday + - + December 2018 Thursday + - + December 2018 Friday + - + December 2018 Saturday + - + December 2018 Sunday + - + December 2018 Monday + - + December 2018 Tuesday + - + December 2018 Wednesday + - + December 2018 Thursday + - + December 2018 Friday + - + December 2018 Saturday + - + December 2018 Sunday + - + December 2018 Monday + - + January 2019 Tuesday + - + January 2019 Wednesday + - + January 2019 Thursday + - + January 2019 Friday + - + January 2019 Saturday +