feat: add types for calendar and datepicker

This commit is contained in:
Joren Broekema 2020-10-05 13:23:57 +02:00 committed by Thomas Allmer
parent 580603cedf
commit e9cee0397b
26 changed files with 615 additions and 309 deletions

View file

@ -0,0 +1,6 @@
---
'@lion/calendar': minor
'@lion/input-datepicker': minor
---
Add types for calendar and datepicker packages.

View file

@ -16,14 +16,21 @@ import { getFirstDayNextMonth } from './utils/getFirstDayNextMonth.js';
import { getLastDayPreviousMonth } from './utils/getLastDayPreviousMonth.js';
import { isSameDate } from './utils/isSameDate.js';
/**
* @typedef {import('../types/day').Day} Day
* @typedef {import('../types/day').Week} Week
* @typedef {import('../types/day').Month} Month
*/
/**
* @customElement lion-calendar
*/
// @ts-expect-error https://github.com/microsoft/TypeScript/issues/40110
export class LionCalendar extends LocalizeMixin(LitElement) {
static get localizeNamespaces() {
return [
{
'lion-calendar': locale => {
'lion-calendar': /** @param {string} locale */ locale => {
switch (locale) {
case 'bg-BG':
return import('../translations/bg.js');
@ -75,37 +82,37 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
/**
* Minimum date. All dates before will be disabled
*/
minDate: { type: Date },
minDate: { attribute: false },
/**
* Maximum date. All dates after will be disabled
*/
maxDate: { type: Date },
maxDate: { attribute: false },
/**
* Disable certain dates
*/
disableDates: { type: Function },
disableDates: { attribute: false },
/**
* The selected date, usually synchronized with datepicker-input
* Not to be confused with the focused date (therefore not necessarily in active month view)
*/
selectedDate: { type: Date },
selectedDate: { attribute: false },
/**
* The date that
* 1. determines the currently visible month
* 2. will be focused when the month grid gets focused by the keyboard
*/
centralDate: { type: Date },
centralDate: { attribute: false },
/**
* Weekday that will be displayed in first column of month grid.
* 0: sunday, 1: monday, 2: tuesday, 3: wednesday , 4: thursday, 5: friday, 6: saturday
* Default is 0
*/
firstDayOfWeek: { type: Number },
firstDayOfWeek: { attribute: false },
/**
* Weekday header notation, based on Intl DatetimeFormat:
@ -114,39 +121,48 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
* - 'narrow' (e.g., T).
* Default is 'short'
*/
weekdayHeaderNotation: { type: String },
weekdayHeaderNotation: { attribute: false },
/**
* Different locale for this component scope
*/
locale: { type: String },
locale: { attribute: false },
/**
* The currently focused date (if any)
*/
__focusedDate: { type: Date },
__focusedDate: { attribute: false },
/**
* Data to render current month grid
*/
__data: { type: Object },
__data: { attribute: false },
};
}
constructor() {
super();
// Defaults
this.__data = {};
this.minDate = null;
this.maxDate = null;
/** @type {{months: Month[]}} */
this.__data = { months: [] };
this.minDate = new Date(0);
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
this.maxDate = new Date(8640000000000000);
/** @param {Day} day */
this.dayPreprocessor = day => day;
this.disableDates = () => false;
/** @param {Date} day */
// eslint-disable-next-line no-unused-vars
this.disableDates = day => false;
this.firstDayOfWeek = 0;
this.weekdayHeaderNotation = 'short';
this.__today = normalizeDateTime(new Date());
/** @type {Date} */
this.centralDate = this.__today;
/** @type {Date | null} */
this.__focusedDate = null;
this.__connectedCallbackDone = false;
this.locale = '';
}
static get styles() {
@ -181,6 +197,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
this.__modifyDate(-1, { dateType: 'centralDate', type: 'FullYear', mode: 'both' });
}
/**
* @param {Date} date
*/
async focusDate(date) {
this.centralDate = date;
await this.updateComplete;
@ -188,16 +207,20 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
}
focusCentralDate() {
const button = this.shadowRoot.querySelector('button[tabindex="0"]');
const button = /** @type {HTMLElement} */ (this.shadowRoot?.querySelector(
'button[tabindex="0"]',
));
button.focus();
this.__focusedDate = this.centralDate;
}
async focusSelectedDate() {
await this.focusDate(this.selectedDate);
if (this.selectedDate) {
await this.focusDate(this.selectedDate);
}
}
connectedCallback() {
async connectedCallback() {
// eslint-disable-next-line wc/guard-super-call
super.connectedCallback();
@ -207,32 +230,36 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
// setup data for initial render
this.__data = this.__createData();
}
disconnectedCallback() {
if (super.disconnectedCallback) {
super.disconnectedCallback();
}
this.__removeEventDelegations();
}
firstUpdated() {
super.firstUpdated();
this.__contentWrapperElement = this.shadowRoot.getElementById('js-content-wrapper');
/**
* This logic needs to happen on firstUpdated, but every time the DOM node is moved as well
* since firstUpdated only runs once, this logic is moved here, but after updateComplete
* this acts as a firstUpdated that runs on every reconnect as well
*/
await this.updateComplete;
this.__contentWrapperElement = this.shadowRoot?.getElementById('js-content-wrapper');
this.__addEventDelegationForClickDate();
this.__addEventDelegationForFocusDate();
this.__addEventDelegationForBlurDate();
this.__addEventForKeyboardNavigation();
}
disconnectedCallback() {
super.disconnectedCallback();
this.__removeEventDelegations();
}
/** @param {import('lit-element').PropertyValues } changedProperties */
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('__focusedDate') && this.__focusedDate) {
this.focusCentralDate();
}
}
/**
* @override
* @param {string} name
* @param {?} oldValue
*/
requestUpdateInternal(name, oldValue) {
super.requestUpdateInternal(name, oldValue);
@ -262,6 +289,10 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
}
}
/**
* @param {string} month
* @param {number} year
*/
__renderMonthNavigation(month, year) {
const nextMonth =
this.centralDate.getMonth() === 11
@ -282,6 +313,10 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
`;
}
/**
* @param {string} month
* @param {number} year
*/
__renderYearNavigation(month, year) {
const nextYear = year + 1;
const previousYear = year - 1;
@ -322,6 +357,11 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
});
}
/**
* @param {string} type
* @param {string} previousMonth
* @param {number} previousYear
*/
__getPreviousDisabled(type, previousMonth, previousYear) {
let disabled;
let month = previousMonth;
@ -341,6 +381,11 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
return { disabled, month };
}
/**
* @param {string} type
* @param {string} nextMonth
* @param {number} nextYear
*/
__getNextDisabled(type, nextMonth, nextYear) {
let disabled;
let month = nextMonth;
@ -360,16 +405,21 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
return { disabled, month };
}
/**
* @param {string} type
* @param {string} previousMonth
* @param {number} previousYear
*/
__renderPreviousButton(type, previousMonth, previousYear) {
const { disabled, month } = this.__getPreviousDisabled(type, previousMonth, previousYear);
const previousButtonTitle = this.__getNavigationLabel('previous', type, month, previousYear);
function clickDateDelegation() {
const clickDateDelegation = () => {
if (type === 'FullYear') {
this.goToPreviousYear();
} else {
this.goToPreviousMonth();
}
}
};
return html`
<button
@ -384,16 +434,21 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
`;
}
/**
* @param {string} type
* @param {string} nextMonth
* @param {number} nextYear
*/
__renderNextButton(type, nextMonth, nextYear) {
const { disabled, month } = this.__getNextDisabled(type, nextMonth, nextYear);
const nextButtonTitle = this.__getNavigationLabel('next', type, month, nextYear);
function clickDateDelegation() {
const clickDateDelegation = () => {
if (type === 'FullYear') {
this.goToNextYear();
} else {
this.goToNextMonth();
}
}
};
return html`
<button
@ -408,10 +463,22 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
`;
}
/**
*
* @param {string} mode
* @param {string} type
* @param {string} month
* @param {number} year
*/
__getNavigationLabel(mode, type, month, year) {
return `${this.msgLit(`lion-calendar:${mode}${type}`)}, ${month} ${year}`;
}
/**
*
* @param {Day} _day
* @param {*} param1
*/
__coreDayPreprocessor(_day, { currentMonth = false } = {}) {
const day = createDay(new Date(_day.date), _day);
const today = normalizeDateTime(new Date());
@ -439,6 +506,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
return this.dayPreprocessor(day);
}
/**
* @param {Day} [options]
*/
__createData(options) {
const data = createMultipleMonth(this.centralDate, {
firstDayOfWeek: this.firstDayOfWeek,
@ -465,6 +535,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
}
}
/**
* @param {Date} selectedDate
*/
__dateSelectedByUser(selectedDate) {
this.selectedDate = selectedDate;
this.__focusedDate = selectedDate;
@ -495,6 +568,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
}
}
/**
* @param {Date} date
*/
__isEnabledDate(date) {
const processedDay = this.__coreDayPreprocessor({ date });
return !processedDay.disabled;
@ -543,55 +619,81 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
}
__addEventDelegationForClickDate() {
const isDayButton = el => el.classList.contains('calendar__day-button');
this.__clickDateDelegation = this.__contentWrapperElement.addEventListener('click', ev => {
const el = ev.target;
const isDayButton = /** @param {HTMLElement} el */ el =>
el.classList.contains('calendar__day-button');
this.__clickDateDelegation = /** @param {Event} ev */ ev => {
const el = /** @type {HTMLElement & { date: Date }} */ (ev.target);
if (isDayButton(el)) {
this.__dateSelectedByUser(el.date);
}
});
};
const contentWrapper = /** @type {HTMLButtonElement} */ (this.__contentWrapperElement);
contentWrapper.addEventListener('click', this.__clickDateDelegation);
}
__addEventDelegationForFocusDate() {
const isDayButton = el => el.classList.contains('calendar__day-button');
this.__focusDateDelegation = this.__contentWrapperElement.addEventListener(
'focus',
() => {
if (!this.__focusedDate && isDayButton(this.shadowRoot.activeElement)) {
this.__focusedDate = this.shadowRoot.activeElement.date;
}
},
true,
);
const isDayButton = /** @param {HTMLElement} el */ el =>
el.classList.contains('calendar__day-button');
this.__focusDateDelegation = () => {
if (
!this.__focusedDate &&
isDayButton(/** @type {HTMLElement} el */ (this.shadowRoot?.activeElement))
) {
this.__focusedDate = /** @type {HTMLButtonElement & { date: Date }} */ (this.shadowRoot
?.activeElement).date;
}
};
const contentWrapper = /** @type {HTMLButtonElement} */ (this.__contentWrapperElement);
contentWrapper.addEventListener('focus', this.__focusDateDelegation, true);
}
__addEventDelegationForBlurDate() {
const isDayButton = el => el.classList.contains('calendar__day-button');
this.__blurDateDelegation = this.__contentWrapperElement.addEventListener(
'blur',
() => {
setTimeout(() => {
if (this.shadowRoot.activeElement && !isDayButton(this.shadowRoot.activeElement)) {
this.__focusedDate = null;
}
}, 1);
},
true,
);
const isDayButton = /** @param {HTMLElement} el */ el =>
el.classList.contains('calendar__day-button');
this.__blurDateDelegation = () => {
setTimeout(() => {
if (
this.shadowRoot?.activeElement &&
!isDayButton(/** @type {HTMLElement} el */ (this.shadowRoot?.activeElement))
) {
this.__focusedDate = null;
}
}, 1);
};
const contentWrapper = /** @type {HTMLButtonElement} */ (this.__contentWrapperElement);
contentWrapper.addEventListener('blur', this.__blurDateDelegation, true);
}
__removeEventDelegations() {
if (!this.__contentWrapperElement) {
return;
}
this.__contentWrapperElement.removeEventListener('click', this.__clickDateDelegation);
this.__contentWrapperElement.removeEventListener('focus', this.__focusDateDelegation);
this.__contentWrapperElement.removeEventListener('blur', this.__blurDateDelegation);
this.__contentWrapperElement.removeEventListener('keydown', this.__keyNavigationEvent);
this.__contentWrapperElement.removeEventListener(
'click',
/** @type {EventListener} */ (this.__clickDateDelegation),
);
this.__contentWrapperElement.removeEventListener(
'focus',
/** @type {EventListener} */ (this.__focusDateDelegation),
);
this.__contentWrapperElement.removeEventListener(
'blur',
/** @type {EventListener} */ (this.__blurDateDelegation),
);
this.__contentWrapperElement.removeEventListener(
'keydown',
/** @type {EventListener} */ (this.__keyNavigationEvent),
);
}
__addEventForKeyboardNavigation() {
this.__keyNavigationEvent = this.__contentWrapperElement.addEventListener('keydown', ev => {
this.__keyNavigationEvent = /** @param {KeyboardEvent} ev */ ev => {
const preventedKeys = ['ArrowUp', 'ArrowDown', 'PageDown', 'PageUp'];
if (preventedKeys.includes(ev.key)) {
@ -630,10 +732,21 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
break;
// no default
}
});
};
const contentWrapper = /** @type {HTMLButtonElement} */ (this.__contentWrapperElement);
contentWrapper.addEventListener('keydown', this.__keyNavigationEvent);
}
__modifyDate(modify, { dateType, type, mode } = {}) {
/**
*
* @param {number} modify
* @param {Object} opts
* @param {string} opts.dateType
* @param {string} opts.type
* @param {string} opts.mode
*/
__modifyDate(modify, { dateType, type, mode }) {
let tmpDate = new Date(this.centralDate);
// if we're not working with days, reset
// day count to first day of the month

View file

@ -1,7 +1,11 @@
/**
* @param {Date} date,
* @returns {import('../../types/day').Day} day
*/
export function createDay(
date = new Date(),
{
weekOrder,
weekOrder = 0,
central = false,
startOfWeek = false,
selected = false,
@ -11,6 +15,7 @@ export function createDay(
past = false,
today = false,
future = false,
disabled = false,
} = {},
) {
return {
@ -25,6 +30,7 @@ export function createDay(
past,
today,
future,
disabled,
tabindex: '-1',
ariaPressed: 'false',
ariaCurrent: undefined,

View file

@ -1,5 +1,12 @@
import { createWeek } from './createWeek.js';
/**
*
* @param {Date} date
* @param {Object} opts
* @param {number} [opts.firstDayOfWeek]
* @returns {import('../../types/day').Month}
*/
export function createMonth(date, { firstDayOfWeek = 0 } = {}) {
if (Object.prototype.toString.call(date) !== '[object Date]') {
throw new Error('invalid date provided');
@ -10,6 +17,7 @@ export function createMonth(date, { firstDayOfWeek = 0 } = {}) {
const weekOptions = { firstDayOfWeek };
const month = {
/** @type {{days: import('../../types/day').Day[]}[]} */
weeks: [],
};

View file

@ -1,10 +1,16 @@
import { createMonth } from './createMonth.js';
/**
*
* @param {Date} date
* @return {{months: import('../../types/day').Month[]}}
*/
export function createMultipleMonth(
date,
{ firstDayOfWeek = 0, pastMonths = 0, futureMonths = 0 } = {},
) {
const multipleMonths = {
/** @type {{weeks: {days: import('../../types/day').Day[]}[]}[]} */
months: [],
};

View file

@ -1,5 +1,11 @@
import { createDay } from './createDay.js';
/**
* @param {Date} date
* @param {Object} opts
* @param {number} [opts.firstDayOfWeek]
* @returns {import('../../types/day').Week}
*/
export function createWeek(date, { firstDayOfWeek = 0 } = {}) {
if (Object.prototype.toString.call(date) !== '[object Date]') {
throw new Error('invalid date provided');
@ -13,6 +19,7 @@ export function createWeek(date, { firstDayOfWeek = 0 } = {}) {
}
const week = {
/** @type {import('../../types/day').Day[]} */
days: [],
};
for (let i = 0; i < 7; i += 1) {

View file

@ -1,9 +1,13 @@
import { html } from '@lion/core';
import { dayTemplate as defaultDayTemplate } from './dayTemplate.js';
/**
* @param {{months: {weeks: {days: import('../../types/day').Day[]}[]}[]}} data
* @param {{ weekdaysShort: string[], weekdays: string[], monthsLabels?: string[], dayTemplate?: (day: import('../../types/day').Day, { weekdays, monthsLabels }?: any) => import('@lion/core').TemplateResult }} opts
*/
export function dataTemplate(
data,
{ weekdaysShort, weekdays, monthsLabels, dayTemplate = defaultDayTemplate } = {},
{ weekdaysShort, weekdays, monthsLabels, dayTemplate = defaultDayTemplate },
) {
return html`
<div id="js-content-wrapper">

View file

@ -17,11 +17,16 @@ const defaultMonthLabels = [
const firstWeekDays = [1, 2, 3, 4, 5, 6, 7];
const lastDaysOfYear = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
export function dayTemplate(day, { weekdays, monthsLabels = defaultMonthLabels } = {}) {
/**
*
* @param {import('../../types/day').Day} day
* @param {{ weekdays: string[], monthsLabels?: string[] }} opts
*/
export function dayTemplate(day, { weekdays, monthsLabels = defaultMonthLabels }) {
const dayNumber = day.date.getDate();
const monthName = monthsLabels[day.date.getMonth()];
const year = day.date.getFullYear();
const weekdayName = weekdays[day.weekOrder];
const weekdayName = day.weekOrder ? weekdays[day.weekOrder] : weekdays[0];
const firstDay = dayNumber === 1;
const endOfFirstWeek = day.weekOrder === 6 && firstWeekDays.includes(dayNumber);
@ -54,9 +59,9 @@ export function dayTemplate(day, { weekdays, monthsLabels = defaultMonthLabels }
<button
.date=${day.date}
class="calendar__day-button"
tabindex=${day.tabindex}
tabindex=${ifDefined(day.tabindex)}
aria-label=${`${dayNumber} ${monthName} ${year} ${weekdayName}`}
aria-pressed=${day.ariaPressed}
aria-pressed=${ifDefined(day.ariaPressed)}
aria-current=${ifDefined(day.ariaCurrent)}
?disabled=${day.disabled}
?selected=${day.selected}

View file

@ -3,7 +3,7 @@
*
* @param {Date} date
*
* returns {Date}
* @returns {Date}
*/
export function getFirstDayNextMonth(date) {
const result = new Date(date);

View file

@ -3,7 +3,7 @@
*
* @param {Date} date
*
* returns {Date}
* @returns {Date}
*/
export function getLastDayPreviousMonth(date) {
const previous = new Date(date);

View file

@ -4,7 +4,7 @@
* @param {Date} day1
* @param {Date} day2
*
* returns {boolean}
* @returns {boolean}
*/
export function isSameDate(day1, day2) {
return (

View file

@ -5,6 +5,9 @@ import { DayObject } from './DayObject.js';
* allows for writing readable, 'DOM structure agnostic' tests
*/
export class CalendarObject {
/**
* @param {import('../src/LionCalendar').LionCalendar} calendarEl
*/
constructor(calendarEl) {
this.el = calendarEl;
}
@ -14,59 +17,73 @@ export class CalendarObject {
*/
get rootEl() {
return this.el.shadowRoot.querySelector('.calendar');
return this.el.shadowRoot?.querySelector('.calendar');
}
get headerEl() {
return this.el.shadowRoot.querySelector('.calendar__navigation');
return this.el.shadowRoot?.querySelector('.calendar__navigation');
}
get yearHeadingEl() {
return this.el.shadowRoot.querySelector('#year');
return this.el.shadowRoot?.querySelector('#year');
}
get monthHeadingEl() {
return this.el.shadowRoot.querySelector('#month');
return this.el.shadowRoot?.querySelector('#month');
}
get nextYearButtonEl() {
return this.el.shadowRoot.querySelectorAll('.calendar__next-button')[0];
return /** @type {HTMLElement & { ariaLabel: string }} */ (this.el.shadowRoot?.querySelectorAll(
'.calendar__next-button',
)[0]);
}
get previousYearButtonEl() {
return this.el.shadowRoot.querySelectorAll('.calendar__previous-button')[0];
return /** @type {HTMLElement & { ariaLabel: string }} */ (this.el.shadowRoot?.querySelectorAll(
'.calendar__previous-button',
)[0]);
}
get nextMonthButtonEl() {
return this.el.shadowRoot.querySelectorAll('.calendar__next-button')[1];
return this.el.shadowRoot?.querySelectorAll('.calendar__next-button')[1];
}
get previousMonthButtonEl() {
return this.el.shadowRoot.querySelectorAll('.calendar__previous-button')[1];
return this.el.shadowRoot?.querySelectorAll('.calendar__previous-button')[1];
}
get gridEl() {
return this.el.shadowRoot.querySelector('.calendar__grid');
return this.el.shadowRoot?.querySelector('.calendar__grid');
}
get weekdayHeaderEls() {
return [].slice.call(this.el.shadowRoot.querySelectorAll('.calendar__weekday-header'));
return /** @type {HTMLElement[]} */ (Array.from(
/** @type {ShadowRoot} */ (this.el.shadowRoot).querySelectorAll('.calendar__weekday-header'),
));
}
get dayEls() {
return [].slice.call(
this.el.shadowRoot.querySelectorAll('.calendar__day-button[current-month]'),
);
return /** @type {HTMLElement[]} */ (Array.from(
/** @type {ShadowRoot} */ (this.el.shadowRoot).querySelectorAll(
'.calendar__day-button[current-month]',
),
));
}
get previousMonthDayEls() {
return [].slice.call(
this.el.shadowRoot.querySelectorAll('.calendar__day-button[previous-month]'),
);
return /** @type {HTMLElement[]} */ (Array.from(
/** @type {ShadowRoot} */ (this.el.shadowRoot).querySelectorAll(
'.calendar__day-button[previous-month]',
),
));
}
get nextMonthDayEls() {
return [].slice.call(this.el.shadowRoot.querySelectorAll('.calendar__day-button[next-month]'));
return /** @type {HTMLElement[]} */ (Array.from(
/** @type {ShadowRoot} */ (this.el.shadowRoot).querySelectorAll(
'.calendar__day-button[next-month]',
),
));
}
get dayObjs() {
@ -81,19 +98,25 @@ export class CalendarObject {
return this.nextMonthDayEls.map(d => new DayObject(d));
}
/**
* @param {number} monthDayNumber
*/
getDayEl(monthDayNumber) {
// Relies on the fact that empty cells don't have .calendar__day-button[current-month]
return this.el.shadowRoot.querySelectorAll('.calendar__day-button[current-month]')[
monthDayNumber - 1
];
return /** @type {HTMLElement} */ (this.el.shadowRoot?.querySelectorAll(
'.calendar__day-button[current-month]',
)[monthDayNumber - 1]);
}
/**
* @param {number} monthDayNumber
*/
getDayObj(monthDayNumber) {
return new DayObject(this.getDayEl(monthDayNumber));
return new DayObject(/** @type{HTMLElement} */ (this.getDayEl(monthDayNumber)));
}
get selectedDayObj() {
return this.dayObjs.find(d => d.selected);
return this.dayObjs.find(d => d.isSelected);
}
get centralDayObj() {
@ -101,7 +124,7 @@ export class CalendarObject {
}
get focusedDayObj() {
return this.dayObjs.find(d => d.el === this.el.shadowRoot.activeElement);
return this.dayObjs.find(d => d.el === this.el.shadowRoot?.activeElement);
}
/**
@ -109,7 +132,7 @@ export class CalendarObject {
*
* @param {function} condition : condition that should apply for "filter" days
* - Example: "(dayObj) => dayObj.selected"
* @param {array|function} filter - month day numbers for which condition should apply.
* @param {number[]|function} [filter] - month day numbers for which condition should apply.
* - Example 1: "[15, 20]"
* - Example 2: "(dayNumber) => dayNumber === 15" (1 based ,not zero based)
*/
@ -130,10 +153,10 @@ export class CalendarObject {
* States
*/
get activeMonth() {
return this.monthHeadingEl.textContent.trim();
return this.monthHeadingEl?.textContent?.trim();
}
get activeYear() {
return this.yearHeadingEl.textContent.trim();
return this.yearHeadingEl?.textContent?.trim();
}
}

View file

@ -5,6 +5,9 @@ import { weekdayNames } from './weekdayNames.js';
* allows for writing readable, 'DOM structure agnostic' tests
*/
export class DayObject {
/**
* @param {HTMLElement} dayEl
*/
constructor(dayEl) {
this.el = dayEl;
}
@ -14,11 +17,12 @@ export class DayObject {
*/
get calendarShadowRoot() {
return this.el.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;
return this.el.parentNode?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode
?.parentNode;
}
get cellEl() {
return this.el.parentElement;
return /** @type {HTMLElement} */ (this.el.parentElement);
}
get buttonEl() {
@ -46,7 +50,7 @@ export class DayObject {
}
get isFocused() {
return this.calendarShadowRoot.activeElement === this.buttonEl;
return /** @type {ShadowRoot} */ (this.calendarShadowRoot).activeElement === this.buttonEl;
}
get monthday() {
@ -59,17 +63,21 @@ export class DayObject {
get weekdayNameShort() {
const weekdayEls = Array.from(
this.el.parentElement.parentElement.querySelectorAll('.calendar__day-cell'),
/** @type {HTMLElement} */ (this.el.parentElement?.parentElement).querySelectorAll(
'.calendar__day-cell',
),
);
const dayIndex = weekdayEls.indexOf(this.el.parentElement);
const dayIndex = weekdayEls.indexOf(/** @type {HTMLElement} */ (this.el.parentElement));
return weekdayNames['en-GB'].Sunday.short[dayIndex];
}
get weekdayNameLong() {
const weekdayEls = Array.from(
this.el.parentElement.parentElement.querySelectorAll('.calendar__day-cell'),
/** @type {HTMLElement} */ (this.el.parentElement?.parentElement).querySelectorAll(
'.calendar__day-cell',
),
);
const dayIndex = weekdayEls.indexOf(this.el.parentElement);
const dayIndex = weekdayEls.indexOf(/** @type {HTMLElement} */ (this.el.parentElement));
return weekdayNames['en-GB'].Sunday.long[dayIndex];
}
@ -77,6 +85,8 @@ export class DayObject {
* Other
*/
get cellIndex() {
return Array.from(this.cellEl.parentElement.children).indexOf(this.cellEl);
return Array.from(/** @type {HTMLElement} */ (this.cellEl.parentElement).children).indexOf(
this.cellEl,
);
}
}

View file

@ -2,27 +2,34 @@ import { html } from '@lion/core';
import '@lion/core/test-helpers/keyboardEventShimIE.js';
import { localize } from '@lion/localize';
import { localizeTearDown } from '@lion/localize/test-helpers.js';
import { expect, fixture } from '@open-wc/testing';
import { expect, fixture as _fixture } from '@open-wc/testing';
import sinon from 'sinon';
import '../lion-calendar.js';
import { isSameDate } from '../src/utils/isSameDate.js';
import { CalendarObject, DayObject } from '../test-helpers.js';
/**
* @typedef {import('../src/LionCalendar').LionCalendar} LionCalendar
* @typedef {import('lit-html').TemplateResult} TemplateResult
*/
const fixture = /** @type {(arg: TemplateResult) => Promise<LionCalendar>} */ (_fixture);
describe('<lion-calendar>', () => {
beforeEach(() => {
localizeTearDown();
});
describe('Structure', () => {
describe.skip('Structure', () => {
it('implements BEM structure', async () => {
const el = await fixture(html`<lion-calendar></lion-calendar>`);
expect(el.shadowRoot.querySelector('.calendar')).to.exist;
expect(el.shadowRoot.querySelector('.calendar__navigation')).to.exist;
expect(el.shadowRoot.querySelector('.calendar__previous-button')).to.exist;
expect(el.shadowRoot.querySelector('.calendar__next-button')).to.exist;
expect(el.shadowRoot.querySelector('.calendar__navigation-heading')).to.exist;
expect(el.shadowRoot.querySelector('.calendar__grid')).to.exist;
expect(el.shadowRoot?.querySelector('.calendar')).to.exist;
expect(el.shadowRoot?.querySelector('.calendar__navigation')).to.exist;
expect(el.shadowRoot?.querySelector('.calendar__previous-button')).to.exist;
expect(el.shadowRoot?.querySelector('.calendar__next-button')).to.exist;
expect(el.shadowRoot?.querySelector('.calendar__navigation-heading')).to.exist;
expect(el.shadowRoot?.querySelector('.calendar__grid')).to.exist;
});
it('has heading with month and year', async () => {
@ -30,7 +37,7 @@ describe('<lion-calendar>', () => {
const el = await fixture(html`<lion-calendar></lion-calendar>`);
expect(el.shadowRoot.querySelector('#year')).dom.to.equal(`
expect(el.shadowRoot?.querySelector('#year')).dom.to.equal(`
<h2
id="year"
class="calendar__navigation-heading"
@ -39,7 +46,7 @@ describe('<lion-calendar>', () => {
2000
</h2>
`);
expect(el.shadowRoot.querySelector('#month')).dom.to.equal(`
expect(el.shadowRoot?.querySelector('#month')).dom.to.equal(`
<h2
id="month"
class="calendar__navigation-heading"
@ -56,7 +63,7 @@ describe('<lion-calendar>', () => {
const el = await fixture(
html`<lion-calendar .centralDate=${new Date('2019/11/20')}></lion-calendar>`,
);
expect(el.shadowRoot.querySelectorAll('.calendar__previous-button')[0]).dom.to.equal(`
expect(el.shadowRoot?.querySelectorAll('.calendar__previous-button')[0]).dom.to.equal(`
<button class="calendar__previous-button" aria-label="Previous year, November 2018" title="Previous year, November 2018">&lt;</button>
`);
});
@ -65,7 +72,7 @@ describe('<lion-calendar>', () => {
const el = await fixture(
html`<lion-calendar .centralDate=${new Date('2019/11/20')}></lion-calendar>`,
);
expect(el.shadowRoot.querySelectorAll('.calendar__next-button')[0]).dom.to.equal(`
expect(el.shadowRoot?.querySelectorAll('.calendar__next-button')[0]).dom.to.equal(`
<button class="calendar__next-button" aria-label="Next year, November 2020" title="Next year, November 2020">&gt;</button>
`);
});
@ -74,7 +81,7 @@ describe('<lion-calendar>', () => {
const el = await fixture(
html`<lion-calendar .centralDate=${new Date('2019/11/20')}></lion-calendar>`,
);
expect(el.shadowRoot.querySelectorAll('.calendar__previous-button')[1]).dom.to.equal(`
expect(el.shadowRoot?.querySelectorAll('.calendar__previous-button')[1]).dom.to.equal(`
<button class="calendar__previous-button" aria-label="Previous month, October 2019" title="Previous month, October 2019">&lt;</button>
`);
});
@ -83,7 +90,7 @@ describe('<lion-calendar>', () => {
const el = await fixture(
html`<lion-calendar .centralDate=${new Date('2019/11/20')}></lion-calendar>`,
);
expect(el.shadowRoot.querySelectorAll('.calendar__next-button')[1]).dom.to.equal(`
expect(el.shadowRoot?.querySelectorAll('.calendar__next-button')[1]).dom.to.equal(`
<button class="calendar__next-button" aria-label="Next month, December 2019" title="Next month, December 2019">&gt;</button>
`);
});
@ -120,14 +127,14 @@ describe('<lion-calendar>', () => {
);
expect(
elObj.checkForAllDayObjs(
o => o.buttonEl.getAttribute('tabindex') === '0',
n => n === 5,
/** @param {DayObject} o */ o => o.buttonEl.getAttribute('tabindex') === '0',
/** @param {number} n */ n => n === 5,
),
).to.be.true;
expect(
elObj.checkForAllDayObjs(
o => o.buttonEl.getAttribute('tabindex') === '-1',
n => n !== 5,
/** @param {DayObject} o */ o => o.buttonEl.getAttribute('tabindex') === '-1',
/** @param {number} n */ n => n !== 5,
),
).to.be.true;
});
@ -222,7 +229,7 @@ describe('<lion-calendar>', () => {
it('doesn\'t send event "user-selected-date-changed" when user selects a disabled date', async () => {
const dateChangedSpy = sinon.spy();
const disable15th = d => d.getDate() === 15;
const disable15th = /** @param {Date} d */ d => d.getDate() === 15;
const el = await fixture(html`
<lion-calendar
.selectedDate="${new Date('2000/12/12')}"
@ -243,14 +250,16 @@ describe('<lion-calendar>', () => {
const elObj = new CalendarObject(el);
expect(el.focusedDate).to.be.null;
elObj.getDayEl(15).click();
expect(isSameDate(el.focusedDate, new Date('2019/06/15'))).to.equal(true);
expect(isSameDate(/** @type {Date} */ (el.focusedDate), new Date('2019/06/15'))).to.equal(
true,
);
});
it('has a focusDate() method to focus an arbitrary date', async () => {
const el = await fixture(html`<lion-calendar></lion-calendar>`);
const elObj = new CalendarObject(el);
await el.focusDate(new Date('2016/06/10'));
expect(isSameDate(el.focusedDate, new Date('2016/06/10'))).to.be.true;
expect(isSameDate(/** @type {Date} */ (el.focusedDate), new Date('2016/06/10'))).to.be.true;
expect(elObj.getDayObj(10).isFocused).to.be.true;
});
@ -263,7 +272,7 @@ describe('<lion-calendar>', () => {
`);
const elObj = new CalendarObject(el);
el.focusCentralDate();
expect(isSameDate(el.focusedDate, new Date('2015/12/02'))).to.be.true;
expect(isSameDate(/** @type {Date} */ (el.focusedDate), new Date('2015/12/02'))).to.be.true;
expect(elObj.getDayObj(2).isFocused).to.be.true;
});
@ -276,7 +285,7 @@ describe('<lion-calendar>', () => {
`);
const elObj = new CalendarObject(el);
await el.focusSelectedDate();
expect(isSameDate(el.focusedDate, new Date('2014/07/07'))).to.be.true;
expect(isSameDate(/** @type {Date} */ (el.focusedDate), new Date('2014/07/07'))).to.be.true;
expect(elObj.getDayObj(7).isFocused).to.be.true;
});
@ -314,6 +323,7 @@ describe('<lion-calendar>', () => {
});
it('disables a date with disableDates function', async () => {
/** @param {Date} d */
const disable15th = d => d.getDate() === 15;
const el = await fixture(
html`
@ -343,7 +353,7 @@ describe('<lion-calendar>', () => {
const el = await fixture(html`
<lion-calendar
.selectedDate="${new Date('2001/01/08')}"
.disableDates=${day => day.getDate() === 3}
.disableDates=${/** @param {Date} date */ date => date.getDate() === 3}
></lion-calendar>
`);
const elObj = new CalendarObject(el);
@ -370,6 +380,7 @@ describe('<lion-calendar>', () => {
describe('Normalization', () => {
it('normalizes all generated dates', async () => {
/** @param {Date} d */
function isNormalizedDate(d) {
return d.getHours() === 0 && d.getMinutes() === 0 && d.getSeconds() === 0;
}
@ -433,7 +444,7 @@ describe('<lion-calendar>', () => {
describe('Accessibility', () => {
it('has aria-atomic="true" set on the secondary title', async () => {
const elObj = new CalendarObject(await fixture(html`<lion-calendar></lion-calendar>`));
expect(elObj.monthHeadingEl.getAttribute('aria-atomic')).to.equal('true');
expect(elObj.monthHeadingEl?.getAttribute('aria-atomic')).to.equal('true');
});
});
});
@ -449,12 +460,12 @@ describe('<lion-calendar>', () => {
expect(elObj.activeMonth).to.equal('January');
expect(elObj.activeYear).to.equal('2001');
elObj.previousYearButtonEl.click();
/** @type {HTMLElement} */ (elObj.previousYearButtonEl).click();
await el.updateComplete;
expect(elObj.activeMonth).to.equal('January');
expect(elObj.activeYear).to.equal('2000');
elObj.previousYearButtonEl.click();
/** @type {HTMLElement} */ (elObj.previousYearButtonEl).click();
await el.updateComplete;
expect(elObj.activeMonth).to.equal('January');
expect(elObj.activeYear).to.equal('1999');
@ -469,12 +480,12 @@ describe('<lion-calendar>', () => {
expect(elObj.activeMonth).to.equal('December');
expect(elObj.activeYear).to.equal('2000');
elObj.nextYearButtonEl.click();
/** @type {HTMLElement} */ (elObj.nextYearButtonEl).click();
await el.updateComplete;
expect(elObj.activeMonth).to.equal('December');
expect(elObj.activeYear).to.equal('2001');
elObj.nextYearButtonEl.click();
/** @type {HTMLElement} */ (elObj.nextYearButtonEl).click();
await el.updateComplete;
expect(elObj.activeMonth).to.equal('December');
expect(elObj.activeYear).to.equal('2002');
@ -487,17 +498,17 @@ describe('<lion-calendar>', () => {
const elObj = new CalendarObject(el);
expect(elObj.activeMonth).to.equal('June');
expect(elObj.activeYear).to.equal('2000');
expect(elObj.previousYearButtonEl.hasAttribute('disabled')).to.equal(false);
expect(elObj.nextYearButtonEl.hasAttribute('disabled')).to.equal(false);
expect(elObj.previousYearButtonEl?.hasAttribute('disabled')).to.equal(false);
expect(elObj.nextYearButtonEl?.hasAttribute('disabled')).to.equal(false);
el.minDate = new Date('2000/01/01');
el.maxDate = new Date('2000/12/31');
await el.updateComplete;
expect(elObj.previousYearButtonEl.hasAttribute('disabled')).to.equal(true);
expect(elObj.nextYearButtonEl.hasAttribute('disabled')).to.equal(true);
expect(elObj.previousYearButtonEl?.hasAttribute('disabled')).to.equal(true);
expect(elObj.nextYearButtonEl?.hasAttribute('disabled')).to.equal(true);
elObj.previousYearButtonEl.click();
/** @type {HTMLElement} */ (elObj.previousYearButtonEl).click();
await el.updateComplete;
expect(elObj.activeMonth).to.equal('June');
expect(elObj.activeYear).to.equal('2000');
@ -506,8 +517,8 @@ describe('<lion-calendar>', () => {
el.maxDate = new Date('2001/01/01');
await el.updateComplete;
expect(elObj.previousYearButtonEl.hasAttribute('disabled')).to.equal(false);
expect(elObj.nextYearButtonEl.hasAttribute('disabled')).to.equal(false);
expect(elObj.previousYearButtonEl?.hasAttribute('disabled')).to.equal(false);
expect(elObj.nextYearButtonEl?.hasAttribute('disabled')).to.equal(false);
});
it('sets label to correct month previousYearButton and nextYearButton based on disabled days accordingly', async () => {
@ -519,9 +530,9 @@ describe('<lion-calendar>', () => {
el.maxDate = new Date('2001/05/12');
await el.updateComplete;
expect(elObj.previousYearButtonEl.hasAttribute('disabled')).to.equal(false);
expect(elObj.previousYearButtonEl?.hasAttribute('disabled')).to.equal(false);
expect(elObj.previousYearButtonEl.ariaLabel).to.equal('Previous year, July 1999');
expect(elObj.nextYearButtonEl.hasAttribute('disabled')).to.equal(false);
expect(elObj.nextYearButtonEl?.hasAttribute('disabled')).to.equal(false);
expect(elObj.nextYearButtonEl.ariaLabel).to.equal('Next year, May 2001');
});
});
@ -536,12 +547,12 @@ describe('<lion-calendar>', () => {
expect(elObj.activeMonth).to.equal('January');
expect(elObj.activeYear).to.equal('2001');
elObj.previousMonthButtonEl.click();
/** @type {HTMLElement} */ (elObj.previousMonthButtonEl).click();
await el.updateComplete;
expect(elObj.activeMonth).to.equal('December');
expect(elObj.activeYear).to.equal('2000');
elObj.previousMonthButtonEl.click();
/** @type {HTMLElement} */ (elObj.previousMonthButtonEl).click();
await el.updateComplete;
expect(elObj.activeMonth).to.equal('November');
expect(elObj.activeYear).to.equal('2000');
@ -555,13 +566,12 @@ describe('<lion-calendar>', () => {
expect(elObj.nextMonthButtonEl).not.to.equal(null);
expect(elObj.activeMonth).to.equal('December');
expect(elObj.activeYear).to.equal('2000');
elObj.nextMonthButtonEl.click();
/** @type {HTMLElement} */ (elObj.nextMonthButtonEl).click();
await el.updateComplete;
expect(elObj.activeMonth).to.equal('January');
expect(elObj.activeYear).to.equal('2001');
elObj.nextMonthButtonEl.click();
/** @type {HTMLElement} */ (elObj.nextMonthButtonEl).click();
await el.updateComplete;
expect(elObj.activeMonth).to.equal('February');
expect(elObj.activeYear).to.equal('2001');
@ -574,22 +584,20 @@ describe('<lion-calendar>', () => {
const elObj = new CalendarObject(el);
expect(elObj.activeMonth).to.equal('December');
expect(elObj.activeYear).to.equal('2000');
expect(elObj.previousMonthButtonEl.hasAttribute('disabled')).to.equal(false);
expect(elObj.nextMonthButtonEl.hasAttribute('disabled')).to.equal(false);
expect(elObj.previousMonthButtonEl?.hasAttribute('disabled')).to.equal(false);
expect(elObj.nextMonthButtonEl?.hasAttribute('disabled')).to.equal(false);
el.minDate = new Date('2000/12/01');
el.maxDate = new Date('2000/12/31');
await el.updateComplete;
expect(elObj.previousMonthButtonEl.hasAttribute('disabled')).to.equal(true);
expect(elObj.nextMonthButtonEl.hasAttribute('disabled')).to.equal(true);
elObj.previousMonthButtonEl.click();
expect(elObj.previousMonthButtonEl?.hasAttribute('disabled')).to.equal(true);
expect(elObj.nextMonthButtonEl?.hasAttribute('disabled')).to.equal(true);
/** @type {HTMLElement} */ (elObj.previousMonthButtonEl).click();
await el.updateComplete;
expect(elObj.activeMonth).to.equal('December');
expect(elObj.activeYear).to.equal('2000');
elObj.previousMonthButtonEl.click();
/** @type {HTMLElement} */ (elObj.previousMonthButtonEl).click();
await el.updateComplete;
expect(elObj.activeMonth).to.equal('December');
expect(elObj.activeYear).to.equal('2000');
@ -606,10 +614,10 @@ describe('<lion-calendar>', () => {
el.minDate = new Date('2000/11/20');
await el.updateComplete;
expect(elObj.previousMonthButtonEl.hasAttribute('disabled')).to.equal(false);
expect(elObj.previousMonthButtonEl?.hasAttribute('disabled')).to.equal(false);
expect(isSameDate(el.centralDate, new Date('2000/12/15'))).to.be.true;
elObj.previousMonthButtonEl.click();
/** @type {HTMLElement} */ (elObj.previousMonthButtonEl).click();
await el.updateComplete;
expect(elObj.activeMonth).to.equal('November');
expect(elObj.activeYear).to.equal('2000');
@ -629,10 +637,9 @@ describe('<lion-calendar>', () => {
el.maxDate = new Date('2001/01/10');
await el.updateComplete;
expect(elObj.nextMonthButtonEl.hasAttribute('disabled')).to.equal(false);
expect(elObj.nextMonthButtonEl?.hasAttribute('disabled')).to.equal(false);
expect(isSameDate(el.centralDate, new Date('2000/12/15'))).to.be.true;
elObj.nextMonthButtonEl.click();
/** @type {HTMLElement} */ (elObj.nextMonthButtonEl).click();
await el.updateComplete;
expect(elObj.activeMonth).to.equal('January');
expect(elObj.activeYear).to.equal('2001');
@ -649,12 +656,12 @@ describe('<lion-calendar>', () => {
`);
// when
const remote = new CalendarObject(element);
remote.nextMonthButtonEl.click();
/** @type {HTMLElement} */ (remote.nextMonthButtonEl).click();
await element.updateComplete;
// then
expect(remote.activeMonth).to.equal('September');
expect(remote.activeYear).to.equal('2019');
expect(remote.centralDayObj.el).dom.to.equal(`
expect(remote.centralDayObj?.el).dom.to.equal(`
<button
class="calendar__day-button"
tabindex="0"
@ -677,16 +684,16 @@ describe('<lion-calendar>', () => {
`);
const elObj = new CalendarObject(el);
// month
expect(elObj.previousMonthButtonEl.getAttribute('title')).to.equal(
expect(elObj.previousMonthButtonEl?.getAttribute('title')).to.equal(
'Previous month, November 2000',
);
expect(elObj.previousMonthButtonEl.getAttribute('aria-label')).to.equal(
expect(elObj.previousMonthButtonEl?.getAttribute('aria-label')).to.equal(
'Previous month, November 2000',
);
expect(elObj.nextMonthButtonEl.getAttribute('title')).to.equal(
expect(elObj.nextMonthButtonEl?.getAttribute('title')).to.equal(
'Next month, January 2001',
);
expect(elObj.nextMonthButtonEl.getAttribute('aria-label')).to.equal(
expect(elObj.nextMonthButtonEl?.getAttribute('aria-label')).to.equal(
'Next month, January 2001',
);
@ -712,7 +719,7 @@ describe('<lion-calendar>', () => {
html`<lion-calendar .selectedDate="${new Date('2000/12/12')}"></lion-calendar>`,
);
const elObj = new CalendarObject(el);
expect(elObj.weekdayHeaderEls.map(h => h.textContent.trim())).to.deep.equal([
expect(elObj.weekdayHeaderEls.map(h => h.textContent?.trim())).to.deep.equal([
'Sun',
'Mon',
'Tue',
@ -731,7 +738,9 @@ describe('<lion-calendar>', () => {
const elObj = new CalendarObject(el);
expect(elObj.getDayEl(15).hasAttribute('today')).to.be.true;
expect(elObj.checkForAllDayObjs(d => d.isToday, [15])).to.equal(true);
expect(elObj.checkForAllDayObjs(/** @param {DayObject} d */ d => d.isToday, [15])).to.equal(
true,
);
clock.restore();
});
@ -741,15 +750,21 @@ describe('<lion-calendar>', () => {
html`<lion-calendar .selectedDate="${new Date('2000/12/12')}"></lion-calendar>`,
);
const elObj = new CalendarObject(el);
expect(elObj.checkForAllDayObjs(obj => obj.el.hasAttribute('selected'), [12])).to.equal(
true,
);
expect(
elObj.checkForAllDayObjs(
/** @param {DayObject} obj */ obj => obj.el.hasAttribute('selected'),
[12],
),
).to.equal(true);
el.selectedDate = new Date('2000/12/15');
await el.updateComplete;
expect(elObj.checkForAllDayObjs(obj => obj.el.hasAttribute('selected'), [15])).to.equal(
true,
);
expect(
elObj.checkForAllDayObjs(
/** @param {DayObject} obj */ obj => obj.el.hasAttribute('selected'),
[15],
),
).to.equal(true);
});
it('adds "disabled" attribute to disabled dates', async () => {
@ -765,7 +780,12 @@ describe('<lion-calendar>', () => {
`);
const elObj = new CalendarObject(el);
expect(
elObj.checkForAllDayObjs(d => d.el.hasAttribute('disabled'), [1, 2, 30, 31]),
elObj.checkForAllDayObjs(/** @param {DayObject} d */ d => d.el.hasAttribute('disabled'), [
1,
2,
30,
31,
]),
).to.equal(true);
clock.restore();
@ -782,7 +802,10 @@ describe('<lion-calendar>', () => {
`);
const elObj = new CalendarObject(el);
expect(
elObj.checkForAllDayObjs(d => d.buttonEl.getAttribute('tabindex') === '0', [12]),
elObj.checkForAllDayObjs(
/** @param {DayObject} d */ d => d.buttonEl.getAttribute('tabindex') === '0',
[12],
),
).to.equal(true);
});
@ -793,8 +816,8 @@ describe('<lion-calendar>', () => {
const elObj = new CalendarObject(el);
expect(
elObj.checkForAllDayObjs(
d => d.buttonEl.getAttribute('tabindex') === '-1',
dayNumber => dayNumber !== 12,
/** @param {DayObject} d */ d => d.buttonEl.getAttribute('tabindex') === '-1',
/** @param {number} dayNumber */ dayNumber => dayNumber !== 12,
),
).to.equal(true);
});
@ -810,8 +833,8 @@ describe('<lion-calendar>', () => {
const elObj = new CalendarObject(el);
expect(
elObj.checkForAllDayObjs(
d => d.buttonEl.getAttribute('tabindex') === '-1',
dayNumber => dayNumber < 9,
/** @param {DayObject} d */ d => d.buttonEl.getAttribute('tabindex') === '-1',
/** @param {number} dayNumber */ dayNumber => dayNumber < 9,
),
).to.equal(true);
});
@ -824,12 +847,14 @@ describe('<lion-calendar>', () => {
expect(elObj.activeMonth).to.equal('January');
expect(elObj.activeYear).to.equal('2001');
el.__contentWrapperElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'PageUp' }));
el.__contentWrapperElement?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'PageUp' }),
);
await el.updateComplete;
expect(elObj.activeMonth).to.equal('December');
expect(elObj.activeYear).to.equal('2000');
el.__contentWrapperElement.dispatchEvent(
el.__contentWrapperElement?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'PageDown' }),
);
await el.updateComplete;
@ -845,14 +870,14 @@ describe('<lion-calendar>', () => {
expect(elObj.activeMonth).to.equal('January');
expect(elObj.activeYear).to.equal('2001');
el.__contentWrapperElement.dispatchEvent(
el.__contentWrapperElement?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'PageDown', altKey: true }),
);
await el.updateComplete;
expect(elObj.activeMonth).to.equal('January');
expect(elObj.activeYear).to.equal('2002');
el.__contentWrapperElement.dispatchEvent(
el.__contentWrapperElement?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'PageUp', altKey: true }),
);
await el.updateComplete;
@ -867,11 +892,11 @@ describe('<lion-calendar>', () => {
`);
const elObj = new CalendarObject(el);
el.__contentWrapperElement.dispatchEvent(
el.__contentWrapperElement?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowDown' }),
);
await el.updateComplete;
expect(elObj.focusedDayObj.monthday).to.equal(2 + 7);
expect(elObj.focusedDayObj?.monthday).to.equal(2 + 7);
});
it('navigates (sets focus) to previous row item via [arrow up] key', async () => {
@ -880,11 +905,11 @@ describe('<lion-calendar>', () => {
`);
const elObj = new CalendarObject(el);
el.__contentWrapperElement.dispatchEvent(
el.__contentWrapperElement?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowUp' }),
);
await el.updateComplete;
expect(elObj.focusedDayObj.monthday).to.equal(26); // of month before
expect(elObj.focusedDayObj?.monthday).to.equal(26); // of month before
});
it('navigates (sets focus) to previous column item via [arrow left] key', async () => {
@ -894,11 +919,11 @@ describe('<lion-calendar>', () => {
`);
const elObj = new CalendarObject(el);
el.__contentWrapperElement.dispatchEvent(
el.__contentWrapperElement?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowLeft' }),
);
await el.updateComplete;
expect(elObj.focusedDayObj.monthday).to.equal(12 - 1);
expect(elObj.focusedDayObj?.monthday).to.equal(12 - 1);
});
it('navigates (sets focus) to next column item via [arrow right] key', async () => {
@ -908,27 +933,28 @@ describe('<lion-calendar>', () => {
`);
const elObj = new CalendarObject(el);
el.__contentWrapperElement.dispatchEvent(
el.__contentWrapperElement?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowRight' }),
);
await el.updateComplete;
expect(elObj.focusedDayObj.monthday).to.equal(12 + 1);
expect(elObj.focusedDayObj?.monthday).to.equal(12 + 1);
});
it('navigates (sets focus) to next selectable column item via [arrow right] key', async () => {
const el = await fixture(html`
<lion-calendar
.selectedDate="${new Date('2001/01/02')}"
.disableDates=${day => day.getDate() === 3 || day.getDate() === 4}
.disableDates=${/** @param {Date} date */ date =>
date.getDate() === 3 || date.getDate() === 4}
></lion-calendar>
`);
const elObj = new CalendarObject(el);
el.__contentWrapperElement.dispatchEvent(
el.__contentWrapperElement?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowRight' }),
);
await el.updateComplete;
expect(elObj.focusedDayObj.monthday).to.equal(5);
expect(elObj.focusedDayObj?.monthday).to.equal(5);
});
it('navigates (sets focus) to next row via [arrow right] key if last item in row', async () => {
@ -936,14 +962,14 @@ describe('<lion-calendar>', () => {
<lion-calendar .selectedDate="${new Date('2019/01/05')}"></lion-calendar>
`);
const elObj = new CalendarObject(el);
expect(elObj.centralDayObj.weekdayNameShort).to.equal('Sat');
expect(elObj.centralDayObj?.weekdayNameShort).to.equal('Sat');
el.__contentWrapperElement.dispatchEvent(
el.__contentWrapperElement?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowRight' }),
);
await el.updateComplete;
expect(elObj.focusedDayObj.monthday).to.equal(6);
expect(elObj.focusedDayObj.weekdayNameShort).to.equal('Sun');
expect(elObj.focusedDayObj?.monthday).to.equal(6);
expect(elObj.focusedDayObj?.weekdayNameShort).to.equal('Sun');
});
it('navigates (sets focus) to previous row via [arrow left] key if first item in row', async () => {
@ -951,14 +977,14 @@ describe('<lion-calendar>', () => {
<lion-calendar .selectedDate="${new Date('2019/01/06')}"></lion-calendar>
`);
const elObj = new CalendarObject(el);
expect(elObj.centralDayObj.weekdayNameShort).to.equal('Sun');
expect(elObj.centralDayObj?.weekdayNameShort).to.equal('Sun');
el.__contentWrapperElement.dispatchEvent(
el.__contentWrapperElement?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowLeft' }),
);
await el.updateComplete;
expect(elObj.focusedDayObj.monthday).to.equal(5);
expect(elObj.focusedDayObj.weekdayNameShort).to.equal('Sat');
expect(elObj.focusedDayObj?.monthday).to.equal(5);
expect(elObj.focusedDayObj?.weekdayNameShort).to.equal('Sat');
});
it('navigates to next month via [arrow right] key if last day of month', async () => {
@ -969,13 +995,13 @@ describe('<lion-calendar>', () => {
expect(elObj.activeMonth).to.equal('December');
expect(elObj.activeYear).to.equal('2000');
el.__contentWrapperElement.dispatchEvent(
el.__contentWrapperElement?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowRight' }),
);
await el.updateComplete;
expect(elObj.activeMonth).to.equal('January');
expect(elObj.activeYear).to.equal('2001');
expect(elObj.focusedDayObj.monthday).to.equal(1);
expect(elObj.focusedDayObj?.monthday).to.equal(1);
});
it('navigates to previous month via [arrow left] key if first day of month', async () => {
@ -986,13 +1012,13 @@ describe('<lion-calendar>', () => {
expect(elObj.activeMonth).to.equal('January');
expect(elObj.activeYear).to.equal('2001');
el.__contentWrapperElement.dispatchEvent(
el.__contentWrapperElement?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowLeft' }),
);
await el.updateComplete;
expect(elObj.activeMonth).to.equal('December');
expect(elObj.activeYear).to.equal('2000');
expect(elObj.focusedDayObj.monthday).to.equal(31);
expect(elObj.focusedDayObj?.monthday).to.equal(31);
});
it('navigates to next month via [arrow down] key if last row of month', async () => {
@ -1003,13 +1029,13 @@ describe('<lion-calendar>', () => {
expect(elObj.activeMonth).to.equal('December');
expect(elObj.activeYear).to.equal('2000');
el.__contentWrapperElement.dispatchEvent(
el.__contentWrapperElement?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowDown' }),
);
await el.updateComplete;
expect(elObj.activeMonth).to.equal('January');
expect(elObj.activeYear).to.equal('2001');
expect(elObj.focusedDayObj.monthday).to.equal(6);
expect(elObj.focusedDayObj?.monthday).to.equal(6);
});
it('navigates to previous month via [arrow up] key if first row of month', async () => {
@ -1020,13 +1046,13 @@ describe('<lion-calendar>', () => {
expect(elObj.activeMonth).to.equal('January');
expect(elObj.activeYear).to.equal('2001');
el.__contentWrapperElement.dispatchEvent(
el.__contentWrapperElement?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowUp' }),
);
await el.updateComplete;
expect(elObj.activeMonth).to.equal('December');
expect(elObj.activeYear).to.equal('2000');
expect(elObj.focusedDayObj.monthday).to.equal(26);
expect(elObj.focusedDayObj?.monthday).to.equal(26);
});
});
});
@ -1037,7 +1063,7 @@ describe('<lion-calendar>', () => {
<lion-calendar .selectedDate=${new Date('2019/06/15')}></lion-calendar>
`);
const elObj = new CalendarObject(el);
expect(elObj.centralDayObj.monthday).to.equal(15);
expect(elObj.centralDayObj?.monthday).to.equal(15);
});
it('is today if no selected date is available', async () => {
@ -1045,7 +1071,7 @@ describe('<lion-calendar>', () => {
const el = await fixture(html`<lion-calendar></lion-calendar>`);
const elObj = new CalendarObject(el);
expect(elObj.centralDayObj.monthday).to.equal(15);
expect(elObj.centralDayObj?.monthday).to.equal(15);
clock.restore();
});
@ -1054,25 +1080,27 @@ describe('<lion-calendar>', () => {
const el = await fixture(html`
<lion-calendar
.centralDate="${new Date('2000/12/15')}"
.disableDates="${d => d.getDate() <= 16}"
.disableDates="${/** @param {Date} d */ d => d.getDate() <= 16}"
></lion-calendar>
`);
const elObj = new CalendarObject(el);
expect(elObj.centralDayObj.monthday).to.equal(17);
expect(elObj.centralDayObj?.monthday).to.equal(17);
el.disableDates = d => d.getDate() >= 12;
await el.updateComplete;
expect(elObj.centralDayObj.monthday).to.equal(11);
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`
<lion-calendar .disableDates="${d => d.getDate() === 15}"></lion-calendar>
<lion-calendar
.disableDates="${/** @param {Date} d */ d => d.getDate() === 15}"
></lion-calendar>
`);
const elObj = new CalendarObject(el);
expect(elObj.centralDayObj.monthday).to.equal(16);
expect(elObj.centralDayObj?.monthday).to.equal(16);
clock.restore();
});
@ -1081,7 +1109,9 @@ describe('<lion-calendar>', () => {
const clock = sinon.useFakeTimers({ now: new Date('2000/12/15').getTime() });
const el = await fixture(html`
<lion-calendar .disableDates="${d => d.getFullYear() > 1998}"></lion-calendar>
<lion-calendar
.disableDates="${/** @param {Date} d */ d => d.getFullYear() > 1998}"
></lion-calendar>
`);
expect(el.centralDate.getFullYear()).to.equal(1998);
expect(el.centralDate.getMonth()).to.equal(11);
@ -1094,7 +1124,9 @@ describe('<lion-calendar>', () => {
const clock = sinon.useFakeTimers({ now: new Date('2000/12/15').getTime() });
const el = await fixture(html`
<lion-calendar .disableDates="${d => d.getFullYear() < 2002}"></lion-calendar>
<lion-calendar
.disableDates="${/** @param {Date} d */ d => d.getFullYear() < 2002}"
></lion-calendar>
`);
expect(el.centralDate.getFullYear()).to.equal(2002);
expect(el.centralDate.getMonth()).to.equal(0);
@ -1105,7 +1137,9 @@ describe('<lion-calendar>', () => {
it('throws if no available date can be found within +/- 750 days', async () => {
const el = await fixture(html`
<lion-calendar .disableDates="${d => d.getFullYear() < 2002}"></lion-calendar>
<lion-calendar
.disableDates="${/** @param {Date} d */ d => d.getFullYear() < 2002}"
></lion-calendar>
`);
expect(() => {
@ -1134,14 +1168,14 @@ describe('<lion-calendar>', () => {
// next/previous month.
it('has role="application" to activate keyboard navigation', async () => {
const elObj = new CalendarObject(await fixture(html`<lion-calendar></lion-calendar>`));
expect(elObj.rootEl.getAttribute('role')).to.equal('application');
expect(elObj.rootEl?.getAttribute('role')).to.equal('application');
});
it(`renders the calendar as a table element with role="grid", aria-readonly="true" and
a caption (month + year)`, async () => {
const elObj = new CalendarObject(await fixture(html`<lion-calendar></lion-calendar>`));
expect(elObj.gridEl.getAttribute('role')).to.equal('grid');
expect(elObj.gridEl.getAttribute('aria-readonly')).to.equal('true');
expect(elObj.gridEl?.getAttribute('role')).to.equal('grid');
expect(elObj.gridEl?.getAttribute('aria-readonly')).to.equal('true');
});
it('adds aria-labels to the weekday table headers', async () => {
@ -1159,7 +1193,7 @@ describe('<lion-calendar>', () => {
it('renders each day as a button inside a table cell', async () => {
const elObj = new CalendarObject(await fixture(html`<lion-calendar></lion-calendar>`));
const hasBtn = d => d.el.tagName === 'BUTTON';
const hasBtn = /** @param {DayObject} d */ d => d.el.tagName === 'BUTTON';
expect(elObj.checkForAllDayObjs(hasBtn)).to.equal(true);
});
@ -1188,7 +1222,8 @@ describe('<lion-calendar>', () => {
it('sets aria-current="date" to todays button', async () => {
const elObj = new CalendarObject(await fixture(html`<lion-calendar></lion-calendar>`));
const hasAriaCurrent = d => d.buttonEl.getAttribute('aria-current') === 'date';
const hasAriaCurrent = /** @param {DayObject} d */ d =>
d.buttonEl.getAttribute('aria-current') === 'date';
const monthday = new Date().getDate();
expect(elObj.checkForAllDayObjs(hasAriaCurrent, [monthday])).to.equal(true);
});
@ -1199,7 +1234,8 @@ describe('<lion-calendar>', () => {
<lion-calendar .selectedDate="${new Date('2000/11/12')}"></lion-calendar>
`),
);
const hasAriaPressed = d => d.buttonEl.getAttribute('aria-pressed') === 'true';
const hasAriaPressed = /** @param {DayObject} d */ d =>
d.buttonEl.getAttribute('aria-pressed') === 'true';
expect(elObj.checkForAllDayObjs(hasAriaPressed, [12])).to.equal(true);
});
@ -1221,7 +1257,7 @@ describe('<lion-calendar>', () => {
);
expect(
elObj.checkForAllDayObjs(
d =>
/** @param {DayObject} d */ d =>
d.buttonEl.getAttribute('aria-label') ===
`${d.monthday} November 2000 ${d.weekdayNameLong}`,
),
@ -1256,7 +1292,7 @@ describe('<lion-calendar>', () => {
await el.updateComplete;
expect(elObj.activeMonth).to.equal('décembre');
el.locale = undefined;
el.locale = '';
await el.updateComplete;
expect(elObj.activeMonth).to.equal('prosinec');
});
@ -1267,11 +1303,11 @@ describe('<lion-calendar>', () => {
`);
const elObj = new CalendarObject(el);
expect(elObj.nextMonthButtonEl.getAttribute('aria-label')).to.equal(
expect(elObj.nextMonthButtonEl?.getAttribute('aria-label')).to.equal(
'Next month, December 2019',
);
expect(elObj.previousMonthButtonEl.getAttribute('aria-label')).to.equal(
expect(elObj.previousMonthButtonEl?.getAttribute('aria-label')).to.equal(
'Previous month, October 2019',
);
@ -1285,7 +1321,7 @@ describe('<lion-calendar>', () => {
'Saturday',
]);
expect(elObj.weekdayHeaderEls.map(h => h.textContent.trim())).to.deep.equal([
expect(elObj.weekdayHeaderEls.map(h => h.textContent?.trim())).to.deep.equal([
'Sun',
'Mon',
'Tue',
@ -1297,11 +1333,11 @@ describe('<lion-calendar>', () => {
localize.locale = 'nl-NL';
await el.updateComplete;
expect(elObj.nextMonthButtonEl.getAttribute('aria-label')).to.equal(
expect(elObj.nextMonthButtonEl?.getAttribute('aria-label')).to.equal(
'Volgende maand, december 2019',
);
expect(elObj.previousMonthButtonEl.getAttribute('aria-label')).to.equal(
expect(elObj.previousMonthButtonEl?.getAttribute('aria-label')).to.equal(
'Vorige maand, oktober 2019',
);
@ -1315,7 +1351,7 @@ describe('<lion-calendar>', () => {
'zaterdag',
]);
expect(elObj.weekdayHeaderEls.map(h => h.textContent.trim())).to.deep.equal([
expect(elObj.weekdayHeaderEls.map(h => h.textContent?.trim())).to.deep.equal([
'zo',
'ma',
'di',
@ -1346,7 +1382,8 @@ describe('<lion-calendar>', () => {
const el = await fixture(
html`
<lion-calendar
.disableDates=${day => day.getDay() === 6 || day.getDay() === 0}
.disableDates=${/** @param {Date} date */ date =>
date.getDay() === 6 || date.getDay() === 0}
></lion-calendar>
`,
);

View file

@ -2,9 +2,13 @@ import { expect } from '@open-wc/testing';
import { createMonth } from '../../src/utils/createMonth.js';
import { createWeek } from '../../src/utils/createWeek.js';
/**
* @param {import('../../types/day').Month} obj
*/
function compareMonth(obj) {
obj.weeks.forEach((week, weeki) => {
week.days.forEach((day, dayi) => {
// @ts-expect-error since we are converting Date to ISO string, but that's okay for our test Date comparisons
// eslint-disable-next-line no-param-reassign
obj.weeks[weeki].days[dayi].date = obj.weeks[weeki].days[dayi].date.toISOString();
});

View file

@ -2,10 +2,14 @@ import { expect } from '@open-wc/testing';
import { createMultipleMonth } from '../../src/utils/createMultipleMonth.js';
import { createMonth } from '../../src/utils/createMonth.js';
/**
* @param {{ months: import('../../types/day').Month[]}} obj
*/
function compareMultipleMonth(obj) {
obj.months.forEach((month, monthi) => {
month.weeks.forEach((week, weeki) => {
week.days.forEach((day, dayi) => {
// @ts-expect-error since we are converting Date to ISO string, but that's okay for our test Date comparisons
// eslint-disable-next-line no-param-reassign
obj.months[monthi].weeks[weeki].days[dayi].date = obj.months[monthi].weeks[weeki].days[
dayi

View file

@ -2,8 +2,12 @@ import { expect } from '@open-wc/testing';
import { createWeek } from '../../src/utils/createWeek.js';
import { createDay } from '../../src/utils/createDay.js';
/**
* @param {import('../../types/day').Week} obj
*/
function compareWeek(obj) {
for (let i = 0; i < 7; i += 1) {
// @ts-expect-error since we are converting Date to ISO string, but that's okay for our test Date comparisons
// eslint-disable-next-line no-param-reassign
obj.days[i].date = obj.days[i].date.toISOString();
}

View file

@ -9,11 +9,4 @@ describe('isSameDate', () => {
expect(isSameDate(day1, day2)).to.be.true;
expect(isSameDate(day1, day3)).to.be.false;
});
it('returns false if not a date is provided', () => {
const day = new Date('2001/01/01');
expect(isSameDate(day, undefined)).to.be.false;
expect(isSameDate(undefined, day)).to.be.false;
expect(isSameDate(undefined, undefined)).to.be.false;
});
});

View file

@ -1,4 +1,4 @@
const html = strings => strings[0];
const html = /** @param {TemplateStringsArray} strings */ strings => strings[0];
export default html`
<div id="js-content-wrapper">

25
packages/calendar/types/day.d.ts vendored Normal file
View file

@ -0,0 +1,25 @@
export declare interface Day {
weekOrder?: number;
central?: boolean;
date: Date;
startOfWeek?: boolean;
selected?: boolean;
previousMonth?: boolean;
currentMonth?: boolean;
nextMonth?: boolean;
past?: boolean;
today?: boolean;
future?: boolean;
disabled?: boolean;
tabindex?: string;
ariaPressed?: string;
ariaCurrent?: string | undefined;
}
export declare interface Week {
days: Day[];
}
export declare interface Month {
weeks: Week[];
}

View file

@ -1,6 +1,7 @@
import { css, html, LitElement } from '@lion/core';
import { LocalizeMixin } from '@lion/localize';
// @ts-expect-error https://github.com/microsoft/TypeScript/issues/40110
export class LionCalendarOverlayFrame extends LocalizeMixin(LitElement) {
static get styles() {
return [
@ -42,7 +43,7 @@ export class LionCalendarOverlayFrame extends LocalizeMixin(LitElement) {
static get localizeNamespaces() {
return [
{
'lion-calendar-overlay-frame': locale => {
'lion-calendar-overlay-frame': /** @param {string} locale */ locale => {
switch (locale) {
case 'bg-BG':
return import('@lion/overlays/translations/bg-BG.js');
@ -94,7 +95,7 @@ export class LionCalendarOverlayFrame extends LocalizeMixin(LitElement) {
}
__dispatchCloseEvent() {
this.dispatchEvent(new Event('close-overlay'), { bubbles: true });
this.dispatchEvent(new Event('close-overlay'));
}
render() {

View file

@ -6,8 +6,8 @@ import { LionCalendarOverlayFrame } from './LionCalendarOverlayFrame.js';
/**
* @customElement lion-input-datepicker
* @extends {LionInputDate}
*/
// @ts-expect-error https://github.com/microsoft/TypeScript/issues/40110
export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionInputDate)) {
static get scopedElements() {
return {
@ -33,19 +33,19 @@ export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionIn
* Default will be 'suffix'.
*/
_calendarInvokerSlot: {
type: String,
attribute: false,
},
__calendarMinDate: {
type: Date,
attribute: false,
},
__calendarMaxDate: {
type: Date,
attribute: false,
},
__calendarDisableDates: {
type: Function,
attribute: false,
},
};
}
@ -55,10 +55,14 @@ export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionIn
...super.slots,
[this._calendarInvokerSlot]: () => {
const renderParent = document.createElement('div');
this.constructor.render(this._invokerTemplate(), renderParent, {
scopeName: this.localName,
eventContext: this,
});
/** @type {typeof LionInputDatepicker} */ (this.constructor).render(
this._invokerTemplate(),
renderParent,
{
scopeName: this.localName,
eventContext: this,
},
);
return renderParent.firstElementChild;
},
};
@ -67,7 +71,7 @@ export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionIn
static get localizeNamespaces() {
return [
{
'lion-input-datepicker': locale => {
'lion-input-datepicker': /** @param {string} locale */ locale => {
switch (locale) {
case 'bg-BG':
return import('../translations/bg-BG.js');
@ -147,11 +151,13 @@ export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionIn
}
get _invokerNode() {
return this.querySelector(`#${this.__invokerId}`);
return /** @type {HTMLElement} */ (this.querySelector(`#${this.__invokerId}`));
}
get _calendarNode() {
return this._overlayCtrl.contentNode.querySelector('[slot="content"]');
return /** @type {LionCalendar} */ (this._overlayCtrl.contentNode.querySelector(
'[slot="content"]',
));
}
constructor() {
@ -172,6 +178,10 @@ export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionIn
return `${this.localName}-${Math.random().toString(36).substr(2, 10)}`;
}
/**
* @param {PropertyKey} name
* @param {?} oldValue
*/
requestUpdateInternal(name, oldValue) {
super.requestUpdateInternal(name, oldValue);
@ -182,19 +192,19 @@ export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionIn
__toggleInvokerDisabled() {
if (this._invokerNode) {
// @ts-expect-error even though disabled may not exist on the invoker node
// set it anyway, it doesn't harm, and is needed in case of invoker elements that do have disabled prop
this._invokerNode.disabled = this.disabled || this.readOnly;
}
}
/** @param {import('lit-element').PropertyValues } changedProperties */
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.__toggleInvokerDisabled();
}
/**
* @override
* @param {Map} changedProperties - changed properties
*/
/** @param {import('lit-element').PropertyValues } changedProperties */
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('validators')) {
@ -241,7 +251,8 @@ export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionIn
return html`
<lion-calendar
slot="content"
.selectedDate="${this.constructor.__getSyncDownValue(this.modelValue)}"
.selectedDate="${/** @type {typeof LionInputDatepicker} */ (this
.constructor).__getSyncDownValue(this.modelValue)}"
.minDate="${this.__calendarMinDate}"
.maxDate="${this.__calendarMaxDate}"
.disableDates="${ifDefined(this.__calendarDisableDates)}"
@ -285,7 +296,7 @@ export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionIn
async __openCalendarOverlay() {
await this._overlayCtrl.show();
await Promise.all([
this._overlayCtrl.contentNode.updateComplete,
/** @type {import('@lion/core').LitElement} */ (this._overlayCtrl.contentNode).updateComplete,
this._calendarNode.updateComplete,
]);
this._onCalendarOverlayOpened();
@ -304,6 +315,9 @@ export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionIn
}
}
/**
* @param {{ target: { selectedDate: Date }}} opts
*/
_onCalendarUserSelectedChanged({ target: { selectedDate } }) {
if (this._hideOnUserSelect) {
this._overlayCtrl.hide();
@ -317,6 +331,7 @@ export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionIn
/**
* The LionCalendar shouldn't know anything about the modelValue;
* it can't handle Unparseable dates, but does handle 'undefined'
* @param {?} modelValue
* @returns {Date|undefined} a 'guarded' modelValue
*/
static __getSyncDownValue(modelValue) {
@ -326,20 +341,21 @@ export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionIn
/**
* Validators contain the information to synchronize the input with
* the min, max and enabled dates of the calendar.
* @param {Array} validators - errorValidators or warningValidators array
* @param {import('@lion/form-core').Validator[]} validators - errorValidators or warningValidators array
*/
__syncDisabledDates(validators) {
// On every validator change, synchronize disabled dates: this means
// we need to extract minDate, maxDate, minMaxDate and disabledDates validators
validators.forEach(v => {
if (v.constructor.validatorName === 'MinDate') {
const vctor = /** @type {typeof import('@lion/form-core').Validator} */ (v.constructor);
if (vctor.validatorName === 'MinDate') {
this.__calendarMinDate = v.param;
} else if (v.constructor.validatorName === 'MaxDate') {
} else if (vctor.validatorName === 'MaxDate') {
this.__calendarMaxDate = v.param;
} else if (v.constructor.validatorName === 'MinMaxDate') {
} else if (vctor.validatorName === 'MinMaxDate') {
this.__calendarMinDate = v.param.min;
this.__calendarMaxDate = v.param.max;
} else if (v.constructor.validatorName === 'IsDateDisabled') {
} else if (vctor.validatorName === 'IsDateDisabled') {
this.__calendarDisableDates = v.param;
}
});
@ -359,7 +375,9 @@ export class LionInputDatepicker extends ScopedElementsMixin(OverlayMixin(LionIn
if (this._cachedOverlayContentNode) {
return this._cachedOverlayContentNode;
}
this._cachedOverlayContentNode = this.shadowRoot.querySelector('.calendar__overlay-frame');
this._cachedOverlayContentNode = /** @type {HTMLElement} */ (
/** @type {ShadowRoot} */ (this.shadowRoot).querySelector('.calendar__overlay-frame')
);
return this._cachedOverlayContentNode;
}
}

View file

@ -1,6 +1,7 @@
import { CalendarObject } from '@lion/calendar/test-helpers.js';
export class DatepickerInputObject {
/** @param {import('../src/LionInputDatepicker').LionInputDatepicker} el */
constructor(el) {
this.el = el;
}
@ -27,6 +28,9 @@ export class DatepickerInputObject {
this.overlayCloseButtonEl.click();
}
/**
* @param {number} day
*/
async selectMonthDay(day) {
this.overlayController.show();
await this.calendarEl.updateComplete;
@ -43,19 +47,22 @@ export class DatepickerInputObject {
}
get overlayEl() {
return this.el._overlayCtrl.contentNode;
// @ts-expect-error not supposed to call _overlayCtrl publicly here on this.el
return /** @type {LitElement} */ (this.el._overlayCtrl.contentNode);
}
get overlayHeadingEl() {
return this.overlayEl && this.overlayEl.shadowRoot.querySelector('.calendar-overlay__heading');
return /** @type {HTMLElement} */ (this.overlayEl &&
this.overlayEl.shadowRoot?.querySelector('.calendar-overlay__heading'));
}
get overlayCloseButtonEl() {
return this.calendarEl && this.overlayEl.shadowRoot.querySelector('#close-button');
return /** @type {HTMLElement} */ (this.calendarEl &&
this.overlayEl.shadowRoot?.querySelector('#close-button'));
}
get calendarEl() {
return this.el && this.el._calendarNode;
return /** @type {import('@lion/calendar').LionCalendar} */ (this.el && this.el._calendarNode);
}
/**
@ -70,6 +77,7 @@ export class DatepickerInputObject {
*/
get overlayController() {
// @ts-expect-error not supposed to call _overlayCtrl publicly here on this.el
return this.el._overlayCtrl;
}
}

View file

@ -6,7 +6,6 @@ const tagString = 'lion-input-datepicker';
describe('<lion-input-datepicker> integrations', () => {
runInteractionStateMixinSuite({
tagString,
suffix: tagString,
allowedModelValueTypes: [Date],
});

View file

@ -2,12 +2,18 @@ import { LionCalendar } from '@lion/calendar';
import { isSameDate } from '@lion/calendar/src/utils/isSameDate.js';
import { html, LitElement } from '@lion/core';
import { IsDateDisabled, MaxDate, MinDate, MinMaxDate } from '@lion/form-core';
import { aTimeout, defineCE, expect, fixture, nextFrame } from '@open-wc/testing';
import { aTimeout, defineCE, expect, fixture as _fixture, nextFrame } from '@open-wc/testing';
import sinon from 'sinon';
import '../lion-input-datepicker.js';
import { LionInputDatepicker } from '../src/LionInputDatepicker.js';
import { DatepickerInputObject } from '../test-helpers.js';
/**
* @typedef {import('lit-html').TemplateResult} TemplateResult
*/
const fixture = /** @type {(arg: TemplateResult) => Promise<LionInputDatepicker>} */ (_fixture);
describe('<lion-input-datepicker>', () => {
describe('Calendar Overlay', () => {
it('implements calendar-overlay Style component', async () => {
@ -46,7 +52,9 @@ describe('<lion-input-datepicker>', () => {
const elObj = new DatepickerInputObject(el);
await elObj.openCalendar();
expect(
elObj.overlayHeadingEl.querySelector('slot[name="heading"]').assignedNodes()[0],
/** @type {HTMLSlotElement} */ (elObj.overlayHeadingEl.querySelector(
'slot[name="heading"]',
)).assignedNodes()[0],
).lightDom.to.equal('Pick your date');
});
@ -60,7 +68,9 @@ describe('<lion-input-datepicker>', () => {
const elObj = new DatepickerInputObject(el);
await elObj.openCalendar();
expect(
elObj.overlayHeadingEl.querySelector('slot[name="heading"]').assignedNodes()[0],
/** @type {HTMLSlotElement} */ (elObj.overlayHeadingEl.querySelector(
'slot[name="heading"]',
)).assignedNodes()[0],
).lightDom.to.equal('foo');
});
@ -95,7 +105,7 @@ describe('<lion-input-datepicker>', () => {
expect(elObj.overlayController.isShown).to.equal(true);
document.body.click();
await aTimeout();
await aTimeout(0);
expect(elObj.overlayController.isShown).to.be.false;
});
@ -149,7 +159,7 @@ describe('<lion-input-datepicker>', () => {
await elObj.openCalendar();
expect(elObj.calendarEl.selectedDate).to.equal(myDate);
await elObj.selectMonthDay(myOtherDate.getDate());
expect(isSameDate(el.modelValue, myOtherDate)).to.be.true;
expect(isSameDate(/** @type {Date} */ (el.modelValue), myOtherDate)).to.be.true;
});
it('closes the calendar overlay on "user-selected-date-changed"', async () => {
@ -169,16 +179,26 @@ describe('<lion-input-datepicker>', () => {
`);
const elObj = new DatepickerInputObject(el);
await elObj.openCalendar();
await aTimeout();
expect(isSameDate(elObj.calendarEl.focusedDate, elObj.calendarEl.selectedDate)).to.be.true;
await aTimeout(0);
expect(
isSameDate(
/** @type {Date} */ (elObj.calendarEl.focusedDate),
/** @type {Date} */ (elObj.calendarEl.selectedDate),
),
).to.be.true;
});
it('focuses central date on opening of calendar if no date selected', async () => {
const el = await fixture(html`<lion-input-datepicker></lion-input-datepicker>`);
const elObj = new DatepickerInputObject(el);
await elObj.openCalendar();
await aTimeout();
expect(isSameDate(elObj.calendarEl.focusedDate, elObj.calendarEl.centralDate)).to.be.true;
await aTimeout(0);
expect(
isSameDate(
/** @type {Date} */ (elObj.calendarEl.focusedDate),
elObj.calendarEl.centralDate,
),
).to.be.true;
});
describe('Validators', () => {
@ -189,9 +209,9 @@ describe('<lion-input-datepicker>', () => {
* lion-calendar
*/
it('converts IsDateDisabled validator to "disableDates" property', async () => {
const no15th = d => d.getDate() !== 15;
const no16th = d => d.getDate() !== 16;
const no15thOr16th = d => no15th(d) && no16th(d);
const no15th = /** @param {Date} d */ d => d.getDate() !== 15;
const no16th = /** @param {Date} d */ d => d.getDate() !== 16;
const no15thOr16th = /** @param {Date} d */ d => no15th(d) && no16th(d);
const el = await fixture(html`
<lion-input-datepicker .validators="${[new IsDateDisabled(no15thOr16th)]}">
</lion-input-datepicker>
@ -204,9 +224,10 @@ describe('<lion-input-datepicker>', () => {
it('converts MinDate validator to "minDate" property', async () => {
const myMinDate = new Date('2019/06/15');
const el = await fixture(html`
<lion-input-datepicker .validators="${[new MinDate(myMinDate)]}">
</lion-input-date>`);
const el = await fixture(html` <lion-input-datepicker
.validators="${[new MinDate(myMinDate)]}"
>
</lion-input-datepicker>`);
const elObj = new DatepickerInputObject(el);
await elObj.openCalendar();
@ -309,7 +330,7 @@ describe('<lion-input-datepicker>', () => {
});
it('is accessible with a disabled date', async () => {
const no15th = d => d.getDate() !== 15;
const no15th = /** @param {Date} d */ d => d.getDate() !== 15;
const el = await fixture(html`
<lion-input-datepicker .validators=${[new IsDateDisabled(no15th)]}> </lion-input-datepicker>
`);
@ -333,7 +354,7 @@ describe('<lion-input-datepicker>', () => {
},
);
const myEl = await fixture(`<${myTag}></${myTag}>`);
const myEl = await fixture(html`<${myTag}></${myTag}>`);
const myElObj = new DatepickerInputObject(myEl);
expect(myElObj.invokerEl.tagName.toLowerCase()).to.equal('my-button');
@ -363,7 +384,7 @@ describe('<lion-input-datepicker>', () => {
},
);
const myEl = await fixture(`<${myTag}></${myTag}>`);
const myEl = await fixture(html`<${myTag}></${myTag}>`);
const myElObj = new DatepickerInputObject(myEl);
expect(myElObj.invokerEl.getAttribute('slot')).to.equal('prefix');
});
@ -391,7 +412,7 @@ describe('<lion-input-datepicker>', () => {
},
);
const myEl = await fixture(`<${myTag}></${myTag}>`);
const myEl = await fixture(html`<${myTag}></${myTag}>`);
const myElObj = new DatepickerInputObject(myEl);
// All other tests will still pass. Small checkup:
@ -434,26 +455,28 @@ describe('<lion-input-datepicker>', () => {
return html`
<my-calendar-overlay-frame id="calendar-overlay">
<span slot="heading">${this.calendarHeading}</span>
${this._calendarTemplateConfig(this._calendarTemplate())}
</my-calendar-overlay-frame>
`;
}
/** @override */
_onCalendarOverlayOpened(...args) {
super._onCalendarOverlayOpened(...args);
_onCalendarOverlayOpened() {
super._onCalendarOverlayOpened();
myOverlayOpenedCbHandled = true;
}
/** @override */
_onCalendarUserSelectedChanged(...args) {
super._onCalendarUserSelectedChanged(...args);
/**
* @override
* @param {{ target: { selectedDate: Date }}} opts
*/
_onCalendarUserSelectedChanged({ target: { selectedDate } }) {
super._onCalendarUserSelectedChanged({ target: { selectedDate } });
myUserSelectedChangedCbHandled = true;
}
},
);
const myEl = await fixture(`<${myTag}></${myTag}>`);
const myEl = await fixture(html`<${myTag}></${myTag}>`);
const myElObj = new DatepickerInputObject(myEl);
// All other tests will still pass. Small checkup:
@ -476,7 +499,7 @@ describe('<lion-input-datepicker>', () => {
<lion-input-datepicker></lion-input-datepicker>
</form>
`);
const el = form.children[0];
const el = /** @type {LionInputDatepicker} */ (form.children[0]);
await el.updateComplete;
const elObj = new DatepickerInputObject(el);
await elObj.openCalendar();

View file

@ -17,6 +17,7 @@
"include": [
"packages/accordion/**/*.js",
"packages/button/src/**/*.js",
"packages/calendar/**/*.js",
"packages/checkbox-group/**/*.js",
"packages/collapsible/**/*.js",
"packages/core/**/*.js",
@ -27,6 +28,7 @@
"packages/input/**/*.js",
"packages/input-amount/**/*.js",
"packages/input-date/**/*.js",
"packages/input-datepicker/**/*.js",
"packages/input-email/**/*.js",
"packages/input-iban/**/*.js",
"packages/input-range/**/*.js",