From c3f408feed5798a2fe45f9fa881db94827698eb0 Mon Sep 17 00:00:00 2001 From: Rong Sen Ng Date: Sun, 16 May 2021 21:26:48 +0800 Subject: [PATCH] feat: add calendar stylings --- index.html | 14 ++- src/date-picker.ts | 95 +++++++++++++------- src/helpers/to-closest-target.ts | 10 +++ src/month-calendar/month-calendar.ts | 124 ++++++++++++++++++++------- src/month-calendar/stylings.ts | 85 ++++++++++++++++++ src/stylings.ts | 45 +++++++++- src/typings.ts | 12 ++- src/year-grid-button/stylings.ts | 4 + src/year-grid/year-grid.ts | 24 +++--- 9 files changed, 333 insertions(+), 80 deletions(-) create mode 100644 src/helpers/to-closest-target.ts diff --git a/index.html b/index.html index 406f655c..1dba8fe7 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,12 @@ app-datepicker + + + + + + - diff --git a/src/date-picker.ts b/src/date-picker.ts index 1ff81f1c..b510e84e 100644 --- a/src/date-picker.ts +++ b/src/date-picker.ts @@ -3,8 +3,10 @@ import './month-calendar/app-month-calendar.js'; import './year-grid/app-year-grid.js'; import type { TemplateResult } from 'lit'; +import { nothing } from 'lit'; import { html,LitElement } from 'lit'; import { state } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; import { repeat } from 'lit/directives/repeat.js'; import { toUTCDate } from 'nodemod/dist/calendar/helpers/to-utc-date.js'; @@ -24,7 +26,7 @@ import { DatePickerMinMaxMixin } from './mixins/date-picker-min-max-mixin.js'; import { DatePickerMixin } from './mixins/date-picker-mixin.js'; import type { MonthCalendarData } from './month-calendar/typings.js'; import { datePickerStyling } from './stylings.js'; -import type { CalendarView, DatePickerChangedProperties, DatePickerProperties, Formatters, YearUpdatedEvent } from './typings.js'; +import type { CalendarView, DatePickerChangedProperties, DatePickerProperties, Formatters, ValueUpdatedEvent, YearUpdatedEvent } from './typings.js'; import type { YearGridData } from './year-grid/typings.js'; export class DatePicker extends DatePickerMixin(DatePickerMinMaxMixin(LitElement)) implements DatePickerProperties { @@ -53,8 +55,8 @@ export class DatePicker extends DatePickerMixin(DatePickerMinMaxMixin(LitElement @state() private _selectedDate: Date; - @state() - private _startView: CalendarView = 'calendar'; + // @state() + // private startView: CalendarView = 'calendar'; //#endregion private states //#region private properties @@ -92,7 +94,7 @@ export class DatePicker extends DatePickerMixin(DatePickerMinMaxMixin(LitElement const oldStartView = changedProperties.get('startView') as CalendarView; if (!calendarViews.includes(this.startView)) { - this._startView = this.startView = oldStartView; + this.startView = this.startView = oldStartView; } } @@ -135,7 +137,7 @@ export class DatePicker extends DatePickerMixin(DatePickerMinMaxMixin(LitElement this.value = toDateString(adjustedCurrentDate); } - if (changedProperties.has('_startView') && this._startView === 'calendar') { + if (changedProperties.has('startView') && this.startView === 'calendar') { const newSelectedYear = adjustOutOfRangeValue( this._min, this._max, @@ -153,7 +155,7 @@ export class DatePicker extends DatePickerMixin(DatePickerMinMaxMixin(LitElement const focusableElements: HTMLElement[] = []; // TODO: focus element - if (this._startView === 'calendar') { + if (this.startView === 'calendar') { // TODO: query select elements in calendar } else { // TODO: query select elements in year list @@ -169,9 +171,9 @@ export class DatePicker extends DatePickerMixin(DatePickerMinMaxMixin(LitElement protected updated(changedProperties: DatePickerChangedProperties): void { super.updated(changedProperties); - if (this._startView === 'calendar') { + if (this.startView === 'calendar') { // TODO: Do stuff for calendar - } else if (this._startView === 'yearGrid') { + } else if (this.startView === 'yearGrid') { // TODO: Do stuff for year grid } } @@ -185,6 +187,7 @@ export class DatePicker extends DatePickerMixin(DatePickerMinMaxMixin(LitElement const locale = this.locale; const todayDate = this._TODAY_DATE; const showWeekNumber = this.showWeekNumber; + const startView = this.startView; const { dayFormat, @@ -196,7 +199,8 @@ export class DatePicker extends DatePickerMixin(DatePickerMinMaxMixin(LitElement } = formatters; const selectedMonth = longMonthFormat(currentDate); const selectedYear = yearFormat(currentDate); - const multiCldr = toMultiCalendars({ + const isStartViewYearGrid = startView === 'yearGrid'; + const multiCldr = isStartViewYearGrid ? undefined : toMultiCalendars({ dayFormat, disabledDates: splitString(this.disabledDates, toResolvedDate), disabledDays: splitString(this.disabledDays, Number), @@ -227,24 +231,36 @@ export class DatePicker extends DatePickerMixin(DatePickerMinMaxMixin(LitElement -
- ${iconChevronLeft} - ${iconChevronRight} -
+ ${ + isStartViewYearGrid ? + nothing : + html` +
+ ${iconChevronLeft} + ${iconChevronRight} +
+ ` + } -
${ - this._startView === 'yearGrid' ? +
${ + isStartViewYearGrid ? html` ` : - html`${ + multiCldr ? html`${ repeat(multiCldr.calendars, ({ key }) => key, (calendar, idx) => { const isVisibleCalendar = idx === 1; const data: MonthCalendarData = { @@ -274,12 +290,14 @@ export class DatePicker extends DatePickerMixin(DatePickerMinMaxMixin(LitElement return html` `; }) - }` + }` : nothing }
`; } @@ -299,16 +317,31 @@ export class DatePicker extends DatePickerMixin(DatePickerMinMaxMixin(LitElement this._currentDate = newCurrentDate; }; + #updateSelectedDate = ({ + detail: { value }, + }: CustomEvent): void => { + this.#updateSelectedAndCurrentDate(value); + + // TODO: To fire value update event + }; + #updateStartView = (): void => { - const isYearGrid = this._startView === 'yearGrid'; + const isYearGrid = this.startView === 'yearGrid'; + + this.startView = isYearGrid ? 'calendar' : 'yearGrid'; + }; - this._startView = isYearGrid ? 'calendar' : 'yearGrid'; + #updateYear = ({ + detail: { year }, + }: CustomEvent): void => { + this.#updateSelectedAndCurrentDate(this._selectedDate.setUTCFullYear(year)); + this.startView = 'calendar'; }; - #updateYear = (ev: CustomEvent): void => { - const { year } = ev.detail; + #updateSelectedAndCurrentDate = (maybeDate: Date | number | string): void => { + const newSelectedDate = new Date(maybeDate); - this._selectedDate = new Date(this._selectedDate.setUTCFullYear(year)); - this._startView = 'calendar'; + this._selectedDate = newSelectedDate; + this._currentDate = new Date(newSelectedDate); }; } diff --git a/src/helpers/to-closest-target.ts b/src/helpers/to-closest-target.ts new file mode 100644 index 00000000..d5bbda1e --- /dev/null +++ b/src/helpers/to-closest-target.ts @@ -0,0 +1,10 @@ +export function toClosestTarget( + event: TargetEvent, + selector: string +): Target | undefined { + const matchedTarget = ( + Array.from(event.composedPath()) as Target[] + ).find((element => element.nodeType === Node.ELEMENT_NODE && element.matches(selector))); + + return matchedTarget; +} diff --git a/src/month-calendar/month-calendar.ts b/src/month-calendar/month-calendar.ts index c822b67b..38ffbc41 100644 --- a/src/month-calendar/month-calendar.ts +++ b/src/month-calendar/month-calendar.ts @@ -7,7 +7,9 @@ import { classMap } from 'lit/directives/class-map.js'; import { resetShadowRoot } from '../ stylings.js'; import { keyCodesRecord } from '../constants.js'; import { computeNextFocusedDate } from '../helpers/compute-next-focused-date.js'; +import { dispatchCustomEvent } from '../helpers/dispatch-custom-event.js'; import { isInTargetMonth } from '../helpers/is-in-current-month.js'; +import { toClosestTarget } from '../helpers/to-closest-target.js'; import { toResolvedDate } from '../helpers/to-resolved-date.js'; import { monthCalendarStyling } from './stylings.js'; import type { MonthCalendarData, MonthCalendarProperties } from './typings.js'; @@ -88,9 +90,16 @@ export class MonthCalendar extends LitElement implements MonthCalendarProperties focusedDate; calendarContent = html` - -
-
${ + + - ${ + ${ weekdays.map( weekday => html` ` ) } @@ -116,46 +125,49 @@ export class MonthCalendar extends LitElement implements MonthCalendarProperties ${ calendar.map((calendarRow) => { return html` - ${ + ${ calendarRow.map((calendarCol, i) => { const { disabled, fullDate, label, value } = calendarCol; /** Week label, if any */ if (!fullDate && value && showWeekNumber && i < 1) { return html``; } /** Empty day */ if (!value || !fullDate) { - return html``; + return html``; } const curTime = +new Date(fullDate); const isCurrentDate = +focusedDate === curTime; const shouldTab = showCaption && $newFocusedDate.getUTCDate() === Number(value); + /** NOTE: lit-plugin does not like this */ + const calendarDayClasses = classMap({ + 'calendar-day': true, + 'day--disabled': disabled, + 'day--today': +todayDate === curTime, + 'day--focused': !disabled && isCurrentDate, + }) as unknown as string; + return html` `; }) @@ -168,4 +180,52 @@ export class MonthCalendar extends LitElement implements MonthCalendarProperties return html`
${calendarContent}
`; } + + #updateSelectedDate = (ev: MouseEvent | KeyboardEvent): void => { + /** Do nothing when keyup.key is neither Enter nor ' ' (or Spacebar on older browsers) */ + if ( + (ev as KeyboardEvent).type === 'keyup' && + !['Enter', ' ', 'Spacebar'].includes((ev as KeyboardEvent).key) + ) return; + + const selectedCalendarDay = toClosestTarget(ev, '.calendar-day'); + + /** NOTE: Required condition check else these will trigger unwanted re-rendering */ + if ( + selectedCalendarDay == null || + [ + 'day--empty', + 'day--disabled', + 'day--focused', + 'weekday-label', + ].some(className => selectedCalendarDay.classList.contains(className)) + ) return; + + const { fullDate } = selectedCalendarDay; + + dispatchCustomEvent(this, 'date-updated', { + isKeypress: false, + value: new Date(fullDate), + }); + + return; + }; +} + +declare global { + // #region HTML element type extensions + // interface HTMLButtonElement { + // year: number; + // } + + // interface HTMLElement { + // part: HTMLElementPart; + // } + + interface HTMLTableCellElement { + day: string; + fullDate: Date; + } + // #endregion HTML element type extensions + } diff --git a/src/month-calendar/stylings.ts b/src/month-calendar/stylings.ts index 0739b170..a05ac144 100644 --- a/src/month-calendar/stylings.ts +++ b/src/month-calendar/stylings.ts @@ -1,4 +1,89 @@ import { css } from 'lit'; export const monthCalendarStyling = css` +.calendar-table, +.calendar-day { + text-align: center; +} + +.calendar-table { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + + border-collapse: collapse; + border-spacing: 0; +} + +.calendar-day, +.calendar-day-value, +.weekday-value { + position: relative; + font-size: 14px; +} + +th, +td { + padding: 0; +} + +.weekday-value { + padding: 0 0 9px; + color: #8c8c8c; +} + +.calendar-day::after, +.calendar-day-value { + top: calc((32px - 28px) / 2); + right: 0; + bottom: 0; + left: calc((32px - 28px) / 2); + width: 28px; + height: 28px; +} + +.calendar-day { + width: 32px; + height: 0; + padding-top: 32px; + outline: none; +} +.calendar-day::after { + content: ''; + display: block; + content: ''; + position: absolute; + border-radius: 50%; + opacity: 0; + pointer-events: none; + will-change: opacity; +} +.calendar-day[aria-selected="true"] { + color: #fff; +} +.calendar-day[aria-selected="true"]::after { + background-color: #1d1d1d; + opacity: 1; +} +.calendar-day:hover::after, +.calendar-day:focus::after { + outline: 1px solid #1d1d1d; + opacity: 1; +} +.calendar-day[aria-disabled="true"], +.calendar-day.day--empty { + background-color: none; + outline: none; + opacity: 0; +} + +.calendar-day-value { + display: flex; + align-items: center; + justify-content: center; + + position: absolute; + color: currentColor; + z-index: 1; +} `; diff --git a/src/stylings.ts b/src/stylings.ts index f384c404..ae1f0a9d 100644 --- a/src/stylings.ts +++ b/src/stylings.ts @@ -1,12 +1,41 @@ import { css } from 'lit'; export const datePickerStyling = css` +:host { + --date-picker-header-base-height: 52px; + --date-picker-year-grid-base-height: calc(4px + (32px * 7)); + --date-picker-base-width: calc((16px * 2) + (32px * 7)); + --date-picker-base-height: calc(32px + 36px + 12px + (32px * 6) + 8px); + + --date-picker-with-week-number-width: calc(var(--date-picker-base-width) + 32px); + --date-picker-in-year-grid-height: calc(var(--date-picker-base-height) + var(--date-picker-year-grid-base-height)); + + --date-picker-width: var(--date-picker-base-width); + --date-picker-height: var(--date-picker-base-height); + + min-width: var(--date-picker-width); + max-width: var(--date-picker-width); + min-height: var(--date-picker-height); + max-height: var(--date-picker-height); + width: 100%; + height: 100%; +} +:host([startview="calendar"][show-week-number]) { + --date-picker-width: var(--date-picker-with-week-number-width); +} +:host(startview="yearGrid") { + --date-picker-height: var(----date-picker-in-year-grid-height); +} + .header { display: grid; grid-auto-flow: column; justify-content: space-between; - margin: 0 8px 0 24px; + max-height: var(--date-picker-base-height); + height: 100%; + margin: 4px 0 0; + padding: 0 0 0 24px; } /** #region header */ @@ -22,10 +51,18 @@ export const datePickerStyling = css` .month-pagination { display: flex; + margin: 0 -4px 0 0; } +/** #endregion header */ -[data-navigation="next"] { - margin: 0 0 0 24px; +.year-grid { + max-height: var(--date-picker-year-grid-base-height); + height: 100%; + padding: 4px 20px 8px 12px; + overflow: auto; +} + +.calendar { + padding: 0 16px 8px; } -/** #endregion header */ `; diff --git a/src/typings.ts b/src/typings.ts index 74b8b44f..700ceb33 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -25,9 +25,13 @@ export type DatePickerChangedProperties = ChangedProperties; +export interface DateUpdatedEvent { + isKeypress: boolean; + value: Date; +} + export interface FirstUpdatedEvent { focusableElements: HTMLElement[]; value: DatePickerMixinProperties['value']; @@ -49,12 +53,18 @@ export interface Formatters extends Pick { export interface SupportedCustomEvent { ['animation-finished']: null; ['changed']: ChangedEvent; + ['date-updated']: DateUpdatedEvent; ['first-updated']: FirstUpdatedEvent; + ['value-updated']: ValueUpdatedEvent; ['year-updated']: YearUpdatedEvent; } export type SupportedKeyCode = typeof keyCodesRecord[keyof typeof keyCodesRecord]; +export interface ValueUpdatedEvent extends Pick { + value: string; +} + export interface YearUpdatedEvent { year: number; } diff --git a/src/year-grid-button/stylings.ts b/src/year-grid-button/stylings.ts index 07197376..832cd02a 100644 --- a/src/year-grid-button/stylings.ts +++ b/src/year-grid-button/stylings.ts @@ -9,6 +9,7 @@ export const yearGridButtonStyling = css` width: 56px; height: 32px; + pointer-events: none; } .mdc-button { @@ -17,6 +18,9 @@ export const yearGridButtonStyling = css` width: 52px; height: 28px; padding: 0; + font: inherit; + font-size: 14px; border-radius: 52px; + pointer-events: auto; } `; diff --git a/src/year-grid/year-grid.ts b/src/year-grid/year-grid.ts index 831a3847..36ed27be 100644 --- a/src/year-grid/year-grid.ts +++ b/src/year-grid/year-grid.ts @@ -9,6 +9,7 @@ import { toUTCDate } from 'nodemod/dist/calendar/helpers/to-utc-date.js'; import { resetShadowRoot } from '../ stylings.js'; import { MAX_DATE } from '../constants.js'; import { dispatchCustomEvent } from '../helpers/dispatch-custom-event.js'; +import { toClosestTarget } from '../helpers/to-closest-target.js'; import { toResolvedDate } from '../helpers/to-resolved-date.js'; import { toYearList } from '../helpers/to-year-list.js'; import { APP_YEAR_GRID_BUTTON_NAME } from '../year-grid-button/constants.js'; @@ -73,17 +74,20 @@ export class YearGrid extends LitElement implements YearGridProperties { `; } - #updateYear = (ev: MouseEvent): void => { - const selectedYearGridButton = Array.from( - ev.composedPath() as HTMLElement[] - ).find( - element => - element.localName === APP_YEAR_GRID_BUTTON_NAME && - element.hasAttribute('data-year') - ); + #updateYear = (ev: MouseEvent | KeyboardEvent): void => { + /** Do nothing when keyup.key is neither Enter nor ' ' (or Spacebar on older browsers) */ + if ( + (ev as KeyboardEvent).type === 'keyup' && + !['Enter', ' ', 'Spacebar'].includes((ev as KeyboardEvent).key) + ) return; + + const selectedYearGridButton = toClosestTarget(ev, `${APP_YEAR_GRID_BUTTON_NAME}[data-year]`); + + /** Do nothing when not tapping on the year button */ + if (selectedYearGridButton == null) return; + const year = Number( - selectedYearGridButton?.getAttribute('data-year') ?? - this.data.date.getUTCFullYear() + selectedYearGridButton.getAttribute('data-year') ); dispatchCustomEvent(this, 'year-updated', { year });
+
${ showCaption && secondMonthSecondCalendarDayFullDate ? longMonthYearFormat(secondMonthSecondCalendarDayFullDate) : '' @@ -98,16 +107,16 @@ export class MonthCalendar extends LitElement implements MonthCalendarProperties
-
${weekday.value}
+
${weekday.value}
${value} -
${value}
+
${value}