diff --git a/.changeset/hip-pears-vanish.md b/.changeset/hip-pears-vanish.md new file mode 100644 index 000000000..430110879 --- /dev/null +++ b/.changeset/hip-pears-vanish.md @@ -0,0 +1,6 @@ +--- +'@lion/calendar': minor +'@lion/input-datepicker': minor +--- + +Add types for calendar and datepicker packages. diff --git a/packages/calendar/src/LionCalendar.js b/packages/calendar/src/LionCalendar.js index b3f716417..224a8a21f 100644 --- a/packages/calendar/src/LionCalendar.js +++ b/packages/calendar/src/LionCalendar.js @@ -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` `); }); @@ -65,7 +72,7 @@ describe('', () => { const el = await fixture( html``, ); - expect(el.shadowRoot.querySelectorAll('.calendar__next-button')[0]).dom.to.equal(` + expect(el.shadowRoot?.querySelectorAll('.calendar__next-button')[0]).dom.to.equal(` `); }); @@ -74,7 +81,7 @@ describe('', () => { const el = await fixture( html``, ); - expect(el.shadowRoot.querySelectorAll('.calendar__previous-button')[1]).dom.to.equal(` + expect(el.shadowRoot?.querySelectorAll('.calendar__previous-button')[1]).dom.to.equal(` `); }); @@ -83,7 +90,7 @@ describe('', () => { const el = await fixture( html``, ); - expect(el.shadowRoot.querySelectorAll('.calendar__next-button')[1]).dom.to.equal(` + expect(el.shadowRoot?.querySelectorAll('.calendar__next-button')[1]).dom.to.equal(` `); }); @@ -120,14 +127,14 @@ describe('', () => { ); 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('', () => { 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` ', () => { 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``); 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('', () => { `); 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('', () => { `); 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('', () => { }); 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('', () => { const el = await fixture(html` day.getDate() === 3} + .disableDates=${/** @param {Date} date */ date => date.getDate() === 3} > `); const elObj = new CalendarObject(el); @@ -370,6 +380,7 @@ describe('', () => { 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('', () => { describe('Accessibility', () => { it('has aria-atomic="true" set on the secondary title', async () => { const elObj = new CalendarObject(await fixture(html``)); - expect(elObj.monthHeadingEl.getAttribute('aria-atomic')).to.equal('true'); + expect(elObj.monthHeadingEl?.getAttribute('aria-atomic')).to.equal('true'); }); }); }); @@ -449,12 +460,12 @@ describe('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { 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('', () => { `); // 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(`