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:
parent
043106c1cf
commit
9fc5488175
44 changed files with 3275 additions and 0 deletions
44
packages/calendar/README.md
Normal file
44
packages/calendar/README.md
Normal 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>
|
||||
```
|
||||
1
packages/calendar/index.js
Normal file
1
packages/calendar/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { LionCalendar } from './src/LionCalendar.js';
|
||||
3
packages/calendar/lion-calendar.js
Normal file
3
packages/calendar/lion-calendar.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { LionCalendar } from './src/LionCalendar.js';
|
||||
|
||||
customElements.define('lion-calendar', LionCalendar);
|
||||
43
packages/calendar/package.json
Normal file
43
packages/calendar/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
547
packages/calendar/src/LionCalendar.js
Normal file
547
packages/calendar/src/LionCalendar.js
Normal 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}
|
||||
>
|
||||
<
|
||||
</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}
|
||||
>
|
||||
>
|
||||
</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;
|
||||
}
|
||||
}
|
||||
74
packages/calendar/src/calendarStyle.js
Normal file
74
packages/calendar/src/calendarStyle.js
Normal 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;
|
||||
}
|
||||
`;
|
||||
29
packages/calendar/src/utils/createDay.js
Normal file
29
packages/calendar/src/utils/createDay.js
Normal 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,
|
||||
};
|
||||
}
|
||||
25
packages/calendar/src/utils/createMonth.js
Normal file
25
packages/calendar/src/utils/createMonth.js
Normal 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;
|
||||
}
|
||||
26
packages/calendar/src/utils/createMultipleMonth.js
Normal file
26
packages/calendar/src/utils/createMultipleMonth.js
Normal 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;
|
||||
}
|
||||
30
packages/calendar/src/utils/createWeek.js
Normal file
30
packages/calendar/src/utils/createWeek.js
Normal 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;
|
||||
}
|
||||
51
packages/calendar/src/utils/dataTemplate.js
Normal file
51
packages/calendar/src/utils/dataTemplate.js
Normal 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>
|
||||
`;
|
||||
}
|
||||
46
packages/calendar/src/utils/dayTemplate.js
Normal file
46
packages/calendar/src/utils/dayTemplate.js
Normal 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>
|
||||
`;
|
||||
}
|
||||
33
packages/calendar/src/utils/differentKeyNamesShimIE.js
Normal file
33
packages/calendar/src/utils/differentKeyNamesShimIE.js
Normal 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;
|
||||
},
|
||||
});
|
||||
}
|
||||
13
packages/calendar/src/utils/getFirstDayNextMonth.js
Normal file
13
packages/calendar/src/utils/getFirstDayNextMonth.js
Normal 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;
|
||||
}
|
||||
12
packages/calendar/src/utils/getLastDayPreviousMonth.js
Normal file
12
packages/calendar/src/utils/getLastDayPreviousMonth.js
Normal 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);
|
||||
}
|
||||
17
packages/calendar/src/utils/isSameDate.js
Normal file
17
packages/calendar/src/utils/isSameDate.js
Normal 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()
|
||||
);
|
||||
}
|
||||
136
packages/calendar/stories/index.stories.js
Executable file
136
packages/calendar/stories/index.stories.js
Executable 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>
|
||||
`;
|
||||
});
|
||||
49
packages/calendar/test/keyboardEventShimIE.js
Normal file
49
packages/calendar/test/keyboardEventShimIE.js
Normal 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;
|
||||
}
|
||||
1074
packages/calendar/test/lion-calendar.test.js
Normal file
1074
packages/calendar/test/lion-calendar.test.js
Normal file
File diff suppressed because it is too large
Load diff
221
packages/calendar/test/test-utils.js
Normal file
221
packages/calendar/test/test-utils.js
Normal 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];
|
||||
}
|
||||
}
|
||||
45
packages/calendar/test/utils/createMonth.test.js
Normal file
45
packages/calendar/test/utils/createMonth.test.js
Normal 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 }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
71
packages/calendar/test/utils/createMultipleMonth.test.js
Normal file
71
packages/calendar/test/utils/createMultipleMonth.test.js
Normal 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')),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
47
packages/calendar/test/utils/createWeek.test.js
Normal file
47
packages/calendar/test/utils/createWeek.test.js
Normal 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 }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
24
packages/calendar/test/utils/dataTemplate.test.js
Normal file
24
packages/calendar/test/utils/dataTemplate.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
28
packages/calendar/test/utils/dayTemplate.test.js
Normal file
28
packages/calendar/test/utils/dayTemplate.test.js
Normal 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>
|
||||
`);
|
||||
});
|
||||
});
|
||||
11
packages/calendar/test/utils/getFirstDayNextMonth.test.js
Normal file
11
packages/calendar/test/utils/getFirstDayNextMonth.test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
11
packages/calendar/test/utils/getLastDayPreviousMonth.test.js
Normal file
11
packages/calendar/test/utils/getLastDayPreviousMonth.test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
19
packages/calendar/test/utils/isSameDate.test.js
Normal file
19
packages/calendar/test/utils/isSameDate.test.js
Normal 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;
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
`;
|
||||
4
packages/calendar/translations/bg.js
Normal file
4
packages/calendar/translations/bg.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
nextMonth: 'Следващ месец',
|
||||
previousMonth: 'Предишен месец',
|
||||
};
|
||||
4
packages/calendar/translations/cs.js
Normal file
4
packages/calendar/translations/cs.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
nextMonth: 'Příští měsíc',
|
||||
previousMonth: 'Předchozí měsíc',
|
||||
};
|
||||
4
packages/calendar/translations/de.js
Normal file
4
packages/calendar/translations/de.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
nextMonth: 'Nächster Monat',
|
||||
previousMonth: 'Vorheriger Monat',
|
||||
};
|
||||
4
packages/calendar/translations/en.js
Normal file
4
packages/calendar/translations/en.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
nextMonth: 'Next month',
|
||||
previousMonth: 'Previous month',
|
||||
};
|
||||
4
packages/calendar/translations/es.js
Normal file
4
packages/calendar/translations/es.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
nextMonth: 'Mes siguiente',
|
||||
previousMonth: 'Mes anterior',
|
||||
};
|
||||
4
packages/calendar/translations/fr.js
Normal file
4
packages/calendar/translations/fr.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
nextMonth: 'Mois prochain',
|
||||
previousMonth: 'Mois précédent',
|
||||
};
|
||||
4
packages/calendar/translations/hu.js
Normal file
4
packages/calendar/translations/hu.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
nextMonth: 'Következő hónap',
|
||||
previousMonth: 'Előző hónap',
|
||||
};
|
||||
4
packages/calendar/translations/it.js
Normal file
4
packages/calendar/translations/it.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
nextMonth: 'Mese successivo',
|
||||
previousMonth: 'Mese precedente',
|
||||
};
|
||||
4
packages/calendar/translations/nl.js
Normal file
4
packages/calendar/translations/nl.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
nextMonth: 'Volgende maand',
|
||||
previousMonth: 'Vorige maand',
|
||||
};
|
||||
4
packages/calendar/translations/pl.js
Normal file
4
packages/calendar/translations/pl.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
nextMonth: 'Następny miesiąc',
|
||||
previousMonth: 'Poprzedni miesiąc',
|
||||
};
|
||||
4
packages/calendar/translations/ro.js
Normal file
4
packages/calendar/translations/ro.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
nextMonth: 'Luna viitoare',
|
||||
previousMonth: 'Luna anterioară',
|
||||
};
|
||||
4
packages/calendar/translations/ru.js
Normal file
4
packages/calendar/translations/ru.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
nextMonth: 'Следующий месяц',
|
||||
previousMonth: 'Предыдущий месяц',
|
||||
};
|
||||
4
packages/calendar/translations/sk.js
Normal file
4
packages/calendar/translations/sk.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
nextMonth: 'Nasledujúci mesiac',
|
||||
previousMonth: 'Predchádzajúci mesiac',
|
||||
};
|
||||
4
packages/calendar/translations/uk.js
Normal file
4
packages/calendar/translations/uk.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
nextMonth: 'Наступний місяць',
|
||||
previousMonth: 'Попередній місяць',
|
||||
};
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in a new issue