Skip to content

Commit

Permalink
feat: add valueAsDate and valueAsNumber in DatePickerInput
Browse files Browse the repository at this point in the history
  • Loading branch information
motss committed Nov 27, 2021
1 parent db68ed4 commit c46f8d1
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 76 deletions.
14 changes: 13 additions & 1 deletion src/__demo__/demo-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ export class DemoApp extends RootElement {
`,
];

protected override firstUpdated(_changedProperties: Map<string | number | symbol, unknown>): void {
Object.defineProperty(globalThis, 'demoApp', {
value: {
datePicker1: this.query('#datePicker1'),
datePicker2: this.query('#datePicker2'),
datePickerInput1: this.query('#datePickerInput1'),
datePickerDialog1: this.query('#datePickerDialog1'),
},
});
}

protected override render() {
return html`
<app-date-picker
Expand All @@ -59,6 +70,7 @@ export class DemoApp extends RootElement {
></app-date-picker>
<app-date-picker-input
id="datePickerInput1"
?outlined=${true}
.label=${'DOB'}
.placeholder=${'Select your date of birth'}
Expand All @@ -68,7 +80,7 @@ export class DemoApp extends RootElement {
></app-date-picker-input>
<button @click=${this.#showDialog}>Open</button>
<app-date-picker-dialog></app-date-picker-dialog>
<app-date-picker-dialog id="datePickerDialog1"></app-date-picker-dialog>
<!-- <app-date-picker-input></app-date-picker-input> -->
<!-- <app-datepicker-dialog class="datepicker-dialog"></app-datepicker-dialog> -->
Expand Down
70 changes: 45 additions & 25 deletions src/__tests__/date-picker-input/app-date-picker-input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,32 +43,50 @@ describe(appDatePickerInputName, () => {
const placeholder = 'Select your date of birth';
const value = '2020-02-02';

it('renders', async () => {
const el = await fixture<AppDatePickerInput>(
html`<app-date-picker-input
.label=${label}
.max=${max}
.min=${min}
.placeholder=${placeholder}
.value=${value}
></app-date-picker-input>`
);

const mdcTextField = el.query(elementSelectors.mdcTextField);
const mdcFloatingLabel = el.query(elementSelectors.mdcFloatingLabel);
const mdcTextFieldInput = el.query<HTMLInputElement>(elementSelectors.mdcTextFieldInput);
const mdcTextFieldIconTrailing = el.query(elementSelectors.mdcTextFieldIconTrailing);
type CaseRenders = [string | undefined | null, string, Date | null, number];
const casesRenders: CaseRenders[] = [
['', '', null, NaN],
[undefined, '', null, NaN],
[null, '', null, NaN],
[value, value, new Date(value), +new Date(value)],
];
casesRenders.forEach((a) => {
const [testValue, expectedValue, expectedValueAsDate, expectedValueAsNumber] = a;

expect(mdcTextField).exist;
expect(mdcFloatingLabel).exist;
expect(mdcTextFieldInput).exist;
expect(mdcTextFieldIconTrailing).exist;
it(
messageFormatter('renders (value=%s)', a),
async () => {
const el = await fixture<AppDatePickerInput>(
html`<app-date-picker-input
.label=${label}
.max=${max}
.min=${min}
.placeholder=${placeholder}
.value=${testValue}
></app-date-picker-input>`
);

expect(el.type).equal(appDatePickerInputType);
expect(mdcFloatingLabel).text(label);
expect(mdcTextFieldInput?.getAttribute('aria-labelledby')).equal('label');
expect(mdcTextFieldInput?.placeholder).equal(placeholder);
expect(mdcTextFieldIconTrailing).lightDom.equal(iconClose.strings.toString());
const mdcTextField = el.query(elementSelectors.mdcTextField);
const mdcFloatingLabel = el.query(elementSelectors.mdcFloatingLabel);
const mdcTextFieldInput = el.query<HTMLInputElement>(elementSelectors.mdcTextFieldInput);
const mdcTextFieldIconTrailing = el.query(elementSelectors.mdcTextFieldIconTrailing);

expect(mdcTextField).exist;
expect(mdcFloatingLabel).exist;
expect(mdcTextFieldInput).exist;
expect(mdcTextFieldIconTrailing).exist;

expect(el.type).equal(appDatePickerInputType);
expect(el.value).equal(expectedValue);
expect(el.valueAsDate).deep.equal(expectedValueAsDate);
expect(el.valueAsNumber).deep.equal(expectedValueAsNumber);

expect(mdcFloatingLabel).text(label);
expect(mdcTextFieldInput?.getAttribute('aria-labelledby')).equal('label');
expect(mdcTextFieldInput?.placeholder).equal(placeholder);
expect(mdcTextFieldIconTrailing).lightDom.equal(iconClose.strings.toString());
}
);
});

type A1 = [string | undefined | null, string];
Expand Down Expand Up @@ -317,7 +335,9 @@ describe(appDatePickerInputName, () => {
mdcTextFieldInput = el.query<HTMLInputElement>(elementSelectors.mdcTextFieldInput);

expect(mdcTextFieldInput).value('');
expect(el).value('');
expect(el.value).equal('');
expect(el.valueAsDate).equal(null);
expect(el.valueAsNumber).deep.equal(NaN);
});

type A3 = typeof keyEnter | typeof keySpace;
Expand Down
4 changes: 2 additions & 2 deletions src/date-picker-dialog/date-picker-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ import { datePickerDialogStyling } from './stylings.js';
import type { DatePickerDialogProperties, DialogClosingEventDetail } from './typings.js';

export class DatePickerDialog extends DatePickerMixin(DatePickerMinMaxMixin(RootElement)) implements DatePickerDialogProperties {
public override get valueAsDate(): Date {
public get valueAsDate(): Date {
return this.#valueAsDate;
}

public override get valueAsNumber(): number {
public get valueAsNumber(): number {
return +this.#valueAsDate;
}

Expand Down
52 changes: 37 additions & 15 deletions src/date-picker-input/date-picker-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { DateTimeFormat } from '../constants.js';
import type { AppDatePicker } from '../date-picker/app-date-picker.js';
import type { AppDatePickerInputSurface } from '../date-picker-input-surface/app-date-picker-input-surface.js';
import { appDatePickerInputSurfaceName } from '../date-picker-input-surface/constants.js';
import { toResolvedDate } from '../helpers/to-resolved-date.js';
import { toDateString } from '../helpers/to-date-string.js';
import { iconClose } from '../icons.js';
import { keyEnter, keyEscape, keySpace } from '../key-values.js';
import { DatePickerMinMaxMixin } from '../mixins/date-picker-min-max-mixin.js';
Expand All @@ -25,6 +25,15 @@ import { datePickerInputStyling } from './stylings.js';
export class DatePickerInput extends ElementMixin(DatePickerMixin(DatePickerMinMaxMixin(TextField))) implements DatePickerMixinProperties {
public override type = appDatePickerInputType;

public get valueAsDate(): Date | null {
return this.#valueAsDate || null;
}

public get valueAsNumber(): number {
return Number(this.#valueAsDate || NaN);
}


@property({ type: String }) public clearLabel = appDatePickerInputClearLabel;
@queryAsync('.mdc-text-field__input') protected $input!: Promise<HTMLInputElement | null>;
@queryAsync(appDatePickerInputSurfaceName) protected $inputSurface!: Promise<AppDatePickerInputSurface | null>;
Expand All @@ -36,7 +45,8 @@ export class DatePickerInput extends ElementMixin(DatePickerMixin(DatePickerMinM
#focusElement: HTMLElement | undefined = undefined;
#isClearAction = false;
#picker: AppDatePicker | undefined = undefined;
#selectedDate!: Date;
#selectedDate: Date | undefined;
#valueAsDate: Date | undefined;
#valueFormatter = this.$toValueFormatter();

public static override styles = [
Expand Down Expand Up @@ -86,15 +96,13 @@ export class DatePickerInput extends ElementMixin(DatePickerMixin(DatePickerMinM
) as string;

this.locale = newLocale;
this.#valueFormatter = this.$toValueFormatter();

if (this.value) {
this._valueText = this.#valueFormatter.format(toResolvedDate(this.value));
}
this.#valueFormatter = this.$toValueFormatter();
this.#updateValues(this.value);
}

if (changedProperties.has('value') && this.value) {
this._valueText = this.#valueFormatter.format(toResolvedDate(this.value));
if (changedProperties.has('value')) {
this.#updateValues(this.value);
}

if (!this._rendered && this._open) {
Expand Down Expand Up @@ -141,11 +149,15 @@ export class DatePickerInput extends ElementMixin(DatePickerMixin(DatePickerMinM
}`;
}

public closePicker() {
public closePicker(): void {
this._open = false;
}

public showPicker() {
public reset(): void {
this.#onResetClick();
}

public showPicker(): void {
this._open = true;
}

Expand Down Expand Up @@ -189,7 +201,7 @@ export class DatePickerInput extends ElementMixin(DatePickerMixin(DatePickerMinM
protected override renderTrailingIcon(): TemplateResult {
return html`
<mwc-icon-button
@click=${this.#onClearClick}
@click=${this.#onResetClick}
aria-label=${this.clearLabel}
class="mdc-text-field__icon mdc-text-field__icon--trailing"
>
Expand All @@ -206,8 +218,9 @@ export class DatePickerInput extends ElementMixin(DatePickerMixin(DatePickerMinM
});
}

#onClearClick() {
#onResetClick() {
this.#isClearAction = true;
this.#selectedDate = this.#valueAsDate = undefined;

this.value = this._valueText = '';
}
Expand Down Expand Up @@ -238,8 +251,7 @@ export class DatePickerInput extends ElementMixin(DatePickerMixin(DatePickerMinM
this.#selectedDate = valueAsDate;

if (!isKeypress || (key === keyEnter || key === keySpace)) {
this.value = this.#valueFormatter.format(this.#selectedDate);

this.value = toDateString(this.#selectedDate);
isKeypress && (await this.$inputSurface)?.close();
}
} else {
Expand All @@ -265,7 +277,17 @@ export class DatePickerInput extends ElementMixin(DatePickerMixin(DatePickerMinM

this.#focusElement?.focus();
}

#updateValues(value: string): void {
if (value) {
const valueDate = new Date(value);

this.#selectedDate = this.#valueAsDate = valueDate;
this._valueText = this.#valueFormatter.format(valueDate);
} else {
this.#onResetClick();
}
}
}

// FIXME: No focus trap in input surface or close input surface when focus is outside
// FIXME: Support valueAsDate:null and valueAsNumber:NaN just like native input[type=date]
4 changes: 2 additions & 2 deletions src/date-picker/date-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ import { datePickerStyling } from './stylings.js';
import type { DatePickerChangedProperties } from './typings.js';

export class DatePicker extends DatePickerMixin(DatePickerMinMaxMixin(RootElement)) implements DatePickerProperties {
public override get valueAsDate(): Date {
public get valueAsDate(): Date {
return this.#valueAsDate;
}

public override get valueAsNumber(): number {
public get valueAsNumber(): number {
return +this.#valueAsDate;
}

Expand Down
4 changes: 0 additions & 4 deletions src/mixins/date-picker-mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ export const DatePickerMixin = <BaseConstructor extends LitConstructor>(
*/
@property() public value = toDateString(toResolvedDate());

public valueAsDate!: Date;

public valueAsNumber!: number;

@property() public weekLabel = 'Wk';

@property({ reflect: true, converter: { toAttribute: nullishAttributeConverter } }) public weekNumberType: WeekNumberType = 'first-4-day-week';
Expand Down
2 changes: 0 additions & 2 deletions src/mixins/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ export interface DatePickerMixinProperties {
showWeekNumber: boolean;
startView: StartView;
value?: string | null;
valueAsDate: Date;
valueAsNumber: number;
weekLabel: string;
weekNumberType: WeekNumberType;
yearDropdownLabel: string;
Expand Down
52 changes: 27 additions & 25 deletions src/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ import type { DatePicker } from './date-picker/date-picker.js';
import type { keyArrowDown, keyArrowLeft, keyArrowRight, keyArrowUp, keyEnd, keyEnter, keyHome, keyPageDown, keyPageUp, keySpace, keyTab } from './key-values.js';
import type { DatePickerMinMaxProperties, DatePickerMixinProperties, ElementMixinProperties } from './mixins/typings.js';

export type StartView = StartViewTuple[number];

export type StartViewTuple = typeof startViews;

export type ChangedProperties<T = Record<string, unknown>> = Map<keyof T, T[keyof T]>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -20,6 +16,25 @@ export interface CustomEventAction<T extends string, CustomEventDetail> {
type: T;
}

export interface CustomEventDetail {
['date-updated']: CustomEventAction<'date-updated', CustomEventDetailDateUpdated>;
['first-updated']: CustomEventAction<'first-updated', CustomEventDetailFirstUpdated>;
['year-updated']: CustomEventAction<'year-updated', CustomEventDetailYearUpdated>;
}

interface CustomEventDetailDateUpdated extends KeyEvent, DatePickerValues {}

interface CustomEventDetailFirstUpdated extends DatePickerValues {
focusableElements: HTMLElement[];
}

/**
* NOTE: No `KeyEvent` is needed as native `button` element will dispatch `click` event on keypress.
*/
interface CustomEventDetailYearUpdated {
year: number;
}

export interface DatePickerProperties extends
DatePickerMinMaxProperties,
DatePickerMixinProperties,
Expand All @@ -40,26 +55,18 @@ export interface Formatters extends Pick<DatePicker, 'locale'> {

export type InferredFromSet<SetType> = SetType extends Set<infer T> ? T : never;

export type LitConstructor = Constructor<LitElement>;

export interface CustomEventDetail {
['date-updated']: CustomEventAction<'date-updated', CustomEventDetailDateUpdated>;
['first-updated']: CustomEventAction<'first-updated', CustomEventDetailFirstUpdated>;
['year-updated']: CustomEventAction<'year-updated', CustomEventDetailYearUpdated>;
interface KeyEvent {
isKeypress: boolean;
key?: SupportedKey;
}

interface CustomEventDetailDateUpdated extends KeyEvent, DatePickerValues {}
export type LitConstructor = Constructor<LitElement>;

interface CustomEventDetailFirstUpdated extends DatePickerValues {
focusableElements: HTMLElement[];
}
export type OmitKey<T, K extends keyof T> = Omit<T, K>;

/**
* NOTE: No `KeyEvent` is needed as native `button` element will dispatch `click` event on keypress.
*/
interface CustomEventDetailYearUpdated {
year: number;
}
export type StartView = StartViewTuple[number];

export type StartViewTuple = typeof startViews;

export type SupportedKey =
| typeof keyArrowDown
Expand All @@ -77,8 +84,3 @@ export type SupportedKey =
export interface ValueUpdatedEvent extends KeyEvent {
value: string;
}

interface KeyEvent {
isKeypress: boolean;
key?: SupportedKey;
}

0 comments on commit c46f8d1

Please sign in to comment.