[perf] calendar-sk
Change-Id: I421e48106ed6649fb490b22c0ca5b3cdc2c11d04
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/299440
Commit-Queue: Joe Gregorio <jcgregorio@google.com>
Reviewed-by: Leandro Lovisolo <lovisolo@google.com>
diff --git a/perf/modules/calendar-sk/calendar-sk-demo.html b/perf/modules/calendar-sk/calendar-sk-demo.html
new file mode 100644
index 0000000..8e5b3a5
--- /dev/null
+++ b/perf/modules/calendar-sk/calendar-sk-demo.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>calendar-sk</title>
+ <meta charset="utf-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <style>
+ section {
+ padding: 16px;
+ }
+ </style>
+</head>
+
+<body>
+ <section class="body-sk">
+ <h2>themes.css</h2>
+ <calendar-sk></calendar-sk>
+ </section>
+
+ <section class="body-sk darkmode">
+ <h2>themes.css - darkmode</h2>
+ <calendar-sk></calendar-sk>
+ <h2>zh-Hans-CN</h2>
+ <calendar-sk></calendar-sk>
+ </section>
+ <pre><code id=evt></code></pre>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/perf/modules/calendar-sk/calendar-sk-demo.ts b/perf/modules/calendar-sk/calendar-sk-demo.ts
new file mode 100644
index 0000000..72f689e
--- /dev/null
+++ b/perf/modules/calendar-sk/calendar-sk-demo.ts
@@ -0,0 +1,16 @@
+import './index.ts';
+import { CalendarSk } from './calendar-sk';
+
+const evt = document.getElementById('evt')!;
+const locales = [undefined, undefined, 'zh-Hans-CN'];
+document.querySelectorAll<CalendarSk>('calendar-sk').forEach((ele, i) => {
+ ele.displayDate = new Date(2020, 4, 21);
+ ele.locale = locales[i];
+ ele.addEventListener('change', (e) => {
+ evt.innerText = (e as CustomEvent<Date>).detail.toString();
+ });
+});
+
+document.addEventListener('keydown', (e) =>
+ document.querySelector<CalendarSk>('calendar-sk')!.keyboardHandler(e)
+);
diff --git a/perf/modules/calendar-sk/calendar-sk.scss b/perf/modules/calendar-sk/calendar-sk.scss
new file mode 100644
index 0000000..0230608
--- /dev/null
+++ b/perf/modules/calendar-sk/calendar-sk.scss
@@ -0,0 +1,54 @@
+@import '~elements-sk/themes/themes.css';
+
+calendar-sk {
+ h2 {
+ margin: 0;
+ padding: 0;
+ }
+
+ td {
+ font-size: 120%;
+ text-align: center;
+ border: solid 2px var(--background);
+ }
+
+ td.today button {
+ color: var(--secondary);
+ }
+
+ td.selected {
+ border: solid 2px var(--secondary);
+ }
+
+ th.clickable {
+ cursor: pointer;
+ }
+
+ th.clickable:hover {
+ background: var(--surface-1dp);
+ }
+
+ tr.weekdayHeader {
+ opacity: 0.6;
+ }
+
+ button {
+ cursor: pointer;
+ margin: 0;
+ padding: 0em;
+ min-width: 16px;
+ min-height: 0;
+ height: 20px;
+ width: 20px;
+ text-transform: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ navigate-next-icon-sk svg.icon-sk-svg,
+ navigate-before-icon-sk svg.icon-sk-svg {
+ width: 16px;
+ height: 16px;
+ }
+ }
+}
diff --git a/perf/modules/calendar-sk/calendar-sk.ts b/perf/modules/calendar-sk/calendar-sk.ts
new file mode 100644
index 0000000..583b6e5
--- /dev/null
+++ b/perf/modules/calendar-sk/calendar-sk.ts
@@ -0,0 +1,419 @@
+/**
+ * @module modules/calendar-sk
+ * @description <h2><code>calendar-sk</code></h2>
+ *
+ * Displays an accessible calendar, one month at a time, and allows selecting a
+ * single day. Offers the ability to navigate by both month and year. Also is
+ * themeable and offers keyboard navigation.
+ *
+ * Why not use input type="date"? It doesn't work on Safari, and the pop-up
+ * calendar isn't styleable.
+ *
+ * Why not use the Elix web component? It is not themeable (at least not
+ * easily), and it is also inaccessible.
+ *
+ * Accessibility advice was derived from this page:
+ * https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/datepicker-dialog.html
+ *
+ * @evt change - A CustomEvent with the selected Date in the detail.
+ *
+ * This element provides a keyboardHandler callback that should be attached and
+ * detached to/from the appropriate containing element when it is used, for
+ * example, a containing 'dialog' element.
+ */
+import {html, TemplateResult} from 'lit-html';
+import {ElementSk} from '../../../infra-sk/modules/ElementSk';
+import 'elements-sk/styles/buttons';
+import 'elements-sk/icon/navigate-before-icon-sk';
+import 'elements-sk/icon/navigate-next-icon-sk';
+
+/*
+ * Most of the Date[1] object's methods return zero-indexed values, with exceptions such as
+ * Date.prototype.getDate() and Date.prototype.getYear().
+ *
+ * To make things easier to follow, we suffix zero-indexed values returned by a
+ * Date object with "Index", e.g.:
+ *
+ * const dayIndex = date.getDay(); // (0-6).
+ * const monthIndex = date.getMonth(); // (0-11).
+ *
+ * [1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
+ */
+
+const getNumberOfDaysInMonth = (year: number, monthIndex: number) => {
+ // Jump forward one month, and back one day to get the last day of the month.
+ // Since days are 1-indexed, a value of 0 represents the last day of the
+ // previous month.
+ return new Date(year, monthIndex + 1, 0).getDate();
+};
+
+// Returns a day of the week [0-6]
+const firstDayIndexOfMonth = (year: number, monthIndex: number): number => {
+ return new Date(year, monthIndex).getDay();
+};
+
+// Used in templates.
+const sevenDaysInAWeek = [0, 1, 2, 3, 4, 5, 6];
+
+// To display a month we need to display up to 6 weeks. Used in templates.
+const sixWeeks = [0, 1, 2, 3, 4, 5];
+
+// The dates that CalendarSk manipulates, always in local time.
+class CalendarDate {
+ year: number;
+ monthIndex: number;
+ date: number;
+
+ constructor(d: Date) {
+ this.year = d.getFullYear();
+ this.monthIndex = d.getMonth();
+ this.date = d.getDate();
+ }
+
+ equal(d: CalendarDate) {
+ return (
+ this.year === d.year &&
+ this.monthIndex === d.monthIndex &&
+ this.date === d.date
+ );
+ }
+}
+
+export class CalendarSk extends ElementSk {
+ private static template = (ele: CalendarSk) => html`
+ <table>
+ <tr>
+ <th>
+ <button
+ @click=${ele.decYear}
+ aria-label="Previous year"
+ title="Previous year"
+ id="previous-year"
+ >
+ <navigate-before-icon-sk></navigate-before-icon-sk>
+ </button>
+ </th>
+ <th colspan="5">
+ <h2 aria-live="polite" id="calendar-year">
+ ${new Intl.DateTimeFormat(ele._locale, {year: 'numeric'}).format(
+ ele._displayDate
+ )}
+ </h2>
+ </th>
+ <th>
+ <button
+ @click=${ele.incYear}
+ aria-label="Next year"
+ title="Next year"
+ id="next-year"
+ >
+ <navigate-next-icon-sk></navigate-next-icon-sk>
+ </button>
+ </th>
+ </tr>
+ <tr>
+ <th>
+ <button
+ @click=${ele.decMonth}
+ aria-label="Previous month"
+ title="Previous month"
+ id="previous-month"
+ >
+ <navigate-before-icon-sk></navigate-before-icon-sk>
+ </button>
+ </th>
+ <th colspan="5">
+ <h2 aria-live="polite" id="calendar-month">
+ ${new Intl.DateTimeFormat(ele._locale, {month: 'long'}).format(
+ ele._displayDate
+ )}
+ </h2>
+ </th>
+ <th>
+ <button
+ @click=${ele.incMonth}
+ aria-label="Next month"
+ title="Next month"
+ id="next-month"
+ >
+ <navigate-next-icon-sk></navigate-next-icon-sk>
+ </button>
+ </th>
+ </tr>
+ ${ele._weekDayHeader}
+ ${sixWeeks.map((i) => CalendarSk.rowTemplate(ele, i))}
+ </table>
+ `;
+
+ private static buttonForDateTemplate = (
+ ele: CalendarSk,
+ date: number,
+ daysInMonth: number,
+ selected: boolean
+ ) => {
+ if (date < 1 || date > daysInMonth) {
+ return html``;
+ }
+ return html`<button
+ @click=${ele.dateClick}
+ data-date=${date}
+ tabindex=${selected ? 0 : -1}
+ aria-selected=${selected}
+ >
+ ${date}
+ </button>`;
+ };
+
+ private static rowTemplate = (ele: CalendarSk, weekIndex: number) => {
+ const year = ele.displayYear();
+ const monthIndex = ele.displayMonthIndex();
+ const today = new CalendarDate(new Date());
+
+ // If June starts on a Tuesday then IndexOfTheFirstDayOfTheMonth = 2,
+ // which means if we are filling in the first row we want to leave the first
+ // two days (Sunday and Monday) blank.
+ const firstDayOfTheMonthIndex = firstDayIndexOfMonth(year, monthIndex);
+ const daysInMonth = getNumberOfDaysInMonth(year, monthIndex);
+ const selectedDate = ele._displayDate.getDate();
+ const currentDate = new CalendarDate(ele._displayDate);
+ return html`<tr>
+ ${sevenDaysInAWeek.map((i) => {
+ const date = 7 * weekIndex + i + 1 - firstDayOfTheMonthIndex;
+ currentDate.date = date;
+ const selected = selectedDate === date;
+ return html`<td
+ class="
+ ${currentDate.equal(today) ? 'today' : ''}
+ ${selected ? 'selected' : ''}
+ "
+ >
+ ${CalendarSk.buttonForDateTemplate(ele, date, daysInMonth, selected)}
+ </td>`;
+ })}
+ </tr>`;
+ };
+
+ private _displayDate: Date = new Date();
+ private _weekDayHeader: TemplateResult = html``;
+ private _locale: string | string[] | undefined = undefined;
+
+ constructor() {
+ super(CalendarSk.template);
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.buildWeekDayHeader();
+ this._render();
+ }
+
+ /**
+ * Attach this handler to the 'keydown' event on the appropriate elements
+ * parent, such as document or a 'dialog' element.
+ *
+ * Allows finer grained control of keyboard events on a page with more
+ * than one keyboard listener.
+ */
+ keyboardHandler(e: KeyboardEvent) {
+ let keyHandled = true;
+ switch (e.code) {
+ case 'PageUp':
+ this.decMonth();
+ break;
+
+ case 'PageDown':
+ this.incMonth();
+ break;
+
+ case 'ArrowRight':
+ this.incDay();
+ break;
+
+ case 'ArrowLeft':
+ this.decDay();
+ break;
+
+ case 'ArrowUp':
+ this.decWeek();
+ break;
+
+ case 'ArrowDown':
+ this.incWeek();
+ break;
+
+ default:
+ keyHandled = false;
+ break;
+ }
+ if (keyHandled) {
+ e.stopPropagation();
+ e.preventDefault();
+ this.querySelector<HTMLButtonElement>(
+ 'button[aria-selected="true"]'
+ )!.focus();
+ }
+ }
+
+ buildWeekDayHeader() {
+ // March 1, 2020 falls on a Sunday, use that to generate the week day headers.
+ const narrowFormatter = new Intl.DateTimeFormat(this._locale, {
+ weekday: 'narrow',
+ });
+ const longFormatter = new Intl.DateTimeFormat(this._locale, {
+ weekday: 'long',
+ });
+ this._weekDayHeader = html`<tr class="weekdayHeader">
+ ${sevenDaysInAWeek.map(
+ (i) =>
+ html`<td>
+ <span abbr="${longFormatter.format(new Date(2020, 2, i + 1))}">
+ ${narrowFormatter.format(new Date(2020, 2, i + 1))}
+ </span>
+ </td>`
+ )}
+ </tr>`;
+ }
+
+ private dateClick(e: MouseEvent) {
+ const d = new Date(this._displayDate);
+ d.setDate(+(e.target as HTMLButtonElement).dataset.date!);
+ this.dispatchEvent(
+ new CustomEvent<Date>('change', {
+ detail: d,
+ bubbles: true,
+ })
+ );
+ this._displayDate = d;
+ this._render();
+ }
+
+ private displayYear(): number {
+ return this._displayDate.getFullYear();
+ }
+
+ private displayMonthIndex(): number {
+ return this._displayDate.getMonth();
+ }
+
+ private incYear() {
+ const year = this.displayYear();
+ const monthIndex = this.displayMonthIndex();
+ let date = this._displayDate.getDate();
+ const daysInMonth = getNumberOfDaysInMonth(year + 1, monthIndex);
+ if (date > daysInMonth) {
+ date = daysInMonth;
+ }
+ this._displayDate = new Date(year + 1, monthIndex, date);
+ this._render();
+ }
+
+ private decYear() {
+ const year = this.displayYear();
+ const monthIndex = this.displayMonthIndex();
+ let date = this._displayDate.getDate();
+ const daysInMonth = getNumberOfDaysInMonth(year - 1, monthIndex);
+ if (date > daysInMonth) {
+ date = daysInMonth;
+ }
+ this._displayDate = new Date(year - 1, monthIndex, date);
+ this._render();
+ }
+
+ private incMonth() {
+ let year = this.displayYear();
+ let monthIndex = this.displayMonthIndex();
+ let date = this._displayDate.getDate();
+
+ monthIndex += 1;
+ if (monthIndex > 11) {
+ monthIndex = 0;
+ year += 1;
+ }
+
+ const daysInMonth = getNumberOfDaysInMonth(year, monthIndex);
+ if (date > daysInMonth) {
+ date = daysInMonth;
+ }
+
+ this._displayDate = new Date(year, monthIndex, date);
+ this._render();
+ }
+
+ private decMonth() {
+ let year = this.displayYear();
+ let monthIndex = this.displayMonthIndex();
+ let date = this._displayDate.getDate();
+
+ monthIndex -= 1;
+ if (monthIndex < 0) {
+ monthIndex = 11;
+ year -= 1;
+ }
+
+ const daysInMonth = getNumberOfDaysInMonth(year, monthIndex);
+ if (date > daysInMonth) {
+ date = daysInMonth;
+ }
+
+ this._displayDate = new Date(year, monthIndex, date);
+ this._render();
+ }
+
+ private incDay() {
+ const year = this.displayYear();
+ const monthIndex = this.displayMonthIndex();
+ const date = this._displayDate.getDate();
+ this._displayDate = new Date(year, monthIndex, date + 1);
+ this._render();
+ }
+
+ private decDay() {
+ const year = this.displayYear();
+ const monthIndex = this.displayMonthIndex();
+ const date = this._displayDate.getDate();
+ this._displayDate = new Date(year, monthIndex, date - 1);
+ this._render();
+ }
+
+ private incWeek() {
+ const year = this.displayYear();
+ const monthIndex = this.displayMonthIndex();
+ const date = this._displayDate.getDate();
+ this._displayDate = new Date(year, monthIndex, date + 7);
+ this._render();
+ }
+
+ private decWeek() {
+ const year = this.displayYear();
+ const monthIndex = this.displayMonthIndex();
+ const date = this._displayDate.getDate();
+ this._displayDate = new Date(year, monthIndex, date - 7);
+ this._render();
+ }
+
+ /** The date to display on the calendar. */
+ get displayDate() {
+ return this._displayDate;
+ }
+
+ set displayDate(v) {
+ this._displayDate = v;
+ this._render();
+ }
+
+ /**
+ * Leave as undefined to use the browser settings. Only really used for
+ * testing.
+ */
+ public get locale() {
+ return this._locale;
+ }
+
+ public set locale(v) {
+ this._locale = v;
+ this.buildWeekDayHeader();
+ this._render();
+ }
+}
+
+window.customElements.define('calendar-sk', CalendarSk);
diff --git a/perf/modules/calendar-sk/calendar-sk_puppeteer_test.ts b/perf/modules/calendar-sk/calendar-sk_puppeteer_test.ts
new file mode 100644
index 0000000..582f82c
--- /dev/null
+++ b/perf/modules/calendar-sk/calendar-sk_puppeteer_test.ts
@@ -0,0 +1,28 @@
+import * as path from 'path';
+import { expect } from 'chai';
+import {
+ setUpPuppeteerAndDemoPageServer,
+ takeScreenshot,
+} from '../../../puppeteer-tests/util';
+
+describe('calendar-sk', () => {
+ const testBed = setUpPuppeteerAndDemoPageServer(
+ path.join(__dirname, '..', '..', 'webpack.config.ts')
+ );
+
+ beforeEach(async () => {
+ await testBed.page.goto(`${testBed.baseUrl}/dist/calendar-sk.html`);
+ await testBed.page.setViewport({ width: 600, height: 1200 });
+ });
+
+ it('should render the demo page', async () => {
+ // Smoke test.
+ expect(await testBed.page.$$('calendar-sk')).to.have.length(3);
+ });
+
+ describe('screenshots', () => {
+ it('shows the default view', async () => {
+ await takeScreenshot(testBed.page, 'perf', 'calendar-sk');
+ });
+ });
+});
diff --git a/perf/modules/calendar-sk/calendar-sk_test.ts b/perf/modules/calendar-sk/calendar-sk_test.ts
new file mode 100644
index 0000000..ce09a87
--- /dev/null
+++ b/perf/modules/calendar-sk/calendar-sk_test.ts
@@ -0,0 +1,173 @@
+import './index';
+import {assert} from 'chai';
+import {CalendarSk} from './calendar-sk';
+import {
+ setUpElementUnderTest,
+ eventPromise,
+} from '../../../infra-sk/modules/test_util';
+
+const container = document.createElement('div');
+document.body.appendChild(container);
+
+afterEach(() => {
+ container.innerHTML = '';
+});
+
+describe('calendar-sk', () => {
+ const newInstance = setUpElementUnderTest<CalendarSk>('calendar-sk');
+ let calendarSk: CalendarSk;
+ beforeEach(() => {
+ calendarSk = newInstance();
+ calendarSk.displayDate = new Date(2020, 4, 21);
+ });
+
+ describe('event', () => {
+ it('fires when button is clicked', () =>
+ window.customElements.whenDefined('calendar-sk').then(async () => {
+ const event = eventPromise<CustomEvent<Date>>('change');
+ calendarSk
+ .querySelector<HTMLButtonElement>('button[data-date="19"]')!
+ .click();
+
+ const detail = (await event).detail;
+
+ assert.equal(
+ new Date(2020, 4, 19).toDateString(),
+ detail.toDateString(),
+ 'Date has changed.'
+ );
+ assert.equal(getSelectedDate(), 19, 'Selected date has changed.');
+ }));
+ });
+ describe('year', () => {
+ it('decrements when button is clicked', () =>
+ window.customElements.whenDefined('calendar-sk').then(() => {
+ calendarSk.querySelector<HTMLButtonElement>('#previous-year')!.click();
+
+ const year = calendarSk.querySelector<HTMLHeadingElement>(
+ '#calendar-year'
+ )!.innerText;
+ assert.equal(year, '2019', 'Year has changed.');
+ assert.equal(getSelectedDate(), 21, 'Selected date has not changed.');
+ }));
+ it('increments when button is clicked', () =>
+ window.customElements.whenDefined('calendar-sk').then(() => {
+ calendarSk.querySelector<HTMLButtonElement>('#next-year')!.click();
+
+ const year = calendarSk.querySelector<HTMLHeadingElement>(
+ '#calendar-year'
+ )!.innerText;
+ assert.equal(year, '2021', 'Year has changed.');
+ assert.equal(getSelectedDate(), 21, 'Selected date has not changed.');
+ }));
+ });
+ describe('month', () => {
+ it('decrements when button is clicked', () =>
+ window.customElements.whenDefined('calendar-sk').then(() => {
+ calendarSk.querySelector<HTMLButtonElement>('#previous-month')!.click();
+
+ const year = calendarSk.querySelector<HTMLHeadingElement>(
+ '#calendar-month'
+ )!.innerText;
+ assert.equal(year, 'April', 'Month has changed.');
+ assert.equal(getSelectedDate(), 21, 'Selected date has not changed.');
+ }));
+ it('increments when button is clicked', () =>
+ window.customElements.whenDefined('calendar-sk').then(() => {
+ calendarSk.querySelector<HTMLButtonElement>('#next-month')!.click();
+
+ const year = calendarSk.querySelector<HTMLHeadingElement>(
+ '#calendar-month'
+ )!.innerText;
+ assert.equal(year, 'June', 'Month has changed.');
+ assert.equal(getSelectedDate(), 21, 'Selected date has not changed.');
+ }));
+ });
+ describe('keyboard', () => {
+ it('moves to next day when ArrowRight is pressed', () =>
+ window.customElements.whenDefined('calendar-sk').then(() => {
+ calendarSk.keyboardHandler(
+ new KeyboardEvent('keydown', {code: 'ArrowRight'})
+ );
+
+ assert.equal(
+ new Date(2020, 4, 22).toDateString(),
+ calendarSk.displayDate.toDateString(),
+ 'Date has changed.'
+ );
+ assert.equal(getSelectedDate(), 22, 'Selected date has changed.');
+ }));
+ it('moves to previous day when ArrowLeft is pressed', () =>
+ window.customElements.whenDefined('calendar-sk').then(() => {
+ calendarSk.keyboardHandler(
+ new KeyboardEvent('keydown', {code: 'ArrowLeft'})
+ );
+
+ assert.equal(
+ new Date(2020, 4, 20).toDateString(),
+ calendarSk.displayDate.toDateString(),
+ 'Date has changed.'
+ );
+ assert.equal(getSelectedDate(), 20, 'Selected date has changed.');
+ }));
+ it('moves to previous week when ArrowUp is pressed', () =>
+ window.customElements.whenDefined('calendar-sk').then(() => {
+ calendarSk.keyboardHandler(
+ new KeyboardEvent('keydown', {code: 'ArrowUp'})
+ );
+
+ assert.equal(
+ new Date(2020, 4, 14).toDateString(),
+ calendarSk.displayDate.toDateString(),
+ 'Date has changed.'
+ );
+ assert.equal(getSelectedDate(), 14, 'Selected date has changed.');
+ }));
+ it('moves to next week when ArrowDown is pressed', () =>
+ window.customElements.whenDefined('calendar-sk').then(() => {
+ calendarSk.keyboardHandler(
+ new KeyboardEvent('keydown', {code: 'ArrowDown'})
+ );
+
+ assert.equal(
+ new Date(2020, 4, 28).toDateString(),
+ calendarSk.displayDate.toDateString(),
+ 'Date has changed.'
+ );
+ assert.equal(getSelectedDate(), 28, 'Selected date has changed.');
+ }));
+
+ it('moves to previous month when PageUp is pressed', () =>
+ window.customElements.whenDefined('calendar-sk').then(() => {
+ calendarSk.keyboardHandler(
+ new KeyboardEvent('keydown', {code: 'PageUp'})
+ );
+
+ assert.equal(
+ new Date(2020, 3, 21).toDateString(),
+ calendarSk.displayDate.toDateString(),
+ 'Date has changed.'
+ );
+ assert.equal(getSelectedDate(), 21, 'Selected date has not changed.');
+ }));
+ it('moves to next month when PageDown is pressed', () =>
+ window.customElements.whenDefined('calendar-sk').then(() => {
+ calendarSk.keyboardHandler(
+ new KeyboardEvent('keydown', {code: 'PageDown'})
+ );
+
+ assert.equal(
+ new Date(2020, 5, 21).toDateString(),
+ calendarSk.displayDate.toDateString(),
+ 'Date has changed.'
+ );
+ assert.equal(getSelectedDate(), 21, 'Selected date has not changed.');
+ }));
+ });
+
+ const getSelectedDate = () => {
+ return +calendarSk.querySelector<HTMLButtonElement>(
+ '[aria-selected="true"]'
+ )!.innerText;
+ };
+});
diff --git a/perf/modules/calendar-sk/index.ts b/perf/modules/calendar-sk/index.ts
new file mode 100644
index 0000000..79fdbfd
--- /dev/null
+++ b/perf/modules/calendar-sk/index.ts
@@ -0,0 +1,2 @@
+import './calendar-sk';
+import './calendar-sk.scss';