diff --git a/packages/input-datepicker/index.js b/packages/input-datepicker/index.js
new file mode 100644
index 000000000..5267e28da
--- /dev/null
+++ b/packages/input-datepicker/index.js
@@ -0,0 +1 @@
+export { LionInputDatepicker } from './src/LionInputDatepicker.js';
diff --git a/packages/input-datepicker/lion-input-datepicker.js b/packages/input-datepicker/lion-input-datepicker.js
new file mode 100644
index 000000000..619dbc90d
--- /dev/null
+++ b/packages/input-datepicker/lion-input-datepicker.js
@@ -0,0 +1,3 @@
+import { LionInputDatepicker } from './src/LionInputDatepicker.js';
+
+customElements.define('lion-input-datepicker', LionInputDatepicker);
diff --git a/packages/input-datepicker/package.json b/packages/input-datepicker/package.json
new file mode 100644
index 000000000..d066aeba4
--- /dev/null
+++ b/packages/input-datepicker/package.json
@@ -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"
+ }
+}
diff --git a/packages/input-datepicker/src/LionCalendarOverlayFrame.js b/packages/input-datepicker/src/LionCalendarOverlayFrame.js
new file mode 100644
index 000000000..fc2cfd3da
--- /dev/null
+++ b/packages/input-datepicker/src/LionCalendarOverlayFrame.js
@@ -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`
+
+
+
+
+ `;
+ }
+}
diff --git a/packages/input-datepicker/src/LionInputDatepicker.js b/packages/input-datepicker/src/LionInputDatepicker.js
new file mode 100644
index 000000000..e0083f188
--- /dev/null
+++ b/packages/input-datepicker/src/LionInputDatepicker.js
@@ -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`
+
+ ${this.calendarHeading}
+ ${this._calendarTemplate()}
+
+ `;
+ }
+
+ /**
+ * Subclassers can replace this with their custom extension of
+ * LionCalendar, like ``
+ */
+ // eslint-disable-next-line class-methods-use-this
+ _calendarTemplate() {
+ return html`
+
+ `;
+ }
+
+ /**
+ * Subclassers can replace this with their custom extension invoker,
+ * like ``
+ */
+ // 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`
+
+ `;
+ }
+
+ // 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;
+ }
+ });
+ }
+}
diff --git a/packages/input-datepicker/src/lion-calendar-overlay-frame.js b/packages/input-datepicker/src/lion-calendar-overlay-frame.js
new file mode 100644
index 000000000..ebe505e60
--- /dev/null
+++ b/packages/input-datepicker/src/lion-calendar-overlay-frame.js
@@ -0,0 +1,3 @@
+import { LionCalendarOverlayFrame } from './LionCalendarOverlayFrame.js';
+
+customElements.define('lion-calendar-overlay-frame', LionCalendarOverlayFrame);
diff --git a/packages/input-datepicker/stories/index.stories.js b/packages/input-datepicker/stories/index.stories.js
new file mode 100644
index 000000000..7ab913cdf
--- /dev/null
+++ b/packages/input-datepicker/stories/index.stories.js
@@ -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`
+
+
+ `,
+ )
+ .add(
+ 'minMaxDateValidator',
+ () => html`
+
+
+ `,
+ )
+ .add(
+ 'disabledDatesValidator',
+ () => html`
+ d.getDate() === 15)]}
+ >
+
+ `,
+ )
+ .add(
+ 'With calendar-heading',
+ () => html`
+
+
+ `,
+ )
+ .add(
+ 'Disabled',
+ () => html`
+
+ `,
+ );
diff --git a/packages/input-datepicker/test/lion-input-datepicker.test.js b/packages/input-datepicker/test/lion-input-datepicker.test.js
new file mode 100644
index 000000000..b912714c8
--- /dev/null
+++ b/packages/input-datepicker/test/lion-input-datepicker.test.js
@@ -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('', () => {
+ beforeEach(() => {
+ localizeTearDown();
+ });
+
+ describe('Calendar Overlay', () => {
+ it('implements calendar-overlay Style component', async () => {
+ const el = await fixture(html`
+
+ `);
+ 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`
+
+ `);
+ 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`
+
+ `);
+ 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`
+
+ `);
+ 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`
+
+ `);
+ 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`
+
+ `);
+ 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`
+
+ `);
+ 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`
+
+ `);
+ 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`
+
+ `);
+ 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`
+
+ `);
+ 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 :
+ * - 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`
+
+
+ `);
+ 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`
+
+ `);
+ 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`
+
+
+ `);
+ 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`
+
+
+ `);
+ 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`
+
+ `);
+ 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`
+
+ `);
+ 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`
+
+ `);
+ 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`
+ Pick my date
+ `;
+ }
+ },
+ );
+
+ 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`
+
+ `;
+ }
+ },
+ );
+
+ 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`
+
+
+
+
+ `;
+ }
+ },
+ );
+
+ let myOverlayOpenedCbHandled = false;
+ let myUserSelectedChangedCbHandled = false;
+
+ const myTag = defineCE(
+ class extends LionInputDatepicker {
+ /** @override */
+ _calendarOverlayTemplate() {
+ return html`
+
+ ${this.calendarHeading}
+ ${this._calendarTemplateConfig(this._calendarTemplate())}
+
+ `;
+ }
+
+ /** @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 () => {});
+ });
+ });
+});
diff --git a/packages/input-datepicker/test/test-utils.js b/packages/input-datepicker/test/test-utils.js
new file mode 100644
index 000000000..f098851ae
--- /dev/null
+++ b/packages/input-datepicker/test/test-utils.js
@@ -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;
+ }
+}
diff --git a/packages/input-datepicker/translations/bg-BG.js b/packages/input-datepicker/translations/bg-BG.js
new file mode 100644
index 000000000..00f5e5f61
--- /dev/null
+++ b/packages/input-datepicker/translations/bg-BG.js
@@ -0,0 +1,5 @@
+import bg from './bg.js';
+
+export default {
+ ...bg,
+};
diff --git a/packages/input-datepicker/translations/bg.js b/packages/input-datepicker/translations/bg.js
new file mode 100644
index 000000000..2e6f21338
--- /dev/null
+++ b/packages/input-datepicker/translations/bg.js
@@ -0,0 +1,3 @@
+export default {
+ openDatepickerLabel: 'Избор на отворена дата',
+};
diff --git a/packages/input-datepicker/translations/cs-CZ.js b/packages/input-datepicker/translations/cs-CZ.js
new file mode 100644
index 000000000..2cac51aa8
--- /dev/null
+++ b/packages/input-datepicker/translations/cs-CZ.js
@@ -0,0 +1,5 @@
+import cs from './cs.js';
+
+export default {
+ ...cs,
+};
diff --git a/packages/input-datepicker/translations/cs.js b/packages/input-datepicker/translations/cs.js
new file mode 100644
index 000000000..fbb1d48c6
--- /dev/null
+++ b/packages/input-datepicker/translations/cs.js
@@ -0,0 +1,3 @@
+export default {
+ openDatepickerLabel: 'Otevřete pro výběr data',
+};
diff --git a/packages/input-datepicker/translations/de-DE.js b/packages/input-datepicker/translations/de-DE.js
new file mode 100644
index 000000000..8e3fb7c86
--- /dev/null
+++ b/packages/input-datepicker/translations/de-DE.js
@@ -0,0 +1,5 @@
+import de from './de.js';
+
+export default {
+ ...de,
+};
diff --git a/packages/input-datepicker/translations/de.js b/packages/input-datepicker/translations/de.js
new file mode 100644
index 000000000..ed1a57cbe
--- /dev/null
+++ b/packages/input-datepicker/translations/de.js
@@ -0,0 +1,3 @@
+export default {
+ openDatepickerLabel: 'Datumswähler öffnen',
+};
diff --git a/packages/input-datepicker/translations/en-AU.js b/packages/input-datepicker/translations/en-AU.js
new file mode 100644
index 000000000..e164dab30
--- /dev/null
+++ b/packages/input-datepicker/translations/en-AU.js
@@ -0,0 +1,5 @@
+import en from './en.js';
+
+export default {
+ ...en,
+};
diff --git a/packages/input-datepicker/translations/en-GB.js b/packages/input-datepicker/translations/en-GB.js
new file mode 100644
index 000000000..e164dab30
--- /dev/null
+++ b/packages/input-datepicker/translations/en-GB.js
@@ -0,0 +1,5 @@
+import en from './en.js';
+
+export default {
+ ...en,
+};
diff --git a/packages/input-datepicker/translations/en-US.js b/packages/input-datepicker/translations/en-US.js
new file mode 100644
index 000000000..e164dab30
--- /dev/null
+++ b/packages/input-datepicker/translations/en-US.js
@@ -0,0 +1,5 @@
+import en from './en.js';
+
+export default {
+ ...en,
+};
diff --git a/packages/input-datepicker/translations/en.js b/packages/input-datepicker/translations/en.js
new file mode 100644
index 000000000..965ecaa11
--- /dev/null
+++ b/packages/input-datepicker/translations/en.js
@@ -0,0 +1,3 @@
+export default {
+ openDatepickerLabel: 'Open date picker',
+};
diff --git a/packages/input-datepicker/translations/es-ES.js b/packages/input-datepicker/translations/es-ES.js
new file mode 100644
index 000000000..94a009944
--- /dev/null
+++ b/packages/input-datepicker/translations/es-ES.js
@@ -0,0 +1,5 @@
+import es from './es.js';
+
+export default {
+ ...es,
+};
diff --git a/packages/input-datepicker/translations/es.js b/packages/input-datepicker/translations/es.js
new file mode 100644
index 000000000..2a2c70d1a
--- /dev/null
+++ b/packages/input-datepicker/translations/es.js
@@ -0,0 +1,3 @@
+export default {
+ openDatepickerLabel: 'Selector de fecha abierta',
+};
diff --git a/packages/input-datepicker/translations/fr-BE.js b/packages/input-datepicker/translations/fr-BE.js
new file mode 100644
index 000000000..da02615de
--- /dev/null
+++ b/packages/input-datepicker/translations/fr-BE.js
@@ -0,0 +1,5 @@
+import fr from './fr.js';
+
+export default {
+ ...fr,
+};
diff --git a/packages/input-datepicker/translations/fr-FR.js b/packages/input-datepicker/translations/fr-FR.js
new file mode 100644
index 000000000..da02615de
--- /dev/null
+++ b/packages/input-datepicker/translations/fr-FR.js
@@ -0,0 +1,5 @@
+import fr from './fr.js';
+
+export default {
+ ...fr,
+};
diff --git a/packages/input-datepicker/translations/fr.js b/packages/input-datepicker/translations/fr.js
new file mode 100644
index 000000000..b14c19357
--- /dev/null
+++ b/packages/input-datepicker/translations/fr.js
@@ -0,0 +1,3 @@
+export default {
+ openDatepickerLabel: 'Ouvrir le sélecteur de dates',
+};
diff --git a/packages/input-datepicker/translations/hu-HU.js b/packages/input-datepicker/translations/hu-HU.js
new file mode 100644
index 000000000..130ba8f66
--- /dev/null
+++ b/packages/input-datepicker/translations/hu-HU.js
@@ -0,0 +1,5 @@
+import hu from './hu.js';
+
+export default {
+ ...hu,
+};
diff --git a/packages/input-datepicker/translations/hu.js b/packages/input-datepicker/translations/hu.js
new file mode 100644
index 000000000..2829fe4ff
--- /dev/null
+++ b/packages/input-datepicker/translations/hu.js
@@ -0,0 +1,3 @@
+export default {
+ openDatepickerLabel: 'Dátumválasztó megnyitása',
+};
diff --git a/packages/input-datepicker/translations/it-IT.js b/packages/input-datepicker/translations/it-IT.js
new file mode 100644
index 000000000..397b5a03b
--- /dev/null
+++ b/packages/input-datepicker/translations/it-IT.js
@@ -0,0 +1,5 @@
+import it from './it.js';
+
+export default {
+ ...it,
+};
diff --git a/packages/input-datepicker/translations/it.js b/packages/input-datepicker/translations/it.js
new file mode 100644
index 000000000..9f514be03
--- /dev/null
+++ b/packages/input-datepicker/translations/it.js
@@ -0,0 +1,3 @@
+export default {
+ openDatepickerLabel: 'Raccoglitore di data aperta',
+};
diff --git a/packages/input-datepicker/translations/nl-BE.js b/packages/input-datepicker/translations/nl-BE.js
new file mode 100644
index 000000000..93467cea6
--- /dev/null
+++ b/packages/input-datepicker/translations/nl-BE.js
@@ -0,0 +1,5 @@
+import nl from './nl.js';
+
+export default {
+ ...nl,
+};
diff --git a/packages/input-datepicker/translations/nl-NL.js b/packages/input-datepicker/translations/nl-NL.js
new file mode 100644
index 000000000..93467cea6
--- /dev/null
+++ b/packages/input-datepicker/translations/nl-NL.js
@@ -0,0 +1,5 @@
+import nl from './nl.js';
+
+export default {
+ ...nl,
+};
diff --git a/packages/input-datepicker/translations/nl.js b/packages/input-datepicker/translations/nl.js
new file mode 100644
index 000000000..2b12f81ff
--- /dev/null
+++ b/packages/input-datepicker/translations/nl.js
@@ -0,0 +1,3 @@
+export default {
+ openDatepickerLabel: 'Open kalender',
+};
diff --git a/packages/input-datepicker/translations/pl-PL.js b/packages/input-datepicker/translations/pl-PL.js
new file mode 100644
index 000000000..cb0d0b8b6
--- /dev/null
+++ b/packages/input-datepicker/translations/pl-PL.js
@@ -0,0 +1,5 @@
+import pl from './pl.js';
+
+export default {
+ ...pl,
+};
diff --git a/packages/input-datepicker/translations/pl.js b/packages/input-datepicker/translations/pl.js
new file mode 100644
index 000000000..ef75a7756
--- /dev/null
+++ b/packages/input-datepicker/translations/pl.js
@@ -0,0 +1,3 @@
+export default {
+ openDatepickerLabel: 'Otwórz pole daty',
+};
diff --git a/packages/input-datepicker/translations/ro-RO.js b/packages/input-datepicker/translations/ro-RO.js
new file mode 100644
index 000000000..8acc92b29
--- /dev/null
+++ b/packages/input-datepicker/translations/ro-RO.js
@@ -0,0 +1,5 @@
+import ro from './ro.js';
+
+export default {
+ ...ro,
+};
diff --git a/packages/input-datepicker/translations/ro.js b/packages/input-datepicker/translations/ro.js
new file mode 100644
index 000000000..9cdf530e7
--- /dev/null
+++ b/packages/input-datepicker/translations/ro.js
@@ -0,0 +1,3 @@
+export default {
+ openDatepickerLabel: 'Deschidere selector dată',
+};
diff --git a/packages/input-datepicker/translations/ru-RU.js b/packages/input-datepicker/translations/ru-RU.js
new file mode 100644
index 000000000..e5f8f2aa1
--- /dev/null
+++ b/packages/input-datepicker/translations/ru-RU.js
@@ -0,0 +1,5 @@
+import ru from './ru.js';
+
+export default {
+ ...ru,
+};
diff --git a/packages/input-datepicker/translations/ru.js b/packages/input-datepicker/translations/ru.js
new file mode 100644
index 000000000..39beda04c
--- /dev/null
+++ b/packages/input-datepicker/translations/ru.js
@@ -0,0 +1,3 @@
+export default {
+ openDatepickerLabel: 'Открыть модуль выбора даты',
+};
diff --git a/packages/input-datepicker/translations/sk-SK.js b/packages/input-datepicker/translations/sk-SK.js
new file mode 100644
index 000000000..3000b323f
--- /dev/null
+++ b/packages/input-datepicker/translations/sk-SK.js
@@ -0,0 +1,5 @@
+import sk from './sk.js';
+
+export default {
+ ...sk,
+};
diff --git a/packages/input-datepicker/translations/sk.js b/packages/input-datepicker/translations/sk.js
new file mode 100644
index 000000000..761f6dde0
--- /dev/null
+++ b/packages/input-datepicker/translations/sk.js
@@ -0,0 +1,3 @@
+export default {
+ openDatepickerLabel: 'Otvoriť nástroj na výber dátumu',
+};
diff --git a/packages/input-datepicker/translations/uk-UA.js b/packages/input-datepicker/translations/uk-UA.js
new file mode 100644
index 000000000..e255cc021
--- /dev/null
+++ b/packages/input-datepicker/translations/uk-UA.js
@@ -0,0 +1,5 @@
+import uk from './uk.js';
+
+export default {
+ ...uk,
+};
diff --git a/packages/input-datepicker/translations/uk.js b/packages/input-datepicker/translations/uk.js
new file mode 100644
index 000000000..dc255e057
--- /dev/null
+++ b/packages/input-datepicker/translations/uk.js
@@ -0,0 +1,3 @@
+export default {
+ openDatepickerLabel: 'Відкрити модуль вибору дати',
+};
diff --git a/stories/index.stories.js b/stories/index.stories.js
index 2931f50e3..00329be36 100755
--- a/stories/index.stories.js
+++ b/stories/index.stories.js
@@ -6,6 +6,7 @@ import '../packages/input/stories/localize.stories.js';
import '../packages/textarea/stories/index.stories.js';
import '../packages/input-amount/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-iban/stories/index.stories.js';
import '../packages/select/stories/index.stories.js';