Skip to content

Commit

Permalink
feat: close picker when focus is outside of DatePickerInput
Browse files Browse the repository at this point in the history
  • Loading branch information
motss committed Nov 27, 2021
1 parent 3239293 commit 5a10623
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 44 deletions.
105 changes: 69 additions & 36 deletions src/__tests__/date-picker-input/app-date-picker-input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import type { AppDatePickerInput } from '../../date-picker-input/app-date-picker
import { appDatePickerInputName, appDatePickerInputType } from '../../date-picker-input/constants';
import type { AppDatePickerInputSurface } from '../../date-picker-input-surface/app-date-picker-input-surface';
import { appDatePickerInputSurfaceName } from '../../date-picker-input-surface/constants';
import { iconClose } from '../../icons';
import { keyEnter, keyEscape, keySpace } from '../../key-values';
import { iconClear } from '../../icons';
import { keyEnter, keyEscape, keySpace, keyTab } from '../../key-values';
import type { AppMonthCalendar } from '../../month-calendar/app-month-calendar';
import { appMonthCalendarName } from '../../month-calendar/constants';
import { eventOnce } from '../test-utils/event-once';
Expand Down Expand Up @@ -84,7 +84,7 @@ describe(appDatePickerInputName, () => {
expect(mdcFloatingLabel).text(label);
expect(mdcTextFieldInput?.getAttribute('aria-labelledby')).equal('label');
expect(mdcTextFieldInput?.placeholder).equal(placeholder);
expect(mdcTextFieldIconTrailing).lightDom.equal(iconClose.strings.toString());
expect(mdcTextFieldIconTrailing).lightDom.equal(iconClear.strings.toString());
}
);
});
Expand Down Expand Up @@ -203,10 +203,11 @@ describe(appDatePickerInputName, () => {
expect(datePicker).exist;
});

type CaseCloseDatePickerBy = [string, 'click' | 'keyup'];
type CaseCloseDatePickerBy = [string, 'click' | 'escape' | 'tab'];
const casesCloseDatePicker: CaseCloseDatePickerBy[] = [
['clicking outside of input surface', 'click'],
['pressing Escape key', 'keyup'],
['clicking outside of date picker input', 'click'],
['pressing Escape key', 'escape'],
['tabbing outside of date picker input', 'tab'],
];
casesCloseDatePicker.forEach((a) => {
const [, testTriggerType] = a;
Expand Down Expand Up @@ -245,11 +246,26 @@ describe(appDatePickerInputName, () => {
'closed',
CustomEvent<DialogClosedEventDetail>>(el, 'closed');

if (testTriggerType === 'click') {
document.body.click();
} else {
await sendKeys({ down: keyEscape });
await sendKeys({ up: keyEscape });
switch (testTriggerType) {
case 'click': {
document.body.click();
break;
}
case 'escape': {
await sendKeys({ down: keyEscape });
await sendKeys({ up: keyEscape });
break;
}
case 'tab': {
const yearDropdown = datePicker?.query(elementSelectors.yearDropdown);

expect(yearDropdown).exist;

yearDropdown?.focus();
for (const _ of Array(4)) await sendKeys({ press: keyTab });
break;
}
default:
}

await closedTask;
Expand Down Expand Up @@ -306,38 +322,55 @@ describe(appDatePickerInputName, () => {
);
});

it('clears value', 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>`
);
type CaseResetsValue = [string, 'reset' | 'click'];
const casesResetsValue: CaseResetsValue[] = [
['calls .reset()', 'reset'],
['clicks clear icon button', 'click'],
];
casesResetsValue.forEach((a) => {
const [_, testTriggerType] = a;

let mdcTextFieldInput =
el.query<HTMLInputElement>(elementSelectors.mdcTextFieldInput);
const mdcTextFieldIconTrailing =
el.query<Button>(elementSelectors.mdcTextFieldIconTrailing);
it(
messageFormatter('%s to reset value', a),
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>`
);

expect(mdcTextFieldInput).exist;
expect(mdcTextFieldIconTrailing).exist;
let mdcTextFieldInput =
el.query<HTMLInputElement>(elementSelectors.mdcTextFieldInput);
const mdcTextFieldIconTrailing =
el.query<Button>(elementSelectors.mdcTextFieldIconTrailing);

const expectedValue = formatter.format(new Date(value));
expect(mdcTextFieldInput).exist;
expect(mdcTextFieldIconTrailing).exist;

expect(mdcTextFieldInput).value(expectedValue);
const expectedValue = formatter.format(new Date(value));

mdcTextFieldIconTrailing?.click();
await el.updateComplete;
expect(mdcTextFieldInput).value(expectedValue);

mdcTextFieldInput = el.query<HTMLInputElement>(elementSelectors.mdcTextFieldInput);
if (testTriggerType === 'click') {
mdcTextFieldIconTrailing?.click();
} else {
el.reset();
}

await el.updateComplete;

mdcTextFieldInput = el.query<HTMLInputElement>(elementSelectors.mdcTextFieldInput);

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

type A3 = typeof keyEnter | typeof keySpace;
Expand Down
20 changes: 14 additions & 6 deletions src/date-picker-input/date-picker-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ 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 { toDateString } from '../helpers/to-date-string.js';
import { iconClose } from '../icons.js';
import { keyEnter, keyEscape, keySpace } from '../key-values.js';
import { iconClear } from '../icons.js';
import { keyEnter, keyEscape, keySpace, keyTab } from '../key-values.js';
import { DatePickerMinMaxMixin } from '../mixins/date-picker-min-max-mixin.js';
import { DatePickerMixin } from '../mixins/date-picker-mixin.js';
import { ElementMixin } from '../mixins/element-mixin.js';
Expand All @@ -24,6 +24,7 @@ import { datePickerInputStyling } from './stylings.js';

export class DatePickerInput extends ElementMixin(DatePickerMixin(DatePickerMinMaxMixin(TextField))) implements DatePickerMixinProperties {
public override type = appDatePickerInputType;
public override iconTrailing = 'clear';

public get valueAsDate(): Date | null {
return this.#valueAsDate || null;
Expand Down Expand Up @@ -66,7 +67,16 @@ export class DatePickerInput extends ElementMixin(DatePickerMixin(DatePickerMinM
const input = await this.$input;
if (input) {
const onBodyKeyup = (ev: KeyboardEvent) => {
if (ev.key === keyEscape) this.closePicker();
if (ev.key === keyEscape) {
this.closePicker();
} else if (ev.key === keyTab) {
const isTabInside = (ev.composedPath() as HTMLElement[]).find(
n => n.nodeType === Node.ELEMENT_NODE &&
this.isEqualNode(n)
);

if (!isTabInside) this.closePicker();
}
}
const onClick = () => this._open = true;
const onKeyup = (ev: KeyboardEvent) => {
Expand Down Expand Up @@ -205,7 +215,7 @@ export class DatePickerInput extends ElementMixin(DatePickerMixin(DatePickerMinM
aria-label=${this.clearLabel}
class="mdc-text-field__icon mdc-text-field__icon--trailing"
>
${iconClose}
${iconClear}
</mwc-icon-button>
`;
}
Expand Down Expand Up @@ -289,5 +299,3 @@ export class DatePickerInput extends ElementMixin(DatePickerMixin(DatePickerMinM
}
}
}

// FIXME: No focus trap in input surface or close input surface when focus is outside
2 changes: 1 addition & 1 deletion src/date-picker-input/stylings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const datePickerInputStyling = css`
}
.mdc-text-field__icon--trailing {
padding: 0 4px 0 0;
padding: 0;
}
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon--trailing {
Expand Down
2 changes: 1 addition & 1 deletion src/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import { svg } from 'lit/static-html.js';
export const iconArrowDropdown = svg`<svg height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M7 10l5 5 5-5z"/></svg>`;
export const iconChevronLeft = svg`<svg height="24" viewBox="0 0 24 24" width="24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path></svg>`;
export const iconChevronRight = svg`<svg height="24" viewBox="0 0 24 24" width="24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"></path></svg>`;
export const iconClose = svg`<svg height="24px" viewBox="0 0 24 24" width="24px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>`;
export const iconClear = svg`<svg height="24px" viewBox="0 0 24 24" width="24px"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>`;
// tslint:enable:max-line-length

0 comments on commit 5a10623

Please sign in to comment.