feat(input-datepicker): added input-datepicker component
Co-authored-by: Erik Kroes <erik.kroes@ing.com> Co-authored-by: Gerjan van Geest <gerjan.van.geest@ing.com> Co-authored-by: Mikhail Bashkirov <mikhail.bashkirov@ing.com>
This commit is contained in:
parent
144ebceb37
commit
9b110ca0f9
42 changed files with 1247 additions and 0 deletions
1
packages/input-datepicker/index.js
Normal file
1
packages/input-datepicker/index.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { LionInputDatepicker } from './src/LionInputDatepicker.js';
|
||||||
3
packages/input-datepicker/lion-input-datepicker.js
Normal file
3
packages/input-datepicker/lion-input-datepicker.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { LionInputDatepicker } from './src/LionInputDatepicker.js';
|
||||||
|
|
||||||
|
customElements.define('lion-input-datepicker', LionInputDatepicker);
|
||||||
49
packages/input-datepicker/package.json
Normal file
49
packages/input-datepicker/package.json
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"name": "@lion/input-datepicker",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Provide a way for users to fill in a date via a calendar overlay",
|
||||||
|
"author": "ing-bank",
|
||||||
|
"homepage": "https://github.com/ing-bank/lion/",
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/ing-bank/lion.git",
|
||||||
|
"directory": "packages/input-datepicker"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"prepublishOnly": "../../scripts/npm-prepublish.js"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"lion",
|
||||||
|
"web-components",
|
||||||
|
"input-date",
|
||||||
|
"input-datepicker",
|
||||||
|
"calendar",
|
||||||
|
"datepicker"
|
||||||
|
],
|
||||||
|
"main": "index.js",
|
||||||
|
"module": "index.js",
|
||||||
|
"files": [
|
||||||
|
"src",
|
||||||
|
"stories",
|
||||||
|
"test",
|
||||||
|
"*.js"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@lion/core": "^0.1.3",
|
||||||
|
"@lion/validate": "^0.1.3",
|
||||||
|
"@lion/input-date": "^0.1.3",
|
||||||
|
"@lion/overlays": "^0.1.3",
|
||||||
|
"@lion/calendar": "^0.1.2",
|
||||||
|
"@lion/localize": "^0.1.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@open-wc/demoing-storybook": "^0.2.0",
|
||||||
|
"@open-wc/testing": "^0.11.1",
|
||||||
|
"@lion/button": "^0.1.3",
|
||||||
|
"@polymer/iron-test-helpers": "^3.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
149
packages/input-datepicker/src/LionCalendarOverlayFrame.js
Normal file
149
packages/input-datepicker/src/LionCalendarOverlayFrame.js
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
import { html, css, LitElement, DomHelpersMixin } from '@lion/core';
|
||||||
|
import { LocalizeMixin } from '@lion/localize';
|
||||||
|
|
||||||
|
export class LionCalendarOverlayFrame extends LocalizeMixin(DomHelpersMixin(LitElement)) {
|
||||||
|
static get styles() {
|
||||||
|
return [
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
background: white;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-overlay__header {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-overlay__heading {
|
||||||
|
padding: 16px 16px 8px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-overlay__heading > .calendar-overlay__close-button {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-overlay__close-button {
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 32px;
|
||||||
|
border-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
static get localizeNamespaces() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
/* FIXME: This awful switch statement is used to make sure it works with polymer build.. */
|
||||||
|
'lion-calendar-overlay-frame': locale => {
|
||||||
|
switch (locale) {
|
||||||
|
case 'bg-BG':
|
||||||
|
return import('@lion/overlays/translations/bg-BG.js');
|
||||||
|
case 'bg':
|
||||||
|
return import('@lion/overlays/translations/bg.js');
|
||||||
|
case 'cs-CZ':
|
||||||
|
return import('@lion/overlays/translations/cs-CZ.js');
|
||||||
|
case 'cs':
|
||||||
|
return import('@lion/overlays/translations/cs.js');
|
||||||
|
case 'de-DE':
|
||||||
|
return import('@lion/overlays/translations/de-DE.js');
|
||||||
|
case 'de':
|
||||||
|
return import('@lion/overlays/translations/de.js');
|
||||||
|
case 'en-AU':
|
||||||
|
return import('@lion/overlays/translations/en-AU.js');
|
||||||
|
case 'en-GB':
|
||||||
|
return import('@lion/overlays/translations/en-GB.js');
|
||||||
|
case 'en-US':
|
||||||
|
return import('@lion/overlays/translations/en-US.js');
|
||||||
|
case 'en':
|
||||||
|
return import('@lion/overlays/translations/en.js');
|
||||||
|
case 'es-ES':
|
||||||
|
return import('@lion/overlays/translations/es-ES.js');
|
||||||
|
case 'es':
|
||||||
|
return import('@lion/overlays/translations/es.js');
|
||||||
|
case 'fr-FR':
|
||||||
|
return import('@lion/overlays/translations/fr-FR.js');
|
||||||
|
case 'fr-BE':
|
||||||
|
return import('@lion/overlays/translations/fr-BE.js');
|
||||||
|
case 'fr':
|
||||||
|
return import('@lion/overlays/translations/fr.js');
|
||||||
|
case 'hu-HU':
|
||||||
|
return import('@lion/overlays/translations/hu-HU.js');
|
||||||
|
case 'hu':
|
||||||
|
return import('@lion/overlays/translations/hu.js');
|
||||||
|
case 'it-IT':
|
||||||
|
return import('@lion/overlays/translations/it-IT.js');
|
||||||
|
case 'it':
|
||||||
|
return import('@lion/overlays/translations/it.js');
|
||||||
|
case 'nl-BE':
|
||||||
|
return import('@lion/overlays/translations/nl-BE.js');
|
||||||
|
case 'nl-NL':
|
||||||
|
return import('@lion/overlays/translations/nl-NL.js');
|
||||||
|
case 'nl':
|
||||||
|
return import('@lion/overlays/translations/nl.js');
|
||||||
|
case 'pl-PL':
|
||||||
|
return import('@lion/overlays/translations/pl-PL.js');
|
||||||
|
case 'pl':
|
||||||
|
return import('@lion/overlays/translations/pl.js');
|
||||||
|
case 'ro-RO':
|
||||||
|
return import('@lion/overlays/translations/ro-RO.js');
|
||||||
|
case 'ro':
|
||||||
|
return import('@lion/overlays/translations/ro.js');
|
||||||
|
case 'ru-RU':
|
||||||
|
return import('@lion/overlays/translations/ru-RU.js');
|
||||||
|
case 'ru':
|
||||||
|
return import('@lion/overlays/translations/ru.js');
|
||||||
|
case 'sk-SK':
|
||||||
|
return import('@lion/overlays/translations/sk-SK.js');
|
||||||
|
case 'sk':
|
||||||
|
return import('@lion/overlays/translations/sk.js');
|
||||||
|
case 'uk-UA':
|
||||||
|
return import('@lion/overlays/translations/uk-UA.js');
|
||||||
|
case 'uk':
|
||||||
|
return import('@lion/overlays/translations/uk.js');
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown locale: ${locale}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...super.localizeNamespaces,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.__dispatchCloseEvent = this.__dispatchCloseEvent.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
__dispatchCloseEvent() {
|
||||||
|
// Designed to work in conjunction with ModalDialogController
|
||||||
|
this.dispatchEvent(new CustomEvent('dialog-close'), { bubbles: true, composed: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// eslint-disable-line class-methods-use-this
|
||||||
|
return html`
|
||||||
|
<div class="calendar-overlay">
|
||||||
|
<div class="calendar-overlay__header">
|
||||||
|
<h1 class="calendar-overlay__heading">
|
||||||
|
<slot name="heading"></slot>
|
||||||
|
</h1>
|
||||||
|
<button
|
||||||
|
@click="${this.__dispatchCloseEvent}"
|
||||||
|
id="close-button"
|
||||||
|
title="${this.msgLit('lion-calendar-overlay-frame:close')}"
|
||||||
|
aria-label="${this.msgLit('lion-calendar-overlay-frame:close')}"
|
||||||
|
class="calendar-overlay__close-button"
|
||||||
|
>
|
||||||
|
<slot name="close-icon">×</slot>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
365
packages/input-datepicker/src/LionInputDatepicker.js
Normal file
365
packages/input-datepicker/src/LionInputDatepicker.js
Normal file
|
|
@ -0,0 +1,365 @@
|
||||||
|
import { html, render, ifDefined } from '@lion/core';
|
||||||
|
import { LionInputDate } from '@lion/input-date';
|
||||||
|
import { overlays, ModalDialogController } from '@lion/overlays';
|
||||||
|
import { Unparseable, isValidatorApplied } from '@lion/validate';
|
||||||
|
import '@lion/calendar/lion-calendar.js';
|
||||||
|
import './lion-calendar-overlay-frame.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @customElement
|
||||||
|
* @extends {LionInputDate}
|
||||||
|
*/
|
||||||
|
export class LionInputDatepicker extends LionInputDate {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
...super.properties,
|
||||||
|
/**
|
||||||
|
* The heading to be added on top of the calendar overlay.
|
||||||
|
* Naming chosen from an Application Developer perspective.
|
||||||
|
* For a Subclasser 'calendarOverlayHeading' would be more appropriate
|
||||||
|
*/
|
||||||
|
calendarHeading: {
|
||||||
|
type: String,
|
||||||
|
attribute: 'calendar-heading',
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The slot to put the invoker button in. Can be 'prefix', 'suffix', 'before' and 'after'.
|
||||||
|
* Default will be 'suffix'.
|
||||||
|
*/
|
||||||
|
_calendarInvokerSlot: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: [delegation of disabled] move this to LionField (or FormControl) level
|
||||||
|
*/
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
|
||||||
|
__calendarMinDate: {
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
|
|
||||||
|
__calendarMaxDate: {
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
|
|
||||||
|
__calendarDisableDates: {
|
||||||
|
type: Function,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get slots() {
|
||||||
|
return {
|
||||||
|
...super.slots,
|
||||||
|
[this._calendarInvokerSlot]: () => this.__createPickerAndReturnInvokerNode(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get localizeNamespaces() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
/* FIXME: This awful switch statement is used to make sure it works with polymer build.. */
|
||||||
|
'lion-input-datepicker': locale => {
|
||||||
|
switch (locale) {
|
||||||
|
case 'bg-BG':
|
||||||
|
return import('../translations/bg-BG.js');
|
||||||
|
case 'bg':
|
||||||
|
return import('../translations/bg.js');
|
||||||
|
case 'cs-CZ':
|
||||||
|
return import('../translations/cs-CZ.js');
|
||||||
|
case 'cs':
|
||||||
|
return import('../translations/cs.js');
|
||||||
|
case 'de-DE':
|
||||||
|
return import('../translations/de-DE.js');
|
||||||
|
case 'de':
|
||||||
|
return import('../translations/de.js');
|
||||||
|
case 'en-AU':
|
||||||
|
return import('../translations/en-AU.js');
|
||||||
|
case 'en-GB':
|
||||||
|
return import('../translations/en-GB.js');
|
||||||
|
case 'en-US':
|
||||||
|
return import('../translations/en-US.js');
|
||||||
|
case 'en':
|
||||||
|
return import('../translations/en.js');
|
||||||
|
case 'es-ES':
|
||||||
|
return import('../translations/es-ES.js');
|
||||||
|
case 'es':
|
||||||
|
return import('../translations/es.js');
|
||||||
|
case 'fr-FR':
|
||||||
|
return import('../translations/fr-FR.js');
|
||||||
|
case 'fr-BE':
|
||||||
|
return import('../translations/fr-BE.js');
|
||||||
|
case 'fr':
|
||||||
|
return import('../translations/fr.js');
|
||||||
|
case 'hu-HU':
|
||||||
|
return import('../translations/hu-HU.js');
|
||||||
|
case 'hu':
|
||||||
|
return import('../translations/hu.js');
|
||||||
|
case 'it-IT':
|
||||||
|
return import('../translations/it-IT.js');
|
||||||
|
case 'it':
|
||||||
|
return import('../translations/it.js');
|
||||||
|
case 'nl-BE':
|
||||||
|
return import('../translations/nl-BE.js');
|
||||||
|
case 'nl-NL':
|
||||||
|
return import('../translations/nl-NL.js');
|
||||||
|
case 'nl':
|
||||||
|
return import('../translations/nl.js');
|
||||||
|
case 'pl-PL':
|
||||||
|
return import('../translations/pl-PL.js');
|
||||||
|
case 'pl':
|
||||||
|
return import('../translations/pl.js');
|
||||||
|
case 'ro-RO':
|
||||||
|
return import('../translations/ro-RO.js');
|
||||||
|
case 'ro':
|
||||||
|
return import('../translations/ro.js');
|
||||||
|
case 'ru-RU':
|
||||||
|
return import('../translations/ru-RU.js');
|
||||||
|
case 'ru':
|
||||||
|
return import('../translations/ru.js');
|
||||||
|
case 'sk-SK':
|
||||||
|
return import('../translations/sk-SK.js');
|
||||||
|
case 'sk':
|
||||||
|
return import('../translations/sk.js');
|
||||||
|
case 'uk-UA':
|
||||||
|
return import('../translations/uk-UA.js');
|
||||||
|
case 'uk':
|
||||||
|
return import('../translations/uk.js');
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown locale: ${locale}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...super.localizeNamespaces,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
get _invokerElement() {
|
||||||
|
return this.querySelector(`#${this.__invokerId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
get _calendarOverlayElement() {
|
||||||
|
return this._overlayCtrl._container.firstElementChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
get _calendarElement() {
|
||||||
|
return this._calendarOverlayElement.querySelector('#calendar');
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
// Create a unique id for the invoker, since it is placed in light dom for a11y.
|
||||||
|
this.__invokerId = this.__createUniqueIdForA11y();
|
||||||
|
this._calendarInvokerSlot = 'suffix';
|
||||||
|
|
||||||
|
// Configuration flags for subclassers
|
||||||
|
this._focusCentralDateOnCalendarOpen = true;
|
||||||
|
this._hideOnUserSelect = true;
|
||||||
|
this._syncOnUserSelect = true;
|
||||||
|
|
||||||
|
this.__openCalendarOverlay = this.__openCalendarOverlay.bind(this);
|
||||||
|
this._onCalendarUserSelectedChanged = this._onCalendarUserSelectedChanged.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
__createUniqueIdForA11y() {
|
||||||
|
return `${this.localName}-${Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.substr(2, 10)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Problem: we need to create a getter for disabled that puts disabled attrs on the invoker
|
||||||
|
* button.
|
||||||
|
* The DelegateMixin creates getters and setters regardless of what's defined on the prototype,
|
||||||
|
* thats why we need to move it out from parent delegations config, in order to make our own
|
||||||
|
* getters and setters work.
|
||||||
|
*
|
||||||
|
* TODO: [delegation of disabled] fix this on a global level:
|
||||||
|
* - LionField
|
||||||
|
* - move all delegations of attrs and props to static get props for docs
|
||||||
|
* - DelegateMixin needs to be refactored, so that it:
|
||||||
|
* - gets config from static get properties
|
||||||
|
* - hooks into _requestUpdate
|
||||||
|
*/
|
||||||
|
get delegations() {
|
||||||
|
return {
|
||||||
|
...super.delegations,
|
||||||
|
properties: super.delegations.properties.filter(p => p !== 'disabled'),
|
||||||
|
attributes: super.delegations.attributes.filter(p => p !== 'disabled'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: [delegation of disabled] move this to LionField (or FormControl) level
|
||||||
|
*/
|
||||||
|
_requestUpdate(name, oldValue) {
|
||||||
|
super._requestUpdate(name, oldValue);
|
||||||
|
if (name === 'disabled') {
|
||||||
|
this.__delegateDisabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: [delegation of disabled] move this to LionField (or FormControl) level
|
||||||
|
*/
|
||||||
|
__delegateDisabled() {
|
||||||
|
if (this.delegations.target()) {
|
||||||
|
this.delegations.target().disabled = this.disabled;
|
||||||
|
}
|
||||||
|
if (this._invokerElement) {
|
||||||
|
this._invokerElement.disabled = this.disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: [delegation of disabled] move this to LionField (or FormControl) level
|
||||||
|
*/
|
||||||
|
firstUpdated(c) {
|
||||||
|
super.firstUpdated(c);
|
||||||
|
this.__delegateDisabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* @param {Map} c - changed properties
|
||||||
|
*/
|
||||||
|
updated(c) {
|
||||||
|
super.updated(c);
|
||||||
|
|
||||||
|
if (c.has('errorValidators') || c.has('warningValidators')) {
|
||||||
|
const validators = [...(this.warningValidators || []), ...(this.errorValidators || [])];
|
||||||
|
this.__syncDisabledDates(validators);
|
||||||
|
}
|
||||||
|
if (c.has('label')) {
|
||||||
|
this.calendarHeading = this.calendarHeading || this.label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_calendarOverlayTemplate() {
|
||||||
|
return html`
|
||||||
|
<lion-calendar-overlay-frame>
|
||||||
|
<span slot="heading">${this.calendarHeading}</span>
|
||||||
|
${this._calendarTemplate()}
|
||||||
|
</lion-calendar-overlay-frame>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subclassers can replace this with their custom extension of
|
||||||
|
* LionCalendar, like `<my-calendar id="calendar"></my-calendar>`
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_calendarTemplate() {
|
||||||
|
return html`
|
||||||
|
<lion-calendar
|
||||||
|
id="calendar"
|
||||||
|
.selectedDate="${this.constructor.__getSyncDownValue(this.modelValue)}"
|
||||||
|
.minDate="${this.__calendarMinDate}"
|
||||||
|
.maxDate="${this.__calendarMaxDate}"
|
||||||
|
.disableDates="${ifDefined(this.__calendarDisableDates)}"
|
||||||
|
@user-selected-date-changed="${this._onCalendarUserSelectedChanged}"
|
||||||
|
></lion-calendar>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subclassers can replace this with their custom extension invoker,
|
||||||
|
* like `<my-button><calendar-icon></calendar-icon></my-button>`
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_invokerTemplate() {
|
||||||
|
// TODO: aria-expanded should be toggled by Overlay system, to allow local overlays
|
||||||
|
// (a.k.a. dropdowns) as well
|
||||||
|
return html`
|
||||||
|
<button
|
||||||
|
@click="${this.__openCalendarOverlay}"
|
||||||
|
id="${this.__invokerId}"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-label="${this.msgLit('lion-input-datepicker:openDatepickerLabel')}"
|
||||||
|
title="${this.msgLit('lion-input-datepicker:openDatepickerLabel')}"
|
||||||
|
>
|
||||||
|
📅
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders the invoker button + the calendar overlay invoked by this button
|
||||||
|
__createPickerAndReturnInvokerNode() {
|
||||||
|
const renderParent = document.createElement('div');
|
||||||
|
render(this._invokerTemplate(), renderParent);
|
||||||
|
const invokerNode = renderParent.firstElementChild;
|
||||||
|
|
||||||
|
// TODO: ModalDialogController should be replaced by a more flexible
|
||||||
|
// overlay, allowing the overlay to switch on smaller screens, for instance from dropdown to
|
||||||
|
// bottom sheet (working name for this controller: ResponsiveOverlayController)
|
||||||
|
this._overlayCtrl = overlays.add(
|
||||||
|
new ModalDialogController({
|
||||||
|
contentTemplate: () => this._calendarOverlayTemplate(),
|
||||||
|
elementToFocusAfterHide: invokerNode,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return invokerNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
async __openCalendarOverlay() {
|
||||||
|
this._overlayCtrl.show();
|
||||||
|
await this._calendarElement.updateComplete;
|
||||||
|
this._onCalendarOverlayOpened();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle callback for subclassers
|
||||||
|
*/
|
||||||
|
_onCalendarOverlayOpened() {
|
||||||
|
if (this._focusCentralDateOnCalendarOpen) {
|
||||||
|
this._calendarElement.focusCentralDate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onCalendarUserSelectedChanged({ target: { selectedDate } }) {
|
||||||
|
if (this._hideOnUserSelect) {
|
||||||
|
this._overlayCtrl.hide();
|
||||||
|
}
|
||||||
|
if (this._syncOnUserSelect) {
|
||||||
|
// Synchronize new selectedDate value to input
|
||||||
|
this.modelValue = selectedDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The LionCalendar shouldn't know anything about the modelValue;
|
||||||
|
* it can't handle Unparseable dates, but does handle 'undefined'
|
||||||
|
* @returns {Date|undefined} a 'guarded' modelValue
|
||||||
|
*/
|
||||||
|
static __getSyncDownValue(modelValue) {
|
||||||
|
return modelValue instanceof Unparseable ? undefined : modelValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validators contain the information to synchronize the input with
|
||||||
|
* the min, max and enabled dates of the calendar.
|
||||||
|
* @param {Array} validators - errorValidators or warningValidators array
|
||||||
|
*/
|
||||||
|
__syncDisabledDates(validators) {
|
||||||
|
// On every validator change, synchronize disabled dates: this means
|
||||||
|
// we need to extract minDate, maxDate, minMaxDate and disabledDates validators
|
||||||
|
validators.forEach(([fn, param]) => {
|
||||||
|
const d = new Date();
|
||||||
|
|
||||||
|
if (isValidatorApplied('minDate', fn, d)) {
|
||||||
|
this.__calendarMinDate = param;
|
||||||
|
} else if (isValidatorApplied('maxDate', fn, d)) {
|
||||||
|
this.__calendarMaxDate = param;
|
||||||
|
} else if (isValidatorApplied('minMaxDate', fn, { min: d, max: d })) {
|
||||||
|
this.__calendarMinDate = param.min;
|
||||||
|
this.__calendarMaxDate = param.max;
|
||||||
|
} else if (isValidatorApplied('isDateDisabled', fn, () => true)) {
|
||||||
|
this.__calendarDisableDates = param;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { LionCalendarOverlayFrame } from './LionCalendarOverlayFrame.js';
|
||||||
|
|
||||||
|
customElements.define('lion-calendar-overlay-frame', LionCalendarOverlayFrame);
|
||||||
54
packages/input-datepicker/stories/index.stories.js
Normal file
54
packages/input-datepicker/stories/index.stories.js
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { storiesOf, html } from '@open-wc/demoing-storybook';
|
||||||
|
import { isDateDisabledValidator, minMaxDateValidator } from '@lion/validate';
|
||||||
|
import '../lion-input-datepicker.js';
|
||||||
|
|
||||||
|
storiesOf('Forms|Input Datepicker', module)
|
||||||
|
.add(
|
||||||
|
'Default',
|
||||||
|
() => html`
|
||||||
|
<lion-input-datepicker label="Date" .modelValue=${new Date('2017/06/15')}>
|
||||||
|
</lion-input-datepicker>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
'minMaxDateValidator',
|
||||||
|
() => html`
|
||||||
|
<lion-input-datepicker
|
||||||
|
label="MinMaxDate"
|
||||||
|
help-text="Enter a date between '2018/05/24' and '2018/06/24'"
|
||||||
|
.modelValue=${new Date('2018/05/30')}
|
||||||
|
.errorValidators=${[
|
||||||
|
minMaxDateValidator({ min: new Date('2018/05/24'), max: new Date('2018/06/24') }),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
</lion-input-datepicker>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
'disabledDatesValidator',
|
||||||
|
() => html`
|
||||||
|
<lion-input-datepicker
|
||||||
|
label="disabledDates"
|
||||||
|
help-text="You're not allowed to choose the 15th"
|
||||||
|
.errorValidators=${[isDateDisabledValidator(d => d.getDate() === 15)]}
|
||||||
|
>
|
||||||
|
</lion-input-datepicker>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
'With calendar-heading',
|
||||||
|
() => html`
|
||||||
|
<lion-input-datepicker
|
||||||
|
label="Date"
|
||||||
|
.calendarHeading="${'Custom heading'}"
|
||||||
|
.modelValue=${new Date()}
|
||||||
|
>
|
||||||
|
</lion-input-datepicker>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
'Disabled',
|
||||||
|
() => html`
|
||||||
|
<lion-input-datepicker disabled></lion-input-datepicker>
|
||||||
|
`,
|
||||||
|
);
|
||||||
425
packages/input-datepicker/test/lion-input-datepicker.test.js
Normal file
425
packages/input-datepicker/test/lion-input-datepicker.test.js
Normal file
|
|
@ -0,0 +1,425 @@
|
||||||
|
import { expect, fixture, aTimeout, defineCE } from '@open-wc/testing';
|
||||||
|
import { localizeTearDown } from '@lion/localize/test-helpers.js';
|
||||||
|
import { html, LitElement } from '@lion/core';
|
||||||
|
import {
|
||||||
|
maxDateValidator,
|
||||||
|
minDateValidator,
|
||||||
|
minMaxDateValidator,
|
||||||
|
isDateDisabledValidator,
|
||||||
|
} from '@lion/validate';
|
||||||
|
import { keyCodes } from '@lion/overlays/src/utils/key-codes.js';
|
||||||
|
import { keyUpOn } from '@polymer/iron-test-helpers/mock-interactions.js';
|
||||||
|
import { LionCalendar } from '@lion/calendar';
|
||||||
|
import { isSameDate } from '@lion/calendar/src/utils/isSameDate.js';
|
||||||
|
import { DatepickerInputObject } from './test-utils.js';
|
||||||
|
import { LionInputDatepicker } from '../src/LionInputDatepicker.js';
|
||||||
|
import '../lion-input-datepicker.js';
|
||||||
|
|
||||||
|
describe('<lion-input-datepicker>', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localizeTearDown();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Calendar Overlay', () => {
|
||||||
|
it('implements calendar-overlay Style component', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-input-datepicker></lion-input-datepicker>
|
||||||
|
`);
|
||||||
|
const elObj = new DatepickerInputObject(el);
|
||||||
|
await elObj.openCalendar();
|
||||||
|
|
||||||
|
expect(elObj.overlayEl.shadowRoot.querySelector('.calendar-overlay')).not.to.equal(null);
|
||||||
|
expect(elObj.overlayEl.shadowRoot.querySelector('.calendar-overlay__header')).not.to.equal(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
expect(elObj.overlayEl.shadowRoot.querySelector('.calendar-overlay__heading')).not.to.equal(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
elObj.overlayEl.shadowRoot.querySelector('.calendar-overlay__close-button'),
|
||||||
|
).not.to.equal(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip('activates full screen mode on mobile screens', async () => {
|
||||||
|
// TODO: should this be part of globalOverlayController as option?
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a close button, with a tooltip "Close"', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-input-datepicker></lion-input-datepicker>
|
||||||
|
`);
|
||||||
|
const elObj = new DatepickerInputObject(el);
|
||||||
|
await elObj.openCalendar();
|
||||||
|
// Since tooltip not ready, use title which can be progressively enhanced in extension layers.
|
||||||
|
expect(elObj.overlayCloseButtonEl.getAttribute('title')).to.equal('Close');
|
||||||
|
expect(elObj.overlayCloseButtonEl.getAttribute('aria-label')).to.equal('Close');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a default title based on input label', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-input-datepicker
|
||||||
|
.label="${'Pick your date'}"
|
||||||
|
.modelValue="${new Date('2020-02-15')}"
|
||||||
|
></lion-input-datepicker>
|
||||||
|
`);
|
||||||
|
const elObj = new DatepickerInputObject(el);
|
||||||
|
await elObj.openCalendar();
|
||||||
|
expect(
|
||||||
|
elObj.overlayHeadingEl.querySelector('slot[name="heading"]').assignedNodes()[0],
|
||||||
|
).lightDom.to.equal('Pick your date');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can have a custom heading', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-input-datepicker
|
||||||
|
label="Pick your date"
|
||||||
|
calendar-heading="foo"
|
||||||
|
></lion-input-datepicker>
|
||||||
|
`);
|
||||||
|
const elObj = new DatepickerInputObject(el);
|
||||||
|
await elObj.openCalendar();
|
||||||
|
expect(
|
||||||
|
elObj.overlayHeadingEl.querySelector('slot[name="heading"]').assignedNodes()[0],
|
||||||
|
).lightDom.to.equal('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: fix the Overlay system, so that the backdrop/body cannot be focused
|
||||||
|
it('closes the calendar on [esc] key', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-input-datepicker></lion-input-datepicker>
|
||||||
|
`);
|
||||||
|
const elObj = new DatepickerInputObject(el);
|
||||||
|
await elObj.openCalendar();
|
||||||
|
expect(elObj.overlayController.isShown).to.equal(true);
|
||||||
|
// Mimic user input: should fire the 'selected-date-changed' event
|
||||||
|
// Make sure focus is inside the calendar/overlay
|
||||||
|
keyUpOn(elObj.calendarEl, keyCodes.escape);
|
||||||
|
expect(elObj.overlayController.isShown).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not in scope:
|
||||||
|
* - centralDate can be overridden
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Calendar Invoker', () => {
|
||||||
|
it('adds invoker button that toggles the overlay on click in suffix slot ', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-input-datepicker></lion-input-datepicker>
|
||||||
|
`);
|
||||||
|
const elObj = new DatepickerInputObject(el);
|
||||||
|
expect(elObj.invokerEl).not.to.equal(null);
|
||||||
|
expect(elObj.overlayController.isShown).to.be.false;
|
||||||
|
await elObj.openCalendar();
|
||||||
|
expect(elObj.overlayController.isShown).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Relies on delegation of disabled property to invoker.
|
||||||
|
// TODO: consider making this (delegation to interactive child nodes) generic functionality
|
||||||
|
// inside LionField/FormControl. Or, for maximum flexibility, add a config attr
|
||||||
|
// to the invoker node like 'data-disabled-is-delegated'
|
||||||
|
it('delegates disabled state of host input', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-input-datepicker disabled></lion-input-datepicker>
|
||||||
|
`);
|
||||||
|
const elObj = new DatepickerInputObject(el);
|
||||||
|
expect(elObj.overlayController.isShown).to.equal(false);
|
||||||
|
await elObj.openCalendar();
|
||||||
|
expect(elObj.overlayController.isShown).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Input - calendar synchronization', () => {
|
||||||
|
it('syncs modelValue with lion-calendar', async () => {
|
||||||
|
const myDate = new Date('2019/06/15');
|
||||||
|
const myOtherDate = new Date('2019/06/28');
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-input-datepicker .modelValue="${myDate}"></lion-input-datepicker>
|
||||||
|
`);
|
||||||
|
const elObj = new DatepickerInputObject(el);
|
||||||
|
await elObj.openCalendar();
|
||||||
|
expect(elObj.calendarEl.selectedDate).to.equal(myDate);
|
||||||
|
await elObj.selectMonthDay(myOtherDate.getDate());
|
||||||
|
expect(isSameDate(el.modelValue, myOtherDate)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes the calendar overlay on "user-selected-date-changed"', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-input-datepicker></lion-input-datepicker>
|
||||||
|
`);
|
||||||
|
const elObj = new DatepickerInputObject(el);
|
||||||
|
// Make sure the calendar overlay is opened
|
||||||
|
await elObj.openCalendar();
|
||||||
|
expect(elObj.overlayController.isShown).to.equal(true);
|
||||||
|
// Mimic user input: should fire the 'user-selected-date-changed' event
|
||||||
|
await elObj.selectMonthDay(12);
|
||||||
|
expect(elObj.overlayController.isShown).to.equal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('focuses interactable date on opening of calendar', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-input-datepicker></lion-input-datepicker>
|
||||||
|
`);
|
||||||
|
const elObj = new DatepickerInputObject(el);
|
||||||
|
await elObj.openCalendar();
|
||||||
|
await aTimeout();
|
||||||
|
expect(elObj.calendarObj.focusedDayObj.el).not.to.equal(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Validators', () => {
|
||||||
|
/**
|
||||||
|
* Validators are the Application Developer facing API in <lion-input-datepicker>:
|
||||||
|
* - setting restrictions on min/max/disallowed dates will be done via validators
|
||||||
|
* - all validators will be translated under the hood to enabledDates and passed to
|
||||||
|
* lion-calendar
|
||||||
|
*/
|
||||||
|
it('converts isDateDisabledValidator to "disableDates" property', async () => {
|
||||||
|
const no15th = d => d.getDate() !== 15;
|
||||||
|
const no16th = d => d.getDate() !== 16;
|
||||||
|
const no15thOr16th = d => no15th(d) && no16th(d);
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-input-datepicker .errorValidators="${[isDateDisabledValidator(no15thOr16th)]}">
|
||||||
|
</lion-input-datepicker>
|
||||||
|
`);
|
||||||
|
const elObj = new DatepickerInputObject(el);
|
||||||
|
await elObj.openCalendar();
|
||||||
|
|
||||||
|
expect(elObj.calendarEl.disableDates).to.equal(no15thOr16th);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts minDateValidator to "minDate" property', async () => {
|
||||||
|
const myMinDate = new Date('2019/06/15');
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-input-datepicker .errorValidators=${[minDateValidator(myMinDate)]}>
|
||||||
|
</lion-input-date>`);
|
||||||
|
const elObj = new DatepickerInputObject(el);
|
||||||
|
await elObj.openCalendar();
|
||||||
|
|
||||||
|
expect(elObj.calendarEl.minDate).to.equal(myMinDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts maxDateValidator to "maxDate" property', async () => {
|
||||||
|
const myMaxDate = new Date('2030/06/15');
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-input-datepicker .errorValidators=${[maxDateValidator(myMaxDate)]}>
|
||||||
|
</lion-input-datepicker>
|
||||||
|
`);
|
||||||
|
const elObj = new DatepickerInputObject(el);
|
||||||
|
await elObj.openCalendar();
|
||||||
|
|
||||||
|
expect(elObj.calendarEl.maxDate).to.equal(myMaxDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts minMaxDateValidator to "minDate" and "maxDate" property', async () => {
|
||||||
|
const myMinDate = new Date('2019/06/15');
|
||||||
|
const myMaxDate = new Date('2030/06/15');
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-input-datepicker
|
||||||
|
.errorValidators=${[minMaxDateValidator({ min: myMinDate, max: myMaxDate })]}
|
||||||
|
>
|
||||||
|
</lion-input-datepicker>
|
||||||
|
`);
|
||||||
|
const elObj = new DatepickerInputObject(el);
|
||||||
|
await elObj.openCalendar();
|
||||||
|
|
||||||
|
expect(elObj.calendarEl.minDate).to.equal(myMinDate);
|
||||||
|
expect(elObj.calendarEl.maxDate).to.equal(myMaxDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not in scope:
|
||||||
|
* - min/max attr (like platform has): could be added in future if observers needed
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Accessibility', () => {
|
||||||
|
it('has a heading of level 1', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-input-datepicker calendar-heading="foo"></lion-input-datepicker>
|
||||||
|
`);
|
||||||
|
const elObj = new DatepickerInputObject(el);
|
||||||
|
await elObj.openCalendar();
|
||||||
|
|
||||||
|
const hNode = elObj.overlayHeadingEl;
|
||||||
|
const headingIsLevel1 =
|
||||||
|
hNode.tagName === 'H1' ||
|
||||||
|
(hNode.getAttribute('role') === 'heading' && hNode.getAttribute('aria-level') === '1');
|
||||||
|
expect(headingIsLevel1).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds accessible label to invoker button', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-input-datepicker></lion-input-datepicker>
|
||||||
|
`);
|
||||||
|
const elObj = new DatepickerInputObject(el);
|
||||||
|
await elObj.openCalendar();
|
||||||
|
|
||||||
|
expect(elObj.invokerEl.getAttribute('title')).to.equal('Open date picker');
|
||||||
|
expect(elObj.invokerEl.getAttribute('aria-label')).to.equal('Open date picker');
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: move this functionality to GlobalOverlay
|
||||||
|
it('adds aria-haspopup="dialog" and aria-expanded="true" to invoker button', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-input-datepicker></lion-input-datepicker>
|
||||||
|
`);
|
||||||
|
const elObj = new DatepickerInputObject(el);
|
||||||
|
|
||||||
|
expect(elObj.invokerEl.getAttribute('aria-haspopup')).to.equal('dialog');
|
||||||
|
expect(elObj.invokerEl.getAttribute('aria-expanded')).to.equal('false');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.skip('Subclassers', () => {
|
||||||
|
describe('Providing a custom invoker', () => {
|
||||||
|
it('can override the invoker template', async () => {
|
||||||
|
const myTag = defineCE(
|
||||||
|
class extends LionInputDatepicker {
|
||||||
|
/** @override */
|
||||||
|
_invokerTemplate() {
|
||||||
|
return html`
|
||||||
|
<my-button>Pick my date</my-button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const myEl = await fixture(`<${myTag}></${myTag}>`);
|
||||||
|
const myElObj = new DatepickerInputObject(myEl);
|
||||||
|
expect(myElObj.invokerEl.tagName.toLowerCase()).to.equal('my-button');
|
||||||
|
|
||||||
|
// All other tests will still pass. Small checkup:
|
||||||
|
expect(myElObj.invokerEl.getAttribute('title')).to.equal('Open date picker');
|
||||||
|
expect(myElObj.invokerEl.getAttribute('aria-label')).to.equal('Open date picker');
|
||||||
|
expect(myElObj.invokerEl.getAttribute('aria-expanded')).to.equal('false');
|
||||||
|
expect(myElObj.invokerEl.getAttribute('aria-haspopup')).to.equal('dialog');
|
||||||
|
expect(myElObj.invokerEl.getAttribute('slot')).to.equal('suffix');
|
||||||
|
expect(myElObj.invokerEl.getAttribute('id')).to.equal(myEl.__invokerId);
|
||||||
|
await myElObj.openCalendar();
|
||||||
|
expect(myElObj.overlayController.isShown).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can allocate the picker in a different slot supported by LionField', async () => {
|
||||||
|
/**
|
||||||
|
* It's important that this api is used instead of Subclassers providing a slot.
|
||||||
|
* When the input-datepicker knows where the calendar invoker is, it can attach
|
||||||
|
* the right logic, localization and accessibility functionality.
|
||||||
|
*/
|
||||||
|
const myTag = defineCE(
|
||||||
|
class extends LionInputDatepicker {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._calendarInvokerSlot = 'prefix';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const myEl = await fixture(`<${myTag}></${myTag}>`);
|
||||||
|
const myElObj = new DatepickerInputObject(myEl);
|
||||||
|
expect(myElObj.invokerEl.getAttribute('slot')).to.equal('prefix');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Providing a custom calendar', () => {
|
||||||
|
it('can override the calendar template', async () => {
|
||||||
|
customElements.define(
|
||||||
|
'my-calendar',
|
||||||
|
class extends LionCalendar {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
// Change some defaults
|
||||||
|
this.firstDayOfWeek = 1; // Start on Mondays instead of Sundays
|
||||||
|
this.weekdayHeaderNotation = 'narrow'; // 'T' instead of 'Thu'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const myTag = defineCE(
|
||||||
|
class extends LionInputDatepicker {
|
||||||
|
_calendarTemplate() {
|
||||||
|
return html`
|
||||||
|
<my-calendar id="calendar"></my-calendar>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const myEl = await fixture(`<${myTag}></${myTag}>`);
|
||||||
|
const myElObj = new DatepickerInputObject(myEl);
|
||||||
|
|
||||||
|
// All other tests will still pass. Small checkup:
|
||||||
|
await myElObj.openCalendar();
|
||||||
|
expect(myElObj.calendarEl.tagName.toLowerCase()).to.equal('my-calendar');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Providing a custom overlay', () => {
|
||||||
|
it('can override the overlay template', async () => {
|
||||||
|
// Keep in mind there is no logic inside this overlay frame; it only handles visuals.
|
||||||
|
// All interaction should be delegated to parent, which interacts with the calendar
|
||||||
|
// component
|
||||||
|
customElements.define(
|
||||||
|
'my-calendar-overlay-frame',
|
||||||
|
class extends LitElement {
|
||||||
|
render() {
|
||||||
|
// eslint-disable-line class-methods-use-this
|
||||||
|
return html`
|
||||||
|
<div class="c-calendar-overlay">
|
||||||
|
<slot></slot>
|
||||||
|
<div class="c-calendar-overlay__footer">
|
||||||
|
<button id="cancel-button" class="c-calendar-overlay__cancel-button">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let myOverlayOpenedCbHandled = false;
|
||||||
|
let myUserSelectedChangedCbHandled = false;
|
||||||
|
|
||||||
|
const myTag = defineCE(
|
||||||
|
class extends LionInputDatepicker {
|
||||||
|
/** @override */
|
||||||
|
_calendarOverlayTemplate() {
|
||||||
|
return html`
|
||||||
|
<my-calendar-overlay-frame id="calendar-overlay">
|
||||||
|
<span slot="heading">${this.calendarHeading}</span>
|
||||||
|
${this._calendarTemplateConfig(this._calendarTemplate())}
|
||||||
|
</my-calendar-overlay-frame>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
_onCalendarOverlayOpened(...args) {
|
||||||
|
super._onCalendarOverlayOpened(...args);
|
||||||
|
myOverlayOpenedCbHandled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
_onCalendarUserSelectedChanged(...args) {
|
||||||
|
super._onCalendarUserSelectedChanged(...args);
|
||||||
|
myUserSelectedChangedCbHandled = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const myEl = await fixture(`<${myTag}></${myTag}>`);
|
||||||
|
const myElObj = new DatepickerInputObject(myEl);
|
||||||
|
|
||||||
|
// All other tests will still pass. Small checkup:
|
||||||
|
await myElObj.openCalendar();
|
||||||
|
expect(myElObj.overlayEl.tagName.toLowerCase()).to.equal('my-calendar-overlay-frame');
|
||||||
|
expect(myOverlayOpenedCbHandled).to.be.true;
|
||||||
|
await myElObj.selectMonthDay(1);
|
||||||
|
expect(myUserSelectedChangedCbHandled).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip('can configure the overlay presentation based on media query switch', async () => {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
65
packages/input-datepicker/test/test-utils.js
Normal file
65
packages/input-datepicker/test/test-utils.js
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { CalendarObject } from '@lion/calendar/test/test-utils.js';
|
||||||
|
|
||||||
|
// TODO: refactor CalendarObject to this approach (only methods when arguments are needed)
|
||||||
|
export class DatepickerInputObject {
|
||||||
|
constructor(el) {
|
||||||
|
this.el = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Methods mimicing User Interaction
|
||||||
|
*/
|
||||||
|
|
||||||
|
async openCalendar() {
|
||||||
|
// Make sure the calendar is opened, not closed/toggled;
|
||||||
|
this.overlayController.hide();
|
||||||
|
this.invokerEl.click();
|
||||||
|
return this.calendarEl ? this.calendarEl.updateComplete : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectMonthDay(day) {
|
||||||
|
this.overlayController.show();
|
||||||
|
await this.calendarEl.updateComplete;
|
||||||
|
this.calendarObj.getDayEl(day).click();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node references
|
||||||
|
*/
|
||||||
|
|
||||||
|
get invokerEl() {
|
||||||
|
return this.el._invokerElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
get overlayEl() {
|
||||||
|
return this.el._overlayCtrl._container && this.el._overlayCtrl._container.firstElementChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
get overlayHeadingEl() {
|
||||||
|
return this.overlayEl && this.overlayEl.shadowRoot.querySelector('.calendar-overlay__heading');
|
||||||
|
}
|
||||||
|
|
||||||
|
get overlayCloseButtonEl() {
|
||||||
|
return this.calendarEl && this.overlayEl.shadowRoot.querySelector('#close-button');
|
||||||
|
}
|
||||||
|
|
||||||
|
get calendarEl() {
|
||||||
|
return this.overlayEl && this.overlayEl.querySelector('#calendar');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property {CalendarObject}
|
||||||
|
*/
|
||||||
|
get calendarObj() {
|
||||||
|
return this.calendarEl && new CalendarObject(this.calendarEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object references
|
||||||
|
*/
|
||||||
|
|
||||||
|
get overlayController() {
|
||||||
|
return this.el._overlayCtrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages/input-datepicker/translations/bg-BG.js
Normal file
5
packages/input-datepicker/translations/bg-BG.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import bg from './bg.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...bg,
|
||||||
|
};
|
||||||
3
packages/input-datepicker/translations/bg.js
Normal file
3
packages/input-datepicker/translations/bg.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
openDatepickerLabel: 'Избор на отворена дата',
|
||||||
|
};
|
||||||
5
packages/input-datepicker/translations/cs-CZ.js
Normal file
5
packages/input-datepicker/translations/cs-CZ.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import cs from './cs.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...cs,
|
||||||
|
};
|
||||||
3
packages/input-datepicker/translations/cs.js
Normal file
3
packages/input-datepicker/translations/cs.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
openDatepickerLabel: 'Otevřete pro výběr data',
|
||||||
|
};
|
||||||
5
packages/input-datepicker/translations/de-DE.js
Normal file
5
packages/input-datepicker/translations/de-DE.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import de from './de.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...de,
|
||||||
|
};
|
||||||
3
packages/input-datepicker/translations/de.js
Normal file
3
packages/input-datepicker/translations/de.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
openDatepickerLabel: 'Datumswähler öffnen',
|
||||||
|
};
|
||||||
5
packages/input-datepicker/translations/en-AU.js
Normal file
5
packages/input-datepicker/translations/en-AU.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import en from './en.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...en,
|
||||||
|
};
|
||||||
5
packages/input-datepicker/translations/en-GB.js
Normal file
5
packages/input-datepicker/translations/en-GB.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import en from './en.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...en,
|
||||||
|
};
|
||||||
5
packages/input-datepicker/translations/en-US.js
Normal file
5
packages/input-datepicker/translations/en-US.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import en from './en.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...en,
|
||||||
|
};
|
||||||
3
packages/input-datepicker/translations/en.js
Normal file
3
packages/input-datepicker/translations/en.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
openDatepickerLabel: 'Open date picker',
|
||||||
|
};
|
||||||
5
packages/input-datepicker/translations/es-ES.js
Normal file
5
packages/input-datepicker/translations/es-ES.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import es from './es.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...es,
|
||||||
|
};
|
||||||
3
packages/input-datepicker/translations/es.js
Normal file
3
packages/input-datepicker/translations/es.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
openDatepickerLabel: 'Selector de fecha abierta',
|
||||||
|
};
|
||||||
5
packages/input-datepicker/translations/fr-BE.js
Normal file
5
packages/input-datepicker/translations/fr-BE.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import fr from './fr.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...fr,
|
||||||
|
};
|
||||||
5
packages/input-datepicker/translations/fr-FR.js
Normal file
5
packages/input-datepicker/translations/fr-FR.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import fr from './fr.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...fr,
|
||||||
|
};
|
||||||
3
packages/input-datepicker/translations/fr.js
Normal file
3
packages/input-datepicker/translations/fr.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
openDatepickerLabel: 'Ouvrir le sélecteur de dates',
|
||||||
|
};
|
||||||
5
packages/input-datepicker/translations/hu-HU.js
Normal file
5
packages/input-datepicker/translations/hu-HU.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import hu from './hu.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...hu,
|
||||||
|
};
|
||||||
3
packages/input-datepicker/translations/hu.js
Normal file
3
packages/input-datepicker/translations/hu.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
openDatepickerLabel: 'Dátumválasztó megnyitása',
|
||||||
|
};
|
||||||
5
packages/input-datepicker/translations/it-IT.js
Normal file
5
packages/input-datepicker/translations/it-IT.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import it from './it.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...it,
|
||||||
|
};
|
||||||
3
packages/input-datepicker/translations/it.js
Normal file
3
packages/input-datepicker/translations/it.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
openDatepickerLabel: 'Raccoglitore di data aperta',
|
||||||
|
};
|
||||||
5
packages/input-datepicker/translations/nl-BE.js
Normal file
5
packages/input-datepicker/translations/nl-BE.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import nl from './nl.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...nl,
|
||||||
|
};
|
||||||
5
packages/input-datepicker/translations/nl-NL.js
Normal file
5
packages/input-datepicker/translations/nl-NL.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import nl from './nl.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...nl,
|
||||||
|
};
|
||||||
3
packages/input-datepicker/translations/nl.js
Normal file
3
packages/input-datepicker/translations/nl.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
openDatepickerLabel: 'Open kalender',
|
||||||
|
};
|
||||||
5
packages/input-datepicker/translations/pl-PL.js
Normal file
5
packages/input-datepicker/translations/pl-PL.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import pl from './pl.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...pl,
|
||||||
|
};
|
||||||
3
packages/input-datepicker/translations/pl.js
Normal file
3
packages/input-datepicker/translations/pl.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
openDatepickerLabel: 'Otwórz pole daty',
|
||||||
|
};
|
||||||
5
packages/input-datepicker/translations/ro-RO.js
Normal file
5
packages/input-datepicker/translations/ro-RO.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import ro from './ro.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...ro,
|
||||||
|
};
|
||||||
3
packages/input-datepicker/translations/ro.js
Normal file
3
packages/input-datepicker/translations/ro.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
openDatepickerLabel: 'Deschidere selector dată',
|
||||||
|
};
|
||||||
5
packages/input-datepicker/translations/ru-RU.js
Normal file
5
packages/input-datepicker/translations/ru-RU.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import ru from './ru.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...ru,
|
||||||
|
};
|
||||||
3
packages/input-datepicker/translations/ru.js
Normal file
3
packages/input-datepicker/translations/ru.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
openDatepickerLabel: 'Открыть модуль выбора даты',
|
||||||
|
};
|
||||||
5
packages/input-datepicker/translations/sk-SK.js
Normal file
5
packages/input-datepicker/translations/sk-SK.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import sk from './sk.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...sk,
|
||||||
|
};
|
||||||
3
packages/input-datepicker/translations/sk.js
Normal file
3
packages/input-datepicker/translations/sk.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
openDatepickerLabel: 'Otvoriť nástroj na výber dátumu',
|
||||||
|
};
|
||||||
5
packages/input-datepicker/translations/uk-UA.js
Normal file
5
packages/input-datepicker/translations/uk-UA.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import uk from './uk.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...uk,
|
||||||
|
};
|
||||||
3
packages/input-datepicker/translations/uk.js
Normal file
3
packages/input-datepicker/translations/uk.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
openDatepickerLabel: 'Відкрити модуль вибору дати',
|
||||||
|
};
|
||||||
|
|
@ -6,6 +6,7 @@ import '../packages/input/stories/localize.stories.js';
|
||||||
import '../packages/textarea/stories/index.stories.js';
|
import '../packages/textarea/stories/index.stories.js';
|
||||||
import '../packages/input-amount/stories/index.stories.js';
|
import '../packages/input-amount/stories/index.stories.js';
|
||||||
import '../packages/input-date/stories/index.stories.js';
|
import '../packages/input-date/stories/index.stories.js';
|
||||||
|
import '../packages/input-datepicker/stories/index.stories.js';
|
||||||
import '../packages/input-email/stories/index.stories.js';
|
import '../packages/input-email/stories/index.stories.js';
|
||||||
import '../packages/input-iban/stories/index.stories.js';
|
import '../packages/input-iban/stories/index.stories.js';
|
||||||
import '../packages/select/stories/index.stories.js';
|
import '../packages/select/stories/index.stories.js';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue