diff --git a/src/__tests__/date-picker/app-date-picker.test.ts b/src/__tests__/date-picker/app-date-picker.test.ts index 34cfd789..b8e2f831 100644 --- a/src/__tests__/date-picker/app-date-picker.test.ts +++ b/src/__tests__/date-picker/app-date-picker.test.ts @@ -1,7 +1,7 @@ import '../../date-picker/app-date-picker'; import { expect } from '@open-wc/testing'; -import { elementUpdated,fixture, html, oneEvent } from '@open-wc/testing-helpers'; +import { elementUpdated, fixture, html, oneEvent } from '@open-wc/testing-helpers'; import { MAX_DATE } from '../../constants'; import type { AppDatePicker } from '../../date-picker/app-date-picker'; @@ -10,7 +10,7 @@ import { toFormatters } from '../../helpers/to-formatters'; import { toResolvedDate } from '../../helpers/to-resolved-date'; import type { MaybeDate } from '../../helpers/typings'; import type { AppMonthCalendar } from '../../month-calendar/app-month-calendar'; -import type { CalendarView, DateUpdatedEvent, Formatters } from '../../typings'; +import type { DateUpdatedEvent, Formatters, StartView } from '../../typings'; import type { AppYearGrid } from '../../year-grid/app-year-grid'; import { messageFormatter } from '../test-utils/message-formatter'; @@ -32,7 +32,7 @@ describe(appDatePickerName, () => { const formatters: Formatters = toFormatters('en-US'); const todayDate = toResolvedDate(); - type A = [CalendarView | undefined, (keyof typeof elementSelectors)[], (keyof typeof elementSelectors)[]]; + type A = [StartView | undefined, (keyof typeof elementSelectors)[], (keyof typeof elementSelectors)[]]; const cases: A[] = [ [ undefined, diff --git a/src/constants.ts b/src/constants.ts index 85675bd8..86177f64 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,7 +9,7 @@ export const MAX_DATE = toResolvedDate('2100-12-31'); export const ONE_DAY_IN_SECONDS = 864e5; //#endregion constants -export const calendarViews = [ +export const startViews = [ 'calendar', 'yearGrid', ] as const; diff --git a/src/date-picker/date-picker.ts b/src/date-picker/date-picker.ts index d278c201..62f790bf 100644 --- a/src/date-picker/date-picker.ts +++ b/src/date-picker/date-picker.ts @@ -2,6 +2,7 @@ import '@material/mwc-icon-button'; import '../month-calendar/app-month-calendar.js'; import '../year-grid/app-year-grid.js'; +import type { IconButton } from '@material/mwc-icon-button'; import type { TemplateResult } from 'lit'; import { html, LitElement, nothing } from 'lit'; import { queryAsync, state } from 'lit/decorators.js'; @@ -10,7 +11,7 @@ import { calendar } from 'nodemod/dist/calendar/calendar.js'; import { getWeekdays } from 'nodemod/dist/calendar/helpers/get-weekdays.js'; import { toUTCDate } from 'nodemod/dist/calendar/helpers/to-utc-date.js'; -import { calendarViews, DateTimeFormat, MAX_DATE } from '../constants.js'; +import { DateTimeFormat, MAX_DATE, startViews } from '../constants.js'; import { clampValue } from '../helpers/clamp-value.js'; import { dateValidator } from '../helpers/date-validator.js'; import { dispatchCustomEvent } from '../helpers/dispatch-custom-event.js'; @@ -26,49 +27,37 @@ import { DatePickerMinMaxMixin } from '../mixins/date-picker-min-max-mixin.js'; import { DatePickerMixin } from '../mixins/date-picker-mixin.js'; import type { AppMonthCalendar } from '../month-calendar/app-month-calendar.js'; import { resetShadowRoot, webkitScrollbarStyling } from '../stylings.js'; -import type { CalendarView, DatePickerProperties, Formatters, ValueUpdatedEvent, YearUpdatedEvent } from '../typings.js'; +import type { DatePickerProperties, Formatters, StartView, ValueUpdatedEvent, YearUpdatedEvent } from '../typings.js'; import type { AppYearGrid } from '../year-grid/app-year-grid.js'; import type { YearGridData } from '../year-grid/typings.js'; import { datePickerStyling } from './stylings.js'; import type { DatePickerChangedProperties } from './typings.js'; export class DatePicker extends DatePickerMixin(DatePickerMinMaxMixin(LitElement)) implements DatePickerProperties { - //#region public properties public valueAsDate: Date; public valueAsNumber: number; - //#endregion public properties - //#region private states - @state() - private _currentDate: Date; + @queryAsync('app-month-calendar') private readonly _monthCalendar!: Promise; - @state() - private _max: Date; + @queryAsync('[data-navigation="previous"]') private readonly _navigationPrevious!: Promise; - @state() - private _min: Date; + @queryAsync('[data-navigation="next"]') private readonly _navigationNext!: Promise; - @state() - private _selectedDate: Date; - //#endregion private states + @queryAsync('.year-dropdown') private readonly _yearDropdown!: Promise; - //#region private properties - #formatters: Formatters; - #shouldUpdateFocusInNavigationButtons = false; - #today: Date; + @queryAsync('app-year-grid') private readonly _yearGrid!: Promise; + + @state() private _currentDate: Date; - @queryAsync('app-month-calendar') - private readonly _monthCalendar!: Promise; + @state() private _max: Date; - @queryAsync('[data-navigation="previous"]') - private readonly _navigationPrevious!: Promise; + @state() private _min: Date; - @queryAsync('[data-navigation="next"]') - private readonly _navigationNext!: Promise; + @state() private _selectedDate: Date; - @queryAsync('app-year-grid') - private readonly _yearGrid!: Promise; - //#endregion private properties + #formatters: Formatters; + #shouldUpdateFocusInNavigationButtons = false; + #today: Date; public static override styles = [ resetShadowRoot, @@ -157,12 +146,12 @@ export class DatePicker extends DatePickerMixin(DatePickerMinMaxMixin(LitElement if (changedProperties.has('startView')) { const oldStartView = - (changedProperties.get('startView') || this.startView) as CalendarView; + (changedProperties.get('startView') || this.startView) as StartView; /** * NOTE: Reset to old `startView` to ensure a valid value. */ - if (!calendarViews.includes(this.startView)) { + if (!startViews.includes(this.startView)) { this.startView = oldStartView; } @@ -192,16 +181,26 @@ export class DatePicker extends DatePickerMixin(DatePickerMinMaxMixin(LitElement protected override async updated( changedProperties: DatePickerChangedProperties ): Promise { - if (this.startView === 'calendar') { - if (changedProperties.has('_currentDate') && this.#shouldUpdateFocusInNavigationButtons) { - const currentDate = this._currentDate; + if (changedProperties.has('startView')) { + if (this.startView === 'calendar') { + if (changedProperties.has('_currentDate') && this.#shouldUpdateFocusInNavigationButtons) { + const currentDate = this._currentDate; + + isInCurrentMonth(this._min, currentDate) && focusElement(this._navigationNext); + isInCurrentMonth(this._max, currentDate) && focusElement(this._navigationPrevious); - isInCurrentMonth(this._min, currentDate) && focusElement(this._navigationNext); - isInCurrentMonth(this._max, currentDate) && focusElement(this._navigationPrevious); + this.#shouldUpdateFocusInNavigationButtons = false; + } + } - this.#shouldUpdateFocusInNavigationButtons = false; + if ( + changedProperties.get('startView') === 'yearGrid' as StartView && + this.startView === 'calendar' + ) { + (await this._yearDropdown)?.focus(); } } + } protected override render(): TemplateResult { diff --git a/src/mixins/date-picker-mixin.ts b/src/mixins/date-picker-mixin.ts index 40ffb2e0..f2b6874b 100644 --- a/src/mixins/date-picker-mixin.ts +++ b/src/mixins/date-picker-mixin.ts @@ -6,7 +6,7 @@ import { DateTimeFormat } from '../constants.js'; import { nullishAttributeConverter } from '../helpers/nullish-attribute-converter.js'; import { toDateString } from '../helpers/to-date-string.js'; import { toResolvedDate } from '../helpers/to-resolved-date.js'; -import type { CalendarView, Constructor } from '../typings.js'; +import type { Constructor, StartView } from '../typings.js'; import type { DatePickerMixinProperties, MixinReturnType } from './typings.js'; export const DatePickerMixin = >( @@ -47,7 +47,7 @@ export const DatePickerMixin = > public showWeekNumber = false; @property({ reflect: true, converter: { toAttribute: nullishAttributeConverter } }) - public startView: CalendarView = 'calendar'; + public startView: StartView = 'calendar'; /** * NOTE: `null` or `''` will always reset to the old valid date. In order to reset to diff --git a/src/mixins/typings.ts b/src/mixins/typings.ts index 3c0f665e..f055f1c9 100644 --- a/src/mixins/typings.ts +++ b/src/mixins/typings.ts @@ -1,7 +1,7 @@ import type { LitElement } from 'lit'; import type { WeekNumberType } from 'nodemod/dist/calendar/typings.js'; -import type { CalendarView, Constructor } from '../typings.js'; +import type { Constructor, StartView } from '../typings.js'; export interface DatePickerMinMaxProperties { max?: string; @@ -20,7 +20,7 @@ export interface DatePickerMixinProperties { previousMonthLabel: string; selectedDateLabel: string; showWeekNumber: boolean; - startView: CalendarView; + startView: StartView; value: string; weekLabel: string; weekNumberType: WeekNumberType; diff --git a/src/month-calendar/month-calendar.ts b/src/month-calendar/month-calendar.ts index c2c697c7..acbed45e 100644 --- a/src/month-calendar/month-calendar.ts +++ b/src/month-calendar/month-calendar.ts @@ -1,9 +1,8 @@ import type { TemplateResult } from 'lit'; -import { html, LitElement , nothing} from 'lit'; +import { html, LitElement, nothing } from 'lit'; import { property, queryAsync } from 'lit/decorators.js'; -import { classMap } from 'lit/directives/class-map.js'; -import { navigationKeySetGrid } from '../constants.js'; +import { confirmKeySet, navigationKeySetGrid } from '../constants.js'; import { dispatchCustomEvent } from '../helpers/dispatch-custom-event.js'; import { focusElement } from '../helpers/focus-element.js'; import { isInCurrentMonth } from '../helpers/is-in-current-month.js'; @@ -12,17 +11,15 @@ import { toNextSelectedDate } from '../helpers/to-next-selected-date.js'; import { toResolvedDate } from '../helpers/to-resolved-date.js'; import { keyHome } from '../key-values.js'; import { baseStyling, resetShadowRoot } from '../stylings.js'; -import type { Formatters, InferredFromSet } from '../typings.js'; +import type { Formatters, InferredFromSet, SupportedKey } from '../typings.js'; import { monthCalendarStyling } from './stylings.js'; -import type { MonthCalendarData, MonthCalendarProperties } from './typings.js'; +import type { MonthCalendarData, MonthCalendarProperties, MonthCalendarRenderCalendarDayInit } from './typings.js'; export class MonthCalendar extends LitElement implements MonthCalendarProperties { - @property({ attribute: false }) - public data?: MonthCalendarData; - - @queryAsync('.calendar-day[aria-selected="true"]') - public selectedCalendarDay!: Promise; + @property({ attribute: false }) public data?: MonthCalendarData; + @queryAsync('.calendar-day[aria-selected="true"]') public selectedCalendarDay!: Promise; + #selectedDate: Date | undefined = undefined; /** * NOTE(motss): This is required to avoid selected date being focused on each update. * Selected date should ONLY be focused during navigation with keyboard, e.g. @@ -118,13 +115,13 @@ export class MonthCalendar extends LitElement implements MonthCalendarProperties calendarContent = html` ${ showCaption && secondMonthSecondCalendarDayFullDate ? html` @@ -175,23 +172,17 @@ export class MonthCalendar extends LitElement implements MonthCalendarProperties return html``; } const curTime = +new Date(fullDate); - const isSelectedDate = +date === curTime; const shouldTab = tabbableDate.getUTCDate() === Number(value); - return html` - - `; + return this.$renderCalendarDay({ + ariaDisabled: String(disabled), + ariaLabel: label, + ariaSelected: String(+date === curTime), + className: +todayDate === curTime ? 'day--today' : '', + day: value, + fullDate, + tabIndex: shouldTab ? 0 : -1, + } as MonthCalendarRenderCalendarDayInit); }) }`; }) @@ -203,16 +194,43 @@ export class MonthCalendar extends LitElement implements MonthCalendarProperties return html`
${calendarContent}
`; } - #updateSelectedDate = (ev: MouseEvent | KeyboardEvent): void => { - let newSelectedDate: Date | undefined = undefined; + protected $renderCalendarDay({ + ariaDisabled, + ariaLabel, + ariaSelected, + className, + day, + fullDate, + tabIndex, + }: MonthCalendarRenderCalendarDayInit): TemplateResult { + return html` + + `; + } - if (ev.type === 'keydown') { - const key = (ev as KeyboardEvent).key as InferredFromSet; + #updateSelectedDate = (event: KeyboardEvent): void => { + const key = event.key as SupportedKey; + const type = event.type as 'click' | 'keydown' | 'keyup'; - if (!navigationKeySetGrid.has(key)) return; + if (type === 'keydown') { + if ( + !navigationKeySetGrid.has(key as InferredFromSet) && + !confirmKeySet.has(key as InferredFromSet) + ) return; - // Stop scrolling with arrow keys - ev.preventDefault(); + // Stop scrolling with arrow keys or Space key + event.preventDefault(); const { currentDate, @@ -223,21 +241,26 @@ export class MonthCalendar extends LitElement implements MonthCalendarProperties min, } = this.data as MonthCalendarData; - newSelectedDate = toNextSelectedDate({ + this.#selectedDate = toNextSelectedDate({ currentDate, date, disabledDatesSet, disabledDaysSet, - hasAltKey: ev.altKey, + hasAltKey: event.altKey, key, maxTime: +max, minTime: +min, }); - this.#shouldFocusSelectedDate = true; - } else if (ev.type === 'click') { + } else if ( + type === 'click' || + ( + type === 'keyup' && + confirmKeySet.has(key as InferredFromSet) + ) + ) { const selectedCalendarDay = - toClosestTarget(ev, '.calendar-day'); + toClosestTarget(event, '.calendar-day'); /** NOTE: Required condition check else these will trigger unwanted re-rendering */ if ( @@ -254,13 +277,16 @@ export class MonthCalendar extends LitElement implements MonthCalendarProperties return; } - newSelectedDate = selectedCalendarDay.fullDate; + this.#selectedDate = selectedCalendarDay.fullDate; } + const newSelectedDate = this.#selectedDate; + if (newSelectedDate == null) return; dispatchCustomEvent(this, 'date-updated', { - isKeypress: ev.type === 'keydown', + isKeypress: Boolean(key), + key, value: new Date(newSelectedDate), }); }; diff --git a/src/month-calendar/typings.ts b/src/month-calendar/typings.ts index 62b5d788..848f4c18 100644 --- a/src/month-calendar/typings.ts +++ b/src/month-calendar/typings.ts @@ -22,3 +22,8 @@ export interface MonthCalendarData { export interface MonthCalendarProperties { data?: MonthCalendarData; } + +export interface MonthCalendarRenderCalendarDayInit extends HTMLElement { + day: string; + fullDate: Date; +} diff --git a/src/typings.ts b/src/typings.ts index bccf5395..ec0888eb 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -1,12 +1,12 @@ import type { DateTimeFormatter } from 'nodemod/dist/calendar/typings.js'; -import type { calendarViews } from './constants.js'; +import type { startViews } from './constants.js'; import type { keyArrowDown, keyArrowLeft, keyArrowRight, keyArrowUp, keyEnd, keyEnter, keyHome, keyPageDown, keyPageUp, keySpace, keyTab } from './key-values.js'; import type { DatePickerMinMaxProperties, DatePickerMixinProperties } from './mixins/typings.js'; -export type CalendarView = CalendarViewTuple[number]; +export type StartView = StartViewTuple[number]; -export type CalendarViewTuple = typeof calendarViews; +export type StartViewTuple = typeof startViews; export interface ChangedEvent extends KeyEvent { value: DatePickerMixinProperties['value'];
-
+