Skip to content

Commit

Permalink
refactor: refactor code around keyboard support and typings
Browse files Browse the repository at this point in the history
  • Loading branch information
motss committed Nov 7, 2021
1 parent ff07461 commit ed80808
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 89 deletions.
6 changes: 3 additions & 3 deletions src/__tests__/date-picker/app-date-picker.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';

Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
69 changes: 34 additions & 35 deletions src/date-picker/date-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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<AppMonthCalendar | null>;

@state()
private _max: Date;
@queryAsync('[data-navigation="previous"]') private readonly _navigationPrevious!: Promise<HTMLButtonElement | null>;

@state()
private _min: Date;
@queryAsync('[data-navigation="next"]') private readonly _navigationNext!: Promise<HTMLButtonElement | null>;

@state()
private _selectedDate: Date;
//#endregion private states
@queryAsync('.year-dropdown') private readonly _yearDropdown!: Promise<IconButton | null>;

//#region private properties
#formatters: Formatters;
#shouldUpdateFocusInNavigationButtons = false;
#today: Date;
@queryAsync('app-year-grid') private readonly _yearGrid!: Promise<AppYearGrid | null>;

@state() private _currentDate: Date;

@queryAsync('app-month-calendar')
private readonly _monthCalendar!: Promise<AppMonthCalendar | null>;
@state() private _max: Date;

@queryAsync('[data-navigation="previous"]')
private readonly _navigationPrevious!: Promise<HTMLButtonElement | null>;
@state() private _min: Date;

@queryAsync('[data-navigation="next"]')
private readonly _navigationNext!: Promise<HTMLButtonElement | null>;
@state() private _selectedDate: Date;

@queryAsync('app-year-grid')
private readonly _yearGrid!: Promise<AppYearGrid | null>;
//#endregion private properties
#formatters: Formatters;
#shouldUpdateFocusInNavigationButtons = false;
#today: Date;

public static override styles = [
resetShadowRoot,
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -192,16 +181,26 @@ export class DatePicker extends DatePickerMixin(DatePickerMinMaxMixin(LitElement
protected override async updated(
changedProperties: DatePickerChangedProperties
): Promise<void> {
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 {
Expand Down
4 changes: 2 additions & 2 deletions src/mixins/date-picker-mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <BaseConstructor extends Constructor<LitElement>>(
Expand Down Expand Up @@ -47,7 +47,7 @@ export const DatePickerMixin = <BaseConstructor extends Constructor<LitElement>>
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
Expand Down
4 changes: 2 additions & 2 deletions src/mixins/typings.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -20,7 +20,7 @@ export interface DatePickerMixinProperties {
previousMonthLabel: string;
selectedDateLabel: string;
showWeekNumber: boolean;
startView: CalendarView;
startView: StartView;
value: string;
weekLabel: string;
weekNumberType: WeekNumberType;
Expand Down
112 changes: 69 additions & 43 deletions src/month-calendar/month-calendar.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<HTMLTableCellElement | null>;
@property({ attribute: false }) public data?: MonthCalendarData;
@queryAsync('.calendar-day[aria-selected="true"]') public selectedCalendarDay!: Promise<HTMLTableCellElement | null>;

#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.
Expand Down Expand Up @@ -118,13 +115,13 @@ export class MonthCalendar extends LitElement implements MonthCalendarProperties

calendarContent = html`
<table
class=calendar-table
part=table
role=grid
aria-labelledby=${calendarCaptionId}
@click=${this.#updateSelectedDate}
@keydown=${this.#updateSelectedDate}
@keyup=${this.#updateSelectedDate}
aria-labelledby=${calendarCaptionId}
class=calendar-table
part=table
role=grid
>
${
showCaption && secondMonthSecondCalendarDayFullDate ? html`
Expand Down Expand Up @@ -175,23 +172,17 @@ export class MonthCalendar extends LitElement implements MonthCalendarProperties
return html`<td class="calendar-day day--empty" aria-hidden="true" part=calendar-day></td>`;
}
const curTime = +new Date(fullDate);
const isSelectedDate = +date === curTime;
const shouldTab = tabbableDate.getUTCDate() === Number(value);
return html`
<td
tabindex=${shouldTab ? '0' : '-1'}
class="calendar-day ${classMap({ 'day--today': +todayDate === curTime })}"
part=calendar-day
role=gridcell
aria-disabled=${disabled ? 'true' : 'false'}
aria-label=${label}
aria-selected=${isSelectedDate ? 'true' : 'false'}
data-day=${value}
.fullDate=${fullDate}
>
</td>
`;
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);
})
}</tr>`;
})
Expand All @@ -203,16 +194,43 @@ export class MonthCalendar extends LitElement implements MonthCalendarProperties
return html`<div class="month-calendar" part="calendar">${calendarContent}</div>`;
}

#updateSelectedDate = (ev: MouseEvent | KeyboardEvent): void => {
let newSelectedDate: Date | undefined = undefined;
protected $renderCalendarDay({
ariaDisabled,
ariaLabel,
ariaSelected,
className,
day,
fullDate,
tabIndex,
}: MonthCalendarRenderCalendarDayInit): TemplateResult {
return html`
<td
.fullDate=${fullDate}
aria-disabled=${ariaDisabled as 'true' | 'false'}
aria-label=${ariaLabel}
aria-selected=${ariaSelected as 'true' | 'false'}
class="calendar-day ${className}"
data-day=${day}
part=calendar-day
role=gridcell
tabindex=${tabIndex}
>
</td>
`;
}

if (ev.type === 'keydown') {
const key = (ev as KeyboardEvent).key as InferredFromSet<typeof navigationKeySetGrid>;
#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<typeof navigationKeySetGrid>) &&
!confirmKeySet.has(key as InferredFromSet<typeof confirmKeySet>)
) return;

// Stop scrolling with arrow keys
ev.preventDefault();
// Stop scrolling with arrow keys or Space key
event.preventDefault();

const {
currentDate,
Expand All @@ -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<typeof confirmKeySet>)
)
) {
const selectedCalendarDay =
toClosestTarget<HTMLTableCellElement>(ev, '.calendar-day');
toClosestTarget<HTMLTableCellElement>(event, '.calendar-day');

/** NOTE: Required condition check else these will trigger unwanted re-rendering */
if (
Expand All @@ -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),
});
};
Expand Down
5 changes: 5 additions & 0 deletions src/month-calendar/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ export interface MonthCalendarData {
export interface MonthCalendarProperties {
data?: MonthCalendarData;
}

export interface MonthCalendarRenderCalendarDayInit extends HTMLElement {
day: string;
fullDate: Date;
}

0 comments on commit ed80808

Please sign in to comment.