feat(calendar): add reusable calendar

Co-authored-by: Erik Kroes <erik.kroes@ing.com>
Co-authored-by: Gerjan van Geest <gerjan.van.geest@ing.com>
Co-authored-by: Thijs Louisse <thijs.louisse@ing.com>
Co-authored-by: Thomas Allmer <thomas.allmer@ing.com>
This commit is contained in:
Mikhail Bashkirov 2019-05-13 10:37:50 +02:00
parent 043106c1cf
commit 9fc5488175
44 changed files with 3275 additions and 0 deletions

View file

@ -0,0 +1,44 @@
# Calendar
[//]: # (AUTO INSERT HEADER PREPUBLISH)
`lion-calendar` is a reusable and accessible calendar view.
## Features
- fully accessible keyboard navigation (Arrow Keys, PgUp, PgDn, ALT+PgUp, ALT+PgDn)
- **minDate**: disables all dates before a given date
- **maxDate**: disables all dates after a given date
- **disableDates**: disables some dates within an available range
- **selectedDate**: currently selected date
- **centralDate**: date that determines the currently visible month and that will be focused when keyboard moves the focus to the month grid
- **focusedDate**: (getter only) currently focused date (if there is any with real focus)
- **focusDate(date)**: focus on a certain date
- **focusSelectedDate()**: focus on the current selected date
- **focusCentralDate()**: focus on the current central date
- **firstDayOfWeek**: typically Sunday (default) or Monday
- **weekdayHeaderNotation**: long/short/narrow for the current locale (e.g. Thursday/Thu/T)
- **locale**: different locale for the current component only
## How to use
### Installation
```sh
npm i --save @lion/calendar
```
```js
import '@lion/calendar/lion-calendar.js';
```
### Example
```html
<lion-calendar
.minDate="${new Date()}"
.maxDate="${new Date('2019/12/09')}"
.disableDates=${day => day.getDay() === 6 || day.getDay() === 0}
>
</lion-calendar>
```

View file

@ -0,0 +1 @@
export { LionCalendar } from './src/LionCalendar.js';

View file

@ -0,0 +1,3 @@
import { LionCalendar } from './src/LionCalendar.js';
customElements.define('lion-calendar', LionCalendar);

View file

@ -0,0 +1,43 @@
{
"name": "@lion/calendar",
"version": "0.0.0",
"description": "Reusable calendar component",
"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/calendar"
},
"scripts": {
"prepublishOnly": "../../scripts/npm-prepublish.js"
},
"keywords": [
"lion",
"web-components",
"calendar"
],
"main": "index.js",
"module": "index.js",
"files": [
"src",
"stories",
"test",
"translations",
"*.js"
],
"dependencies": {
"@lion/core": "^0.1.4",
"@lion/localize": "^0.1.7"
},
"devDependencies": {
"@lion/button": "^0.1.7",
"@open-wc/demoing-storybook": "^0.2.0",
"@open-wc/testing": "^0.11.1",
"sinon": "^7.2.2"
}
}

View file

@ -0,0 +1,547 @@
import { html, LitElement } from '@lion/core';
import { localize, getWeekdayNames, getMonthNames, LocalizeMixin } from '@lion/localize';
import { createMultipleMonth } from './utils/createMultipleMonth.js';
import { dayTemplate } from './utils/dayTemplate.js';
import { dataTemplate } from './utils/dataTemplate.js';
import { getFirstDayNextMonth } from './utils/getFirstDayNextMonth.js';
import { getLastDayPreviousMonth } from './utils/getLastDayPreviousMonth.js';
import { isSameDate } from './utils/isSameDate.js';
import { calendarStyle } from './calendarStyle.js';
import './utils/differentKeyNamesShimIE.js';
import { createDay } from './utils/createDay.js';
/**
* @customElement
*/
export class LionCalendar extends LocalizeMixin(LitElement) {
static get localizeNamespaces() {
return [
{
'lion-calendar': locale => {
switch (locale) {
case 'bg-BG':
case 'bg':
return import('../translations/bg.js');
case 'cs-CZ':
case 'cs':
return import('../translations/cs.js');
case 'de-AT':
case 'de-DE':
case 'de':
return import('../translations/de.js');
case 'en-AU':
case 'en-GB':
case 'en-US':
case 'en':
return import('../translations/en.js');
case 'es-ES':
case 'es':
return import('../translations/es.js');
case 'fr-FR':
case 'fr-BE':
case 'fr':
return import('../translations/fr.js');
case 'hu-HU':
case 'hu':
return import('../translations/hu.js');
case 'it-IT':
case 'it':
return import('../translations/it.js');
case 'nl-BE':
case 'nl-NL':
case 'nl':
return import('../translations/nl.js');
case 'pl-PL':
case 'pl':
return import('../translations/pl.js');
case 'ro-RO':
case 'ro':
return import('../translations/ro.js');
case 'ru-RU':
case 'ru':
return import('../translations/ru.js');
case 'sk-SK':
case 'sk':
return import('../translations/sk.js');
case 'uk-UA':
case 'uk':
return import('../translations/uk.js');
default:
throw new Error(`Unknown locale: ${locale}`);
}
},
},
...super.localizeNamespaces,
];
}
static get properties() {
return {
/**
* Minimum date. All dates before will be disabled
*/
minDate: { type: Date },
/**
* Maximum date. All dates after will be disabled
*/
maxDate: { type: Date },
/**
* Disable certain dates
*/
disableDates: { type: Function },
/**
* The selected date, usually synchronized with datepicker-input
* Not to be confused with the focused date (therefore not necessarily in active month view)
*/
selectedDate: { type: Date },
/**
* The date that
* 1. determines the currently visible month
* 2. will be focused when the month grid gets focused by the keyboard
*/
centralDate: { type: Date },
/**
* Weekday that will be displayed in first column of month grid.
* 0: sunday, 1: monday, 2: tuesday, 3: wednesday , 4: thursday, 5: friday, 6: saturday
* Default is 0
*/
firstDayOfWeek: { type: Number },
/**
* Weekday header notation, based on Intl DatetimeFormat:
* - 'long' (e.g., Thursday)
* - 'short' (e.g., Thu)
* - 'narrow' (e.g., T).
* Default is 'short'
*/
weekdayHeaderNotation: { type: String },
/**
* Different locale for this component scope
*/
locale: { type: String },
/**
* The currently focused date (if any)
*/
__focusedDate: { type: Date },
/**
* Data to render current month grid
*/
__data: { type: Object },
};
}
constructor() {
super();
// Defaults
this.__data = {};
this.minDate = null;
this.maxDate = null;
this.dayPreprocessor = day => day;
this.disableDates = () => false;
this.firstDayOfWeek = 0;
this.weekdayHeaderNotation = 'short';
this.__today = new Date();
this.centralDate = this.__today;
this.__focusedDate = null;
this.__connectedCallbackDone = false;
}
static get styles() {
return [calendarStyle];
}
render() {
return html`
<div class="calendar" role="application">
${this.__renderHeader()} ${this.__renderData()}
</div>
`;
}
get focusedDate() {
return this.__focusedDate;
}
goToNextMonth() {
this.__modifyDate(1, { dateType: 'centralDate', type: 'Month', mode: 'both' });
}
goToPreviousMonth() {
this.__modifyDate(-1, { dateType: 'centralDate', type: 'Month', mode: 'both' });
}
async focusDate(date) {
this.centralDate = date;
await this.updateComplete;
this.focusCentralDate();
}
focusCentralDate() {
const button = this.shadowRoot.querySelector('button[tabindex="0"]');
button.focus();
this.__focusedDate = this.centralDate;
}
async focusSelectedDate() {
await this.focusDate(this.selectedDate);
}
connectedCallback() {
// eslint-disable-next-line wc/guard-super-call
super.connectedCallback();
this.__connectedCallbackDone = true;
this.__calculateInitialCentralDate();
// setup data for initial render
this.__data = this.__createData();
}
disconnectedCallback() {
if (super.disconnectedCallback) {
super.disconnectedCallback();
}
this.__removeEventDelegations();
}
firstUpdated() {
super.firstUpdated();
this.__contentWrapperElement = this.shadowRoot.getElementById('js-content-wrapper');
this.__addEventDelegationForClickDate();
this.__addEventDelegationForFocusDate();
this.__addEventDelegationForBlurDate();
this.__addEventForKeyboardNavigation();
}
updated(changed) {
if (changed.has('__focusedDate') && this.__focusedDate) {
this.focusCentralDate();
}
}
/**
* @override
*/
_requestUpdate(name, oldValue) {
super._requestUpdate(name, oldValue);
const map = {
disableDates: () => this.__disableDatesChanged(),
centralDate: () => this.__centralDateChanged(),
__focusedDate: () => this.__focusedDateChanged(),
};
if (map[name]) {
map[name]();
}
const updateDataOn = ['centralDate', 'minDate', 'maxDate', 'selectedDate', 'disableDates'];
if (updateDataOn.includes(name) && this.__connectedCallbackDone) {
this.__data = this.__createData();
}
}
__calculateInitialCentralDate() {
if (this.centralDate === this.__today && this.selectedDate) {
// initialised with selectedDate only if user didn't provide another one
this.centralDate = this.selectedDate;
} else {
this.__ensureValidCentralDate();
}
}
__renderHeader() {
const month = getMonthNames({ locale: this.__getLocale() })[this.centralDate.getMonth()];
const year = this.centralDate.getFullYear();
return html`
<div class="calendar__header">
${this.__renderPreviousButton()}
<h2
class="calendar__month-heading"
id="month_and_year"
aria-live="polite"
aria-atomic="true"
>
${month} ${year}
</h2>
${this.__renderNextButton()}
</div>
`;
}
__renderData() {
return dataTemplate(this.__data, {
monthsLabels: getMonthNames({ locale: this.__getLocale() }),
weekdaysShort: getWeekdayNames({
locale: this.__getLocale(),
style: this.weekdayHeaderNotation,
firstDayOfWeek: this.firstDayOfWeek,
}),
weekdays: getWeekdayNames({
locale: this.__getLocale(),
style: 'long',
firstDayOfWeek: this.firstDayOfWeek,
}),
dayTemplate,
});
}
__renderPreviousButton() {
return html`
<button
class="calendar__previous-month-button"
aria-label=${this.msgLit('lion-calendar:previousMonth')}
title=${this.msgLit('lion-calendar:previousMonth')}
@click=${this.goToPreviousMonth}
?disabled=${this.isPreviousMonthDisabled}
>
&lt;
</button>
`;
}
__renderNextButton() {
return html`
<button
class="calendar__next-month-button"
aria-label=${this.msgLit('lion-calendar:nextMonth')}
title=${this.msgLit('lion-calendar:nextMonth')}
@click=${this.goToNextMonth}
?disabled=${this.isNextMonthDisabled}
>
&gt;
</button>
`;
}
__coreDayPreprocessor(_day, { currentMonth = false } = {}) {
const day = createDay(new Date(_day.date), _day);
const today = new Date();
day.central = isSameDate(day.date, this.centralDate);
day.previousMonth = currentMonth && day.date.getMonth() < currentMonth.getMonth();
day.currentMonth = currentMonth && day.date.getMonth() === currentMonth.getMonth();
day.nextMonth = currentMonth && day.date.getMonth() > currentMonth.getMonth();
day.selected = this.selectedDate ? isSameDate(day.date, this.selectedDate) : false;
day.past = day.date < today;
day.today = isSameDate(day.date, today);
day.future = day.date > today;
day.disabled = this.disableDates(day.date);
if (this.minDate && day.date < this.minDate) {
day.disabled = true;
}
if (this.maxDate && day.date > this.maxDate) {
day.disabled = true;
}
return this.dayPreprocessor(day);
}
__createData(options) {
const data = createMultipleMonth(this.centralDate, {
firstDayOfWeek: this.firstDayOfWeek,
...options,
});
data.months.forEach((month, monthi) => {
month.weeks.forEach((week, weeki) => {
week.days.forEach((day, dayi) => {
// eslint-disable-next-line no-unused-vars
const currentDay = data.months[monthi].weeks[weeki].days[dayi];
const currentMonth = data.months[monthi].weeks[0].days[6].date;
data.months[monthi].weeks[weeki].days[dayi] = this.__coreDayPreprocessor(currentDay, {
currentMonth,
});
});
});
});
this.isNextMonthDisabled =
this.maxDate && getFirstDayNextMonth(this.centralDate) > this.maxDate;
this.isPreviousMonthDisabled =
this.minDate && getLastDayPreviousMonth(this.centralDate) < this.minDate;
return data;
}
__disableDatesChanged() {
this.__ensureValidCentralDate();
}
__dateSelectedByUser(selectedDate) {
this.selectedDate = selectedDate;
this.__focusedDate = selectedDate;
this.dispatchEvent(
new CustomEvent('user-selected-date-changed', {
detail: {
selectedDate,
},
}),
);
}
__centralDateChanged() {
if (this.__connectedCallbackDone) {
this.__ensureValidCentralDate();
}
}
__focusedDateChanged() {
if (this.__focusedDate) {
this.centralDate = this.__focusedDate;
}
}
__ensureValidCentralDate() {
if (!this.__isEnabledDate(this.centralDate)) {
this.centralDate = this.__findBestEnabledDateFor(this.centralDate);
}
}
__isEnabledDate(date) {
const processedDay = this.__coreDayPreprocessor({ date });
return !processedDay.disabled;
}
/**
* @param {Date} date
* @param {Object} opts
* @param {String} [opts.mode] Find best date in `future/past/both`
*/
__findBestEnabledDateFor(date, { mode = 'both' } = {}) {
const futureDate =
this.minDate && this.minDate > date ? new Date(this.minDate) : new Date(date);
const pastDate = this.maxDate && this.maxDate < date ? new Date(this.maxDate) : new Date(date);
let i = 0;
do {
i += 1;
if (mode === 'both' || mode === 'future') {
futureDate.setDate(futureDate.getDate() + 1);
if (this.__isEnabledDate(futureDate)) {
return futureDate;
}
}
if (mode === 'both' || mode === 'past') {
pastDate.setDate(pastDate.getDate() - 1);
if (this.__isEnabledDate(pastDate)) {
return pastDate;
}
}
} while (i < 750); // 2 years+
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
throw new Error(
`Could not find a selectable date within +/- 750 day for ${year}/${month}/${day}`,
);
}
__addEventDelegationForClickDate() {
const isDayCellOrButton = el =>
el.classList.contains('calendar__day-cell') || el.classList.contains('calendar__day-button');
this.__clickDateDelegation = this.__contentWrapperElement.addEventListener('click', ev => {
const el = ev.composedPath()[0];
if (isDayCellOrButton(el)) {
this.__dateSelectedByUser(el.date);
}
});
}
__addEventDelegationForFocusDate() {
const isDayButton = el => el.classList.contains('calendar__day-button');
this.__focusDateDelegation = this.__contentWrapperElement.addEventListener(
'focus',
() => {
if (!this.__focusedDate && isDayButton(this.shadowRoot.activeElement)) {
this.__focusedDate = this.shadowRoot.activeElement.date;
}
},
true,
);
}
__addEventDelegationForBlurDate() {
const isDayButton = el => el.classList.contains('calendar__day-button');
this.__blurDateDelegation = this.__contentWrapperElement.addEventListener(
'blur',
() => {
setTimeout(() => {
if (this.shadowRoot.activeElement && !isDayButton(this.shadowRoot.activeElement)) {
this.__focusedDate = null;
}
}, 1);
},
true,
);
}
__removeEventDelegations() {
this.__contentWrapperElement.removeEventListener('click', this.__clickDateDelegation);
this.__contentWrapperElement.removeEventListener('focus', this.__focusDateDelegation);
this.__contentWrapperElement.removeEventListener('blur', this.__blurDateDelegation);
this.__contentWrapperElement.removeEventListener('keydown', this.__keyNavigationEvent);
}
__addEventForKeyboardNavigation() {
this.__keyNavigationEvent = this.__contentWrapperElement.addEventListener('keydown', ev => {
switch (ev.key) {
case 'ArrowUp':
this.__modifyDate(-7, { dateType: '__focusedDate', type: 'Date', mode: 'past' });
break;
case 'ArrowDown':
this.__modifyDate(7, { dateType: '__focusedDate', type: 'Date', mode: 'future' });
break;
case 'ArrowLeft':
this.__modifyDate(-1, { dateType: '__focusedDate', type: 'Date', mode: 'past' });
break;
case 'ArrowRight':
this.__modifyDate(1, { dateType: '__focusedDate', type: 'Date', mode: 'future' });
break;
case 'PageDown':
if (ev.altKey === true) {
this.__modifyDate(1, { dateType: '__focusedDate', type: 'FullYear', mode: 'future' });
} else {
this.__modifyDate(1, { dateType: '__focusedDate', type: 'Month', mode: 'future' });
}
break;
case 'PageUp':
if (ev.altKey === true) {
this.__modifyDate(-1, { dateType: '__focusedDate', type: 'FullYear', mode: 'past' });
} else {
this.__modifyDate(-1, { dateType: '__focusedDate', type: 'Month', mode: 'past' });
}
break;
case 'Tab':
this.__focusedDate = null;
break;
// no default
}
});
}
__modifyDate(modify, { dateType, type, mode } = {}) {
let tmpDate = new Date(this.centralDate);
tmpDate[`set${type}`](tmpDate[`get${type}`]() + modify);
if (!this.__isEnabledDate(tmpDate)) {
tmpDate = this.__findBestEnabledDateFor(tmpDate, { mode });
}
this[dateType] = tmpDate;
}
__getLocale() {
return this.locale || localize.locale;
}
}

View file

@ -0,0 +1,74 @@
import { css } from '@lion/core';
export const calendarStyle = css`
:host {
display: block;
}
.calendar {
display: block;
}
.calendar__header {
display: flex;
justify-content: space-between;
border-bottom: 1px solid #adadad;
padding: 0 8px;
}
.calendar__month-heading {
margin: 0.5em 0;
}
.calendar__previous-month-button,
.calendar__next-month-button {
background-color: #fff;
border: 0;
padding: 0;
min-width: 40px;
min-height: 40px;
}
.calendar__grid {
width: 100%;
padding: 8px 8px;
}
.calendar__weekday-header {
}
.calendar__day-cell {
text-align: center;
}
.calendar__day-button {
background-color: #fff;
border: 0;
padding: 0;
min-width: 40px;
min-height: 40px;
}
.calendar__day-button[today] {
text-decoration: underline;
}
.calendar__day-button[selected] {
background: #ccc;
}
.calendar__day-button[previous-month],
.calendar__day-button[next-month] {
color: #ddd;
}
.calendar__day-button:hover {
border: 1px solid green;
}
.calendar__day-button[disabled] {
background-color: #fff;
color: #eee;
outline: none;
}
`;

View file

@ -0,0 +1,29 @@
export function createDay(
date = new Date(),
{
weekOrder,
central = false,
startOfWeek = false,
selected = false,
previousMonth = false,
currentMonth = false,
nextMonth = false,
past = false,
today = false,
future = false,
} = {},
) {
return {
weekOrder,
central,
date,
startOfWeek,
selected,
previousMonth,
currentMonth,
nextMonth,
past,
today,
future,
};
}

View file

@ -0,0 +1,25 @@
import { createWeek } from './createWeek.js';
export function createMonth(date, { firstDayOfWeek = 0 } = {}) {
if (Object.prototype.toString.call(date) !== '[object Date]') {
throw new Error('invalid date provided');
}
const firstDayOfMonth = new Date(date);
firstDayOfMonth.setDate(1);
const monthNumber = firstDayOfMonth.getMonth();
const weekOptions = { firstDayOfWeek };
const month = {
weeks: [],
};
let nextWeek = createWeek(firstDayOfMonth, weekOptions);
do {
month.weeks.push(nextWeek);
const firstDayOfNextWeek = new Date(nextWeek.days[6].date); // last day of current week
firstDayOfNextWeek.setDate(firstDayOfNextWeek.getDate() + 1); // make it first day of next week
nextWeek = createWeek(firstDayOfNextWeek, weekOptions);
} while (nextWeek.days[0].date.getMonth() === monthNumber);
return month;
}

View file

@ -0,0 +1,26 @@
import { createMonth } from './createMonth.js';
export function createMultipleMonth(
date,
{ firstDayOfWeek = 0, pastMonths = 0, futureMonths = 0 } = {},
) {
const multipleMonths = {
months: [],
};
for (let i = pastMonths; i > 0; i -= 1) {
const pastDate = new Date(date);
pastDate.setMonth(pastDate.getMonth() - i);
multipleMonths.months.push(createMonth(pastDate, { firstDayOfWeek }));
}
multipleMonths.months.push(createMonth(date, { firstDayOfWeek }));
for (let i = 0; i < futureMonths; i += 1) {
const futureDate = new Date(date);
futureDate.setMonth(futureDate.getMonth() + (i + 1));
multipleMonths.months.push(createMonth(futureDate, { firstDayOfWeek }));
}
return multipleMonths;
}

View file

@ -0,0 +1,30 @@
import { createDay } from './createDay.js';
export function createWeek(date, { firstDayOfWeek = 0 } = {}) {
if (Object.prototype.toString.call(date) !== '[object Date]') {
throw new Error('invalid date provided');
}
let weekStartDate = new Date(date);
const tmpDate = new Date(date);
while (tmpDate.getDay() !== firstDayOfWeek) {
tmpDate.setDate(tmpDate.getDate() - 1);
weekStartDate = new Date(tmpDate);
}
const week = {
days: [],
};
for (let i = 0; i < 7; i += 1) {
if (i !== 0) {
weekStartDate.setDate(weekStartDate.getDate() + 1);
}
week.days.push(
createDay(new Date(weekStartDate), {
weekOrder: i,
startOfWeek: i === 0,
}),
);
}
return week;
}

View file

@ -0,0 +1,51 @@
import { html } from '@lion/core';
import { dayTemplate as defaultDayTemplate } from './dayTemplate.js';
export function dataTemplate(
data,
{ weekdaysShort, weekdays, monthsLabels, dayTemplate = defaultDayTemplate } = {},
) {
return html`
<div id="js-content-wrapper">
${data.months.map(
month => html`
<table
role="grid"
data-wrap-cols
aria-readonly="true"
class="calendar__grid"
aria-labelledby="month_and_year"
>
<thead>
<tr role="row">
${weekdaysShort.map(
(header, i) => html`
<th
role="columnheader"
class="calendar__weekday-header"
scope="col"
aria-label="${weekdays[i]}"
>
${header}
</th>
`,
)}
</tr>
</thead>
<tbody>
${month.weeks.map(
week => html`
<tr role="row">
${week.days.map(day =>
dayTemplate(day, { weekdaysShort, weekdays, monthsLabels }),
)}
</tr>
`,
)}
</tbody>
</table>
`,
)}
</div>
`;
}

View file

@ -0,0 +1,46 @@
import { html, ifDefined } from '@lion/core';
const defaultMonthLabels = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
// TODO: remove as much logic as possible from this template and move to processor
export function dayTemplate(day, { weekdays, monthsLabels = defaultMonthLabels } = {}) {
const dayNumber = day.date.getDate();
const monthName = monthsLabels[day.date.getMonth()];
const year = day.date.getFullYear();
const weekdayName = weekdays[day.weekOrder];
return html`
<td role="gridcell" class="calendar__day-cell">
<button
.date=${day.date}
class="calendar__day-button"
tabindex=${day.central ? '0' : '-1'}
aria-label=${`${dayNumber} ${monthName} ${year} ${weekdayName}`}
aria-selected=${day.selected ? 'true' : 'false'}
aria-current=${ifDefined(day.today ? 'date' : undefined)}
?disabled=${day.disabled}
?selected=${day.selected}
?past=${day.past}
?today=${day.today}
?future=${day.future}
?previous-month=${day.previousMonth}
?current-month=${day.currentMonth}
?next-month=${day.nextMonth}
>
${day.date.getDate()}
</button>
</td>
`;
}

View file

@ -0,0 +1,33 @@
const event = KeyboardEvent.prototype;
const descriptor = Object.getOwnPropertyDescriptor(event, 'key');
if (descriptor) {
const keys = {
Win: 'Meta',
Scroll: 'ScrollLock',
Spacebar: ' ',
Down: 'ArrowDown',
Left: 'ArrowLeft',
Right: 'ArrowRight',
Up: 'ArrowUp',
Del: 'Delete',
Apps: 'ContextMenu',
Esc: 'Escape',
Multiply: '*',
Add: '+',
Subtract: '-',
Decimal: '.',
Divide: '/',
};
Object.defineProperty(event, 'key', {
// eslint-disable-next-line object-shorthand, func-names
get: function() {
const key = descriptor.get.call(this);
// eslint-disable-next-line no-prototype-builtins
return keys.hasOwnProperty(key) ? keys[key] : key;
},
});
}

View file

@ -0,0 +1,13 @@
/**
* Gives the first day of the next month
*
* @param {Date} date
*
* returns {Date}
*/
export function getFirstDayNextMonth(date) {
const result = new Date(date);
result.setDate(1);
result.setMonth(date.getMonth() + 1);
return result;
}

View file

@ -0,0 +1,12 @@
/**
* Gives the last day of the previous month
*
* @param {Date} date
*
* returns {Date}
*/
export function getLastDayPreviousMonth(date) {
const previous = new Date(date);
previous.setDate(0);
return new Date(previous);
}

View file

@ -0,0 +1,17 @@
/**
* Compares if two days are the same
*
* @param {Date} day1
* @param {Date} day2
*
* returns {boolean}
*/
export function isSameDate(day1, day2) {
return (
day1 instanceof Date &&
day2 instanceof Date &&
day1.getDate() === day2.getDate() &&
day1.getMonth() === day2.getMonth() &&
day1.getFullYear() === day2.getFullYear()
);
}

View file

@ -0,0 +1,136 @@
import { storiesOf, html } from '@open-wc/demoing-storybook';
import { css } from '@lion/core';
import '@lion/button/lion-button.js';
import '../lion-calendar.js';
const calendarDemoStyle = css`
.demo-calendar {
border: 1px solid #adadad;
box-shadow: 0 0 16px #ccc;
max-width: 500px;
}
`;
storiesOf('Calendar|Standalone', module)
.add(
'default',
() => html`
<style>
${calendarDemoStyle}
</style>
<lion-calendar class="demo-calendar"></lion-calendar>
`,
)
.add('selectedDate', () => {
const today = new Date();
const selectedDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
return html`
<style>
${calendarDemoStyle}
</style>
<lion-calendar class="demo-calendar" .selectedDate="${selectedDate}"></lion-calendar>
`;
})
.add('centralDate', () => {
const today = new Date();
const centralDate = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
return html`
<style>
${calendarDemoStyle}
</style>
<lion-calendar class="demo-calendar" .centralDate="${centralDate}"></lion-calendar>
<p>Use TAB to see which date will be focused first.</p>
`;
})
.add('control focus', () => {
const today = new Date();
const selectedDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
const centralDate = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
return html`
<style>
${calendarDemoStyle}
</style>
<lion-calendar
id="js-demo-calendar"
class="demo-calendar"
.selectedDate="${selectedDate}"
.centralDate="${centralDate}"
></lion-calendar>
<p>
Focus:
<lion-button
@click="${() => document.querySelector('#js-demo-calendar').focusCentralDate()}"
>
Central date
</lion-button>
<lion-button
@click="${() => document.querySelector('#js-demo-calendar').focusSelectedDate()}"
>
Selected date
</lion-button>
<lion-button @click="${() => document.querySelector('#js-demo-calendar').focusDate(today)}">
Today
</lion-button>
</p>
<p>Be aware that the central date changes when a new date is focused.</p>
`;
})
.add('minDate', () => {
const today = new Date();
const minDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 2);
return html`
<style>
${calendarDemoStyle}
</style>
<lion-calendar class="demo-calendar" .minDate="${minDate}"></lion-calendar>
`;
})
.add('maxDate', () => {
const today = new Date();
const maxDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 2);
return html`
<style>
${calendarDemoStyle}
</style>
<lion-calendar class="demo-calendar" .maxDate="${maxDate}"></lion-calendar>
`;
})
.add(
'disableDates',
() => html`
<style>
${calendarDemoStyle}
</style>
<lion-calendar
class="demo-calendar"
.disableDates=${day => day.getDay() === 6 || day.getDay() === 0}
></lion-calendar>
`,
)
.add('combined disabled dates', () => {
const today = new Date();
const maxDate = new Date(today.getFullYear(), today.getMonth() + 2, today.getDate());
return html`
<style>
${calendarDemoStyle}
</style>
<lion-calendar
class="demo-calendar"
.disableDates=${day => day.getDay() === 6 || day.getDay() === 0}
.minDate="${new Date()}"
.maxDate="${maxDate}"
></lion-calendar>
`;
});

View file

@ -0,0 +1,49 @@
if (typeof window.KeyboardEvent !== 'function') {
// e.g. is IE and needs "polyfill"
const KeyboardEvent = (event, _params) => {
// current spec for it https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent
const params = {
bubbles: false,
cancelable: false,
view: document.defaultView,
key: false,
location: false,
ctrlKey: false,
shiftKey: false,
altKey: false,
metaKey: false,
repeat: false,
..._params,
};
const modifiersListArray = [];
if (params.ctrlKey) {
modifiersListArray.push('Control');
}
if (params.shiftKey) {
modifiersListArray.push('Shift');
}
if (params.altKey) {
modifiersListArray.push('Alt');
}
if (params.metaKey) {
modifiersListArray.push('Meta');
}
const ev = document.createEvent('KeyboardEvent');
// IE Spec for it https://technet.microsoft.com/en-us/windows/ff975297(v=vs.60)
ev.initKeyboardEvent(
event,
params.bubbles,
params.cancelable,
params.view,
params.key,
params.location,
modifiersListArray.join(' '),
params.repeat ? 1 : 0,
params.locale,
);
return ev;
};
KeyboardEvent.prototype = window.Event.prototype;
window.KeyboardEvent = KeyboardEvent;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,221 @@
export const weekdayNames = {
'en-GB': {
Sunday: {
long: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
},
},
};
/**
* Abstraction around calendar day DOM structure,
* allows for writing readable, 'DOM structure agnostic' tests
*/
export class DayObject {
constructor(dayEl) {
this.el = dayEl;
}
/**
* Node references
*/
get calendarShadowRoot() {
return this.el.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;
}
get cellEl() {
return this.el.parentElement;
}
get buttonEl() {
return this.el;
}
/**
* States
*/
get isDisabled() {
return this.buttonEl.hasAttribute('disabled');
}
get isSelected() {
return this.buttonEl.hasAttribute('selected');
}
get isToday() {
return this.buttonEl.hasAttribute('today');
}
get isCentral() {
return this.buttonEl.getAttribute('tabindex') === '0';
}
get isFocused() {
this.calendarShadowRoot.activeElement;
this.buttonEl;
return this.calendarShadowRoot.activeElement === this.buttonEl;
}
get monthday() {
return Number(this.buttonEl.textContent);
}
/**
* Text
*/
get weekdayNameShort() {
const weekdayEls = Array.from(
this.el.parentElement.parentElement.querySelectorAll('.calendar__day-cell'),
);
const dayIndex = weekdayEls.indexOf(this.el.parentElement);
return weekdayNames['en-GB'].Sunday.short[dayIndex];
}
get weekdayNameLong() {
const weekdayEls = Array.from(
this.el.parentElement.parentElement.querySelectorAll('.calendar__day-cell'),
);
const dayIndex = weekdayEls.indexOf(this.el.parentElement);
return weekdayNames['en-GB'].Sunday.long[dayIndex];
}
/**
* Other
*/
get cellIndex() {
return Array.from(this.cellEl.parentElement.children).indexOf(this.cellEl);
}
}
/**
* Abstraction around calendar DOM structure,
* allows for writing readable, 'DOM structure agnostic' tests
*/
export class CalendarObject {
constructor(calendarEl) {
this.el = calendarEl;
}
/**
* Node references
*/
get rootEl() {
return this.el.shadowRoot.querySelector('.calendar');
}
get headerEl() {
return this.el.shadowRoot.querySelector('.calendar__header');
}
get monthHeadingEl() {
return this.el.shadowRoot.querySelector('.calendar__month-heading');
}
get nextMonthButtonEl() {
return this.el.shadowRoot.querySelector('.calendar__next-month-button');
}
get previousMonthButtonEl() {
return this.el.shadowRoot.querySelector('.calendar__previous-month-button');
}
get gridEl() {
return this.el.shadowRoot.querySelector('.calendar__grid');
}
get weekdayHeaderEls() {
return [].slice.call(this.el.shadowRoot.querySelectorAll('.calendar__weekday-header'));
}
get dayEls() {
return [].slice.call(
this.el.shadowRoot.querySelectorAll('.calendar__day-button[current-month]'),
);
}
get previousMonthDayEls() {
return [].slice.call(
this.el.shadowRoot.querySelectorAll('.calendar__day-button[previous-month]'),
);
}
get nextMonthDayEls() {
return [].slice.call(this.el.shadowRoot.querySelectorAll('.calendar__day-button[next-month]'));
}
get dayObjs() {
return this.dayEls.map(d => new DayObject(d));
}
get previousMonthDayObjs() {
return this.previousMonthDayEls.map(d => new DayObject(d));
}
get nextMonthDayObjs() {
return this.nextMonthDayEls.map(d => new DayObject(d));
}
getDayEl(monthDayNumber) {
// Relies on the fact that empty cells don't have .calendar__day-button[current-month]
return this.el.shadowRoot.querySelectorAll('.calendar__day-button[current-month]')[
monthDayNumber - 1
];
}
getDayObj(monthDayNumber) {
return new DayObject(this.getDayEl(monthDayNumber));
}
get selectedDayObj() {
return this.dayObjs.find(d => d.selected);
}
get centralDayObj() {
return this.dayObjs.find(d => d.isCentral);
}
get focusedDayObj() {
return this.dayObjs.find(d => d.el === this.el.shadowRoot.activeElement);
}
/**
* @desc Applies condition to all days, or days in filter
*
* @param {function} condition : condition that should apply for "filter" days
* - Example: "(dayObj) => dayObj.selected"
* @param {array|function} filter - month day numbers for which condition should apply.
* - Example 1: "[15, 20]"
* - Example 2: "(dayNumber) => dayNumber === 15" (1 based ,not zero based)
*/
checkForAllDayObjs(condition, filter) {
return this.dayEls.every(d => {
const dayObj = new DayObject(d);
const dayNumber = dayObj.monthday;
let shouldApply = true;
if (filter !== undefined) {
shouldApply = filter instanceof Array ? filter.includes(dayNumber) : filter(dayNumber);
}
// for instance, should be 'disabled' for the 15th and 20th day
return !shouldApply || (condition(dayObj) && shouldApply);
});
}
/**
* States
*/
get activeMonthAndYear() {
return this.monthHeadingEl.textContent.trim();
}
get activeMonth() {
return this.activeMonthAndYear.split(' ')[0];
}
get activeYear() {
return this.activeMonthAndYear.split(' ')[1];
}
}

View file

@ -0,0 +1,45 @@
import { expect } from '@open-wc/testing';
import { createMonth } from '../../src/utils/createMonth.js';
import { createWeek } from '../../src/utils/createWeek.js';
function compareMonth(obj) {
obj.weeks.forEach((week, weeki) => {
week.days.forEach((day, dayi) => {
// eslint-disable-next-line no-param-reassign
obj.weeks[weeki].days[dayi].date = obj.weeks[weeki].days[dayi].date.toISOString();
});
});
return obj;
}
describe('createMonth', () => {
it('creates month data with Sunday as first day of week by default', () => {
expect(compareMonth(createMonth(new Date('2018/12/01')))).to.deep.equal(
compareMonth({
weeks: [
createWeek(new Date('2018/11/25'), { firstDayOfWeek: 0 }),
createWeek(new Date('2018/12/02'), { firstDayOfWeek: 0 }),
createWeek(new Date('2018/12/09'), { firstDayOfWeek: 0 }),
createWeek(new Date('2018/12/16'), { firstDayOfWeek: 0 }),
createWeek(new Date('2018/12/23'), { firstDayOfWeek: 0 }),
createWeek(new Date('2018/12/30'), { firstDayOfWeek: 0 }),
],
}),
);
});
it('can create month data for different first day of week', () => {
expect(compareMonth(createMonth(new Date('2018/12/01'), { firstDayOfWeek: 1 }))).to.deep.equal(
compareMonth({
weeks: [
createWeek(new Date('2018/11/26'), { firstDayOfWeek: 1 }),
createWeek(new Date('2018/12/03'), { firstDayOfWeek: 1 }),
createWeek(new Date('2018/12/10'), { firstDayOfWeek: 1 }),
createWeek(new Date('2018/12/17'), { firstDayOfWeek: 1 }),
createWeek(new Date('2018/12/24'), { firstDayOfWeek: 1 }),
createWeek(new Date('2018/12/31'), { firstDayOfWeek: 1 }),
],
}),
);
});
});

View file

@ -0,0 +1,71 @@
import { expect } from '@open-wc/testing';
import { createMultipleMonth } from '../../src/utils/createMultipleMonth.js';
import { createMonth } from '../../src/utils/createMonth.js';
function compareMultipleMonth(obj) {
obj.months.forEach((month, monthi) => {
month.weeks.forEach((week, weeki) => {
week.days.forEach((day, dayi) => {
// eslint-disable-next-line no-param-reassign
obj.months[monthi].weeks[weeki].days[dayi].date = obj.months[monthi].weeks[weeki].days[
dayi
].date.toISOString();
});
});
});
return obj;
}
describe('createMultipleMonth', () => {
it('creates 1 month by default', () => {
expect(compareMultipleMonth(createMultipleMonth(new Date('2018/12/01')))).to.deep.equal(
compareMultipleMonth({
months: [createMonth(new Date('2018/12/01'))],
}),
);
});
it('can create extra months in the past', () => {
expect(
compareMultipleMonth(createMultipleMonth(new Date('2018/12/01'), { pastMonths: 2 })),
).to.deep.equal(
compareMultipleMonth({
months: [
createMonth(new Date('2018/10/01')),
createMonth(new Date('2018/11/01')),
createMonth(new Date('2018/12/01')),
],
}),
);
});
it('can create extra months in the future', () => {
expect(
compareMultipleMonth(createMultipleMonth(new Date('2018/12/01'), { futureMonths: 2 })),
).to.deep.equal(
compareMultipleMonth({
months: [
createMonth(new Date('2018/12/01')),
createMonth(new Date('2019/01/01')),
createMonth(new Date('2019/02/01')),
],
}),
);
});
it('can create extra months in the past and future', () => {
expect(
compareMultipleMonth(
createMultipleMonth(new Date('2018/12/01'), { pastMonths: 1, futureMonths: 1 }),
),
).to.deep.equal(
compareMultipleMonth({
months: [
createMonth(new Date('2018/11/01')),
createMonth(new Date('2018/12/01')),
createMonth(new Date('2019/01/01')),
],
}),
);
});
});

View file

@ -0,0 +1,47 @@
import { expect } from '@open-wc/testing';
import { createWeek } from '../../src/utils/createWeek.js';
import { createDay } from '../../src/utils/createDay.js';
function compareWeek(obj) {
for (let i = 0; i < 7; i += 1) {
// eslint-disable-next-line no-param-reassign
obj.days[i].date = obj.days[i].date.toISOString();
}
return obj;
}
describe('createWeek', () => {
it('creates week data starting from Sunday by default', () => {
// https://www.timeanddate.com/date/weeknumber.html?d1=30&m1=12&y1=2018&w2=&y2=&wncm=1&wncd=1&wncs=4&fdow=7
expect(compareWeek(createWeek(new Date('2018/12/30')))).to.deep.equal(
compareWeek({
days: [
createDay(new Date('2018/12/30'), { weekOrder: 0, startOfWeek: true }),
createDay(new Date('2018/12/31'), { weekOrder: 1 }),
createDay(new Date('2019/01/01'), { weekOrder: 2 }),
createDay(new Date('2019/01/02'), { weekOrder: 3 }),
createDay(new Date('2019/01/03'), { weekOrder: 4 }),
createDay(new Date('2019/01/04'), { weekOrder: 5 }),
createDay(new Date('2019/01/05'), { weekOrder: 6 }),
],
}),
);
});
it('can create week data starting from different day', () => {
// https://www.timeanddate.com/date/weeknumber.html?d1=31&m1=12&y1=2018&w2=&y2=&wncm=1&wncd=1&wncs=4&fdow=0
expect(compareWeek(createWeek(new Date('2018/12/31'), { firstDayOfWeek: 1 }))).to.deep.equal(
compareWeek({
days: [
createDay(new Date('2018/12/31'), { weekOrder: 0, startOfWeek: true }),
createDay(new Date('2019/01/01'), { weekOrder: 1 }),
createDay(new Date('2019/01/02'), { weekOrder: 2 }),
createDay(new Date('2019/01/03'), { weekOrder: 3 }),
createDay(new Date('2019/01/04'), { weekOrder: 4 }),
createDay(new Date('2019/01/05'), { weekOrder: 5 }),
createDay(new Date('2019/01/06'), { weekOrder: 6 }),
],
}),
);
});
});

View file

@ -0,0 +1,24 @@
/* eslint-disable no-unused-expressions */
import { expect, fixture } from '@open-wc/testing';
import { createMultipleMonth } from '../../src/utils/createMultipleMonth.js';
import { dataTemplate } from '../../src/utils/dataTemplate.js';
import { weekdayNames } from '../test-utils.js';
// eslint-disable-next-line camelcase
import snapshot_enGB_Sunday_201812 from './snapshots/monthTemplate_en-GB_Sunday_2018-12.js';
describe('dataTemplate', () => {
it('renders one month table', async () => {
const date = new Date('2018/12/01');
const month = createMultipleMonth(date, { firstDayOfWeek: 0 });
const el = await fixture(
dataTemplate(month, {
weekdaysShort: weekdayNames['en-GB'].Sunday.short,
weekdays: weekdayNames['en-GB'].Sunday.long,
}),
);
expect(el).dom.to.equal(snapshot_enGB_Sunday_201812);
});
});

View file

@ -0,0 +1,28 @@
/* eslint-disable no-unused-expressions */
import { expect, fixture } from '@open-wc/testing';
import { createDay } from '../../src/utils/createDay.js';
import { dayTemplate } from '../../src/utils/dayTemplate.js';
describe('dayTemplate', () => {
it('renders day cell', async () => {
const day = createDay(new Date('2019/04/19'), { weekOrder: 5 });
const el = await fixture(
dayTemplate(day, {
weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
}),
);
expect(el).dom.to.equal(`
<td role="gridcell" class="calendar__day-cell">
<button
class="calendar__day-button"
tabindex="-1"
aria-label="19 April 2019 Friday"
aria-selected="false"
>
19
</button>
</td>
`);
});
});

View file

@ -0,0 +1,11 @@
import { expect } from '@open-wc/testing';
import { formatDate } from '../../../localize/src/date/formatDate.js';
import { getFirstDayNextMonth } from '../../src/utils/getFirstDayNextMonth.js';
describe('getFirstDayNextMonth', () => {
it('returns the first day of the next month', () => {
expect(formatDate(getFirstDayNextMonth(new Date('2001/01/01')))).to.be.equal('01/02/2001');
expect(formatDate(getFirstDayNextMonth(new Date('2001/10/10')))).to.be.equal('01/11/2001');
expect(formatDate(getFirstDayNextMonth(new Date('2000/03/10')))).to.be.equal('01/04/2000');
});
});

View file

@ -0,0 +1,11 @@
import { expect } from '@open-wc/testing';
import { formatDate } from '../../../localize/src/date/formatDate.js';
import { getLastDayPreviousMonth } from '../../src/utils/getLastDayPreviousMonth.js';
describe('getLastDayPreviousMonth', () => {
it('returns the last day of the previous month', () => {
expect(formatDate(getLastDayPreviousMonth(new Date('2001/01/01')))).to.be.equal('31/12/2000');
expect(formatDate(getLastDayPreviousMonth(new Date('2001/10/10')))).to.be.equal('30/09/2001');
expect(formatDate(getLastDayPreviousMonth(new Date('2000/03/10')))).to.be.equal('29/02/2000');
});
});

View file

@ -0,0 +1,19 @@
import { expect } from '@open-wc/testing';
import { isSameDate } from '../../src/utils/isSameDate.js';
describe('isSameDate', () => {
it('returns true if the same date is given', () => {
const day1 = new Date('2001/01/01');
const day2 = new Date('2001/01/01');
const day3 = new Date('2002/02/02');
expect(isSameDate(day1, day2)).to.be.true;
expect(isSameDate(day1, day3)).to.be.false;
});
it('returns false if not a date is provided', () => {
const day = new Date('2001/01/01');
expect(isSameDate(day, undefined)).to.be.false;
expect(isSameDate(undefined, day)).to.be.false;
expect(isSameDate(undefined, undefined)).to.be.false;
});
});

View file

@ -0,0 +1,488 @@
const html = strings => strings[0];
export default html`
<div id="js-content-wrapper">
<table
aria-labelledby="month_and_year"
aria-readonly="true"
class="calendar__grid"
data-wrap-cols=""
role="grid"
>
<thead>
<tr role="row">
<th class="calendar__weekday-header" aria-label="Sunday" scope="col" role="columnheader">
Sun
</th>
<th class="calendar__weekday-header" aria-label="Monday" scope="col" role="columnheader">
Mon
</th>
<th class="calendar__weekday-header" aria-label="Tuesday" scope="col" role="columnheader">
Tue
</th>
<th
class="calendar__weekday-header"
aria-label="Wednesday"
scope="col"
role="columnheader"
>
Wed
</th>
<th
class="calendar__weekday-header"
aria-label="Thursday"
scope="col"
role="columnheader"
>
Thu
</th>
<th class="calendar__weekday-header" aria-label="Friday" scope="col" role="columnheader">
Fri
</th>
<th
class="calendar__weekday-header"
aria-label="Saturday"
scope="col"
role="columnheader"
>
Sat
</th>
</tr>
</thead>
<tbody>
<tr role="row">
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="25 November 2018 Sunday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
25
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="26 November 2018 Monday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
26
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="27 November 2018 Tuesday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
27
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="28 November 2018 Wednesday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
28
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="29 November 2018 Thursday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
29
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="30 November 2018 Friday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
30
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="1 December 2018 Saturday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
1
</button>
</td>
</tr>
<tr role="row">
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="2 December 2018 Sunday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
2
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="3 December 2018 Monday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
3
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="4 December 2018 Tuesday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
4
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="5 December 2018 Wednesday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
5
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="6 December 2018 Thursday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
6
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="7 December 2018 Friday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
7
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="8 December 2018 Saturday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
8
</button>
</td>
</tr>
<tr role="row">
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="9 December 2018 Sunday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
9
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="10 December 2018 Monday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
10
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="11 December 2018 Tuesday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
11
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="12 December 2018 Wednesday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
12
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="13 December 2018 Thursday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
13
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="14 December 2018 Friday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
14
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="15 December 2018 Saturday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
15
</button>
</td>
</tr>
<tr role="row">
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="16 December 2018 Sunday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
16
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="17 December 2018 Monday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
17
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="18 December 2018 Tuesday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
18
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="19 December 2018 Wednesday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
19
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="20 December 2018 Thursday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
20
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="21 December 2018 Friday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
21
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="22 December 2018 Saturday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
22
</button>
</td>
</tr>
<tr role="row">
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="23 December 2018 Sunday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
23
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="24 December 2018 Monday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
24
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="25 December 2018 Tuesday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
25
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="26 December 2018 Wednesday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
26
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="27 December 2018 Thursday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
27
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="28 December 2018 Friday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
28
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="29 December 2018 Saturday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
29
</button>
</td>
</tr>
<tr role="row">
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="30 December 2018 Sunday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
30
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="31 December 2018 Monday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
31
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="1 January 2019 Tuesday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
1
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="2 January 2019 Wednesday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
2
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="3 January 2019 Thursday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
3
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="4 January 2019 Friday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
4
</button>
</td>
<td class="calendar__day-cell" role="gridcell">
<button
aria-label="5 January 2019 Saturday"
aria-selected="false"
class="calendar__day-button"
tabindex="-1"
>
5
</button>
</td>
</tr>
</tbody>
</table>
</div>
`;

View file

@ -0,0 +1,4 @@
export default {
nextMonth: 'Следващ месец',
previousMonth: 'Предишен месец',
};

View file

@ -0,0 +1,4 @@
export default {
nextMonth: 'Příští měsíc',
previousMonth: 'Předchozí měsíc',
};

View file

@ -0,0 +1,4 @@
export default {
nextMonth: 'Nächster Monat',
previousMonth: 'Vorheriger Monat',
};

View file

@ -0,0 +1,4 @@
export default {
nextMonth: 'Next month',
previousMonth: 'Previous month',
};

View file

@ -0,0 +1,4 @@
export default {
nextMonth: 'Mes siguiente',
previousMonth: 'Mes anterior',
};

View file

@ -0,0 +1,4 @@
export default {
nextMonth: 'Mois prochain',
previousMonth: 'Mois précédent',
};

View file

@ -0,0 +1,4 @@
export default {
nextMonth: 'Következő hónap',
previousMonth: 'Előző hónap',
};

View file

@ -0,0 +1,4 @@
export default {
nextMonth: 'Mese successivo',
previousMonth: 'Mese precedente',
};

View file

@ -0,0 +1,4 @@
export default {
nextMonth: 'Volgende maand',
previousMonth: 'Vorige maand',
};

View file

@ -0,0 +1,4 @@
export default {
nextMonth: 'Następny miesiąc',
previousMonth: 'Poprzedni miesiąc',
};

View file

@ -0,0 +1,4 @@
export default {
nextMonth: 'Luna viitoare',
previousMonth: 'Luna anterioară',
};

View file

@ -0,0 +1,4 @@
export default {
nextMonth: 'Следующий месяц',
previousMonth: 'Предыдущий месяц',
};

View file

@ -0,0 +1,4 @@
export default {
nextMonth: 'Nasledujúci mesiac',
previousMonth: 'Predchádzajúci mesiac',
};

View file

@ -0,0 +1,4 @@
export default {
nextMonth: 'Наступний місяць',
previousMonth: 'Попередній місяць',
};

View file

@ -22,3 +22,4 @@ import '../packages/localize/stories/index.stories.js';
import '../packages/overlays/stories/index.stories.js';
import '../packages/popup/stories/index.stories.js';
import '../packages/tooltip/stories/index.stories.js';
import '../packages/calendar/stories/index.stories.js';