diff --git a/apps/dev/src/datepicker/datepicker-demo.component.scss b/apps/dev/src/datepicker/datepicker-demo.component.scss
new file mode 100644
index 0000000000..2fca28ddd8
--- /dev/null
+++ b/apps/dev/src/datepicker/datepicker-demo.component.scss
@@ -0,0 +1,7 @@
+.dt-checkbox {
+ margin-top: 10px;
+}
+
+.dt-example-dark h2 {
+ color: white;
+}
diff --git a/apps/dev/src/datepicker/datepicker-demo.component.ts b/apps/dev/src/datepicker/datepicker-demo.component.ts
new file mode 100644
index 0000000000..f00d97d0be
--- /dev/null
+++ b/apps/dev/src/datepicker/datepicker-demo.component.ts
@@ -0,0 +1,32 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'demo-component',
+ templateUrl: 'datepicker-demo.component.html',
+ styleUrls: ['datepicker-demo.component.scss'],
+})
+export class DatepickerDemo {
+ startAt = new Date(2020, 7, 31);
+ isDatepickerDisabled = false;
+ isTimepickerDisabled = false;
+ isDarkDatepickerDisabled = false;
+ isDarkTimepickerDisabled = false;
+ isDatepickerTimeEnabled = true;
+ isDarkDatepickerTimeEnabled = true;
+}
diff --git a/apps/dev/src/devapp-routing.module.ts b/apps/dev/src/devapp-routing.module.ts
index 3c24581c39..9549a23cca 100644
--- a/apps/dev/src/devapp-routing.module.ts
+++ b/apps/dev/src/devapp-routing.module.ts
@@ -31,6 +31,7 @@ import { ConsumptionDemo } from './consumption/consumption-demo.component';
import { ContainerBreakpointObserverDemo } from './container-breakpoint-observer/container-breakpoint-observer-demo.component';
import { ContextDialogDemo } from './context-dialog/context-dialog-demo.component';
import { CopyToClipboardDemo } from './copy-to-clipboard/copy-to-clipboard-demo.component';
+import { DatepickerDemo } from './datepicker/datepicker-demo.component';
import { DrawerDemo } from './drawer/drawer-demo.component';
import { DrawerTableDemo } from './drawer-table/drawer-table-demo.component';
import { EmptyStateDemo } from './empty-state/empty-state-demo';
@@ -95,6 +96,7 @@ const routes: Routes = [
{ path: 'context-dialog', component: ContextDialogDemo },
{ path: 'confirmation-dialog', component: ConfirmationDialogDemo },
{ path: 'copy-to-clipboard', component: CopyToClipboardDemo },
+ { path: 'datepicker', component: DatepickerDemo },
{ path: 'drawer', component: DrawerDemo },
{ path: 'drawer-table', component: DrawerTableDemo },
{ path: 'empty-state', component: EmptyStateDemo },
diff --git a/apps/dev/src/devapp.component.ts b/apps/dev/src/devapp.component.ts
index bee35f665b..03db9d8d13 100644
--- a/apps/dev/src/devapp.component.ts
+++ b/apps/dev/src/devapp.component.ts
@@ -58,6 +58,7 @@ export class DevApp implements AfterContentInit, OnDestroy {
},
{ name: 'Context-dialog', route: '/context-dialog' },
{ name: 'Copy-to-clipboard', route: '/copy-to-clipboard' },
+ { name: 'Datepicker', route: '/datepicker' },
{ name: 'Drawer', route: '/drawer' },
{ name: 'Drawer-table', route: '/drawer-table' },
{ name: 'Empty-state', route: '/empty-state' },
diff --git a/apps/dev/src/dt-components.module.ts b/apps/dev/src/dt-components.module.ts
index 4bc8cb3996..22ca853c82 100644
--- a/apps/dev/src/dt-components.module.ts
+++ b/apps/dev/src/dt-components.module.ts
@@ -30,6 +30,7 @@ import { DtConsumptionModule } from '@dynatrace/barista-components/consumption';
import { DtContainerBreakpointObserverModule } from '@dynatrace/barista-components/container-breakpoint-observer';
import { DtContextDialogModule } from '@dynatrace/barista-components/context-dialog';
import { DtCopyToClipboardModule } from '@dynatrace/barista-components/copy-to-clipboard';
+import { DtDatepickerModule } from '@dynatrace/barista-components/experimental/datepicker';
import { DtDrawerModule } from '@dynatrace/barista-components/drawer';
import { DtDrawerTableModule } from '@dynatrace/barista-components/experimental/drawer-table';
import { DtEmptyStateModule } from '@dynatrace/barista-components/empty-state';
@@ -95,6 +96,7 @@ import { DtComboboxModule } from '@dynatrace/barista-components/experimental/com
DtConfirmationDialogModule,
DtContextDialogModule,
DtCopyToClipboardModule,
+ DtDatepickerModule,
DtDrawerModule,
DtDrawerTableModule,
DtExpandablePanelModule,
diff --git a/libs/barista-components/config.bzl b/libs/barista-components/config.bzl
index b183ead056..37965916b8 100644
--- a/libs/barista-components/config.bzl
+++ b/libs/barista-components/config.bzl
@@ -22,6 +22,7 @@ COMPONENTS = [
"expandable-section",
"expandable-text",
"experimental/combobox",
+ "experimental/datepicker",
"experimental/drawer-table",
"quick-filter",
"filter-field",
diff --git a/libs/barista-components/core/index.ts b/libs/barista-components/core/index.ts
index e3e82bd027..7ed12f398c 100644
--- a/libs/barista-components/core/index.ts
+++ b/libs/barista-components/core/index.ts
@@ -24,3 +24,4 @@ export * from './src/tree/index';
export * from './src/animations/index';
export * from './src/overlay/index';
export * from './src/testing/index';
+export * from './src/date/index';
diff --git a/libs/barista-components/core/src/date/date-adapter.ts b/libs/barista-components/core/src/date/date-adapter.ts
new file mode 100644
index 0000000000..46497f446c
--- /dev/null
+++ b/libs/barista-components/core/src/date/date-adapter.ts
@@ -0,0 +1,130 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { inject, InjectionToken, LOCALE_ID } from '@angular/core';
+import { Observable, Subject } from 'rxjs';
+
+/** InjectionToken for datepicker that can be used to override default locale code. */
+export const DT_DATE_LOCALE = new InjectionToken('DT_DATE_LOCALE', {
+ providedIn: 'root',
+ factory: DT_DATE_LOCALE_FACTORY,
+});
+
+/** @docs-private */
+export function DT_DATE_LOCALE_FACTORY(): string {
+ return inject(LOCALE_ID);
+}
+
+export abstract class DtDateAdapter {
+ /** The locale to use for all dates. */
+ protected locale: any;
+
+ /** A stream that emits when the locale changes. */
+ get localeChanges(): Observable {
+ return this._localeChanges.asObservable();
+ }
+ protected _localeChanges = new Subject();
+
+ /** Sets the locale used for all dates. */
+ setLocale(locale: any): void {
+ this.locale = locale;
+ this._localeChanges.next();
+ }
+
+ /**
+ * Creates a date with the given year, month, and date.
+ * Does not allow over/under-flow of the month and date.
+ */
+ abstract createDate(year: number, month: number, date: number): D;
+
+ /** Gets today's date. */
+ abstract today(): D;
+
+ /** Gets the year component of the given date. */
+ abstract getYear(date: D): number;
+
+ /** Gets the month component of the given date. */
+ abstract getMonth(date: D): number;
+
+ /** Gets the date of the month component of the given date. */
+ abstract getDate(date: D): number;
+
+ /** Gets the day of the week component of the given date. */
+ abstract getDayOfWeek(date: D): number;
+
+ /** Gets the first day of the week. */
+ abstract getFirstDayOfWeek(): number;
+
+ /** Gets a list of names for the days of the week. */
+ abstract getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[];
+
+ /** Gets the number of days in the month of the given date. */
+ abstract getNumDaysInMonth(date: D): number;
+
+ /** Gets a list of names for the dates of the month. */
+ abstract getDateNames(): string[];
+
+ /** Checks whether the given object is considered a date instance by this DateAdapter. */
+ abstract isDateInstance(obj: any): obj is D;
+
+ /** Checks whether the given date is valid. */
+ abstract isValid(date: D): boolean;
+
+ /** Formats a date as a string according to the given format. */
+ abstract format(date: D, displayFormat: Object): string;
+
+ /**
+ * Adds the given number of years to the date. Years are counted as if flipping 12 pages on the
+ * calendar for each year and then finding the closest date in the new month. For example when
+ * adding 1 year to Feb 29, 2016, the resulting date will be Feb 28, 2017.
+ */
+ abstract addCalendarYears(date: D, years: number): D;
+
+ /**
+ * Adds the given number of months to the date. Months are counted as if flipping a page on the
+ * calendar for each month and then finding the closest date in the new month. For example when
+ * adding 1 month to Jan 31, 2017, the resulting date will be Feb 28, 2017.
+ */
+ abstract addCalendarMonths(date: D, months: number): D;
+
+ /** Adds the given number of days to the date. */
+ abstract addCalendarDays(date: D, days: number): D;
+
+ /**
+ * Compares two dates.
+ * Returns 0 if the dates are equal,
+ * a number less than 0 if the first date is earlier,
+ * a number greater than 0 if the first date is later
+ */
+ compareDate(first: D, second: D): number {
+ return (
+ this.getYear(first) - this.getYear(second) ||
+ this.getMonth(first) - this.getMonth(second) ||
+ this.getDate(first) - this.getDate(second)
+ );
+ }
+
+ /** Clamp the given date between min and max dates. */
+ clampDate(date: D, min?: D | null, max?: D | null): D {
+ if (min && this.compareDate(date, min) < 0) {
+ return min;
+ }
+ if (max && this.compareDate(date, max) > 0) {
+ return max;
+ }
+ return date;
+ }
+}
diff --git a/libs/barista-components/core/src/date/date-module.ts b/libs/barista-components/core/src/date/date-module.ts
new file mode 100644
index 0000000000..6b1ddbabaa
--- /dev/null
+++ b/libs/barista-components/core/src/date/date-module.ts
@@ -0,0 +1,24 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { NgModule } from '@angular/core';
+import { DtDateAdapter } from './date-adapter';
+import { DtNativeDateAdapter } from './native-date-adapter';
+
+@NgModule({
+ providers: [{ provide: DtDateAdapter, useClass: DtNativeDateAdapter }],
+})
+export class DtNativeDateModule {}
diff --git a/libs/barista-components/core/src/date/index.ts b/libs/barista-components/core/src/date/index.ts
new file mode 100644
index 0000000000..554460aba5
--- /dev/null
+++ b/libs/barista-components/core/src/date/index.ts
@@ -0,0 +1,19 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './date-adapter';
+export * from './native-date-adapter';
+export * from './date-module';
diff --git a/libs/barista-components/core/src/date/native-date-adapter.ts b/libs/barista-components/core/src/date/native-date-adapter.ts
new file mode 100644
index 0000000000..ee190c1821
--- /dev/null
+++ b/libs/barista-components/core/src/date/native-date-adapter.ts
@@ -0,0 +1,228 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { DtDateAdapter, DT_DATE_LOCALE } from './date-adapter';
+import { Optional, Inject, LOCALE_ID, Injectable } from '@angular/core';
+
+/** The default day of the week names to use if Intl API is not available. */
+const DEFAULT_DAY_OF_WEEK_NAMES = {
+ long: [
+ 'Sunday',
+ 'Monday',
+ 'Tuesday',
+ 'Wednesday',
+ 'Thursday',
+ 'Friday',
+ 'Saturday',
+ ],
+ short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+ narrow: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+};
+
+const DEFAULT_DATE_NAMES = fillArray(31, (i) => String(i + 1));
+
+let SUPPORTS_INTL_API: boolean;
+try {
+ SUPPORTS_INTL_API = typeof Intl != 'undefined';
+} catch {
+ SUPPORTS_INTL_API = false;
+}
+
+/**
+ * Simple version of the Angular Material's NativeDateAdapter.
+ * This class can be replaced by Angular's adapter if it is moved to the CDK.
+ */
+@Injectable()
+export class DtNativeDateAdapter extends DtDateAdapter {
+ constructor(
+ @Optional() @Inject(DT_DATE_LOCALE) dtDateLocale?: string,
+ @Optional() @Inject(LOCALE_ID) locale?: string,
+ ) {
+ super();
+ super.setLocale(dtDateLocale ?? locale ?? 'en-US');
+ }
+
+ createDate(year: number, month: number, date: number): Date {
+ if (month < 0 || month > 11) {
+ throw Error(
+ `Invalid month index "${month}". Month index has to be between 0 and 11.`,
+ );
+ }
+
+ if (date < 1) {
+ throw Error(`Invalid date "${date}". Date has to be greater than 0.`);
+ }
+
+ const result = createDateWithOverflow(year, month, date);
+
+ if (result.getMonth() != month) {
+ throw Error(`Invalid date "${date}" for month with index "${month}".`);
+ }
+
+ return result;
+ }
+
+ today(): Date {
+ return new Date();
+ }
+
+ getYear(date: Date): number {
+ return date.getFullYear();
+ }
+
+ getMonth(date: Date): number {
+ return date.getMonth();
+ }
+
+ getDate(date: Date): number {
+ return date.getDate();
+ }
+
+ getDayOfWeek(date: Date): number {
+ return date.getDay();
+ }
+
+ getFirstDayOfWeek(): number {
+ return 1; // Monday
+ }
+
+ getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[] {
+ return DEFAULT_DAY_OF_WEEK_NAMES[style];
+ }
+
+ getNumDaysInMonth(date: Date): number {
+ return this.getDate(
+ createDateWithOverflow(this.getYear(date), this.getMonth(date) + 1, 0),
+ );
+ }
+
+ getDateNames(): string[] {
+ if (SUPPORTS_INTL_API) {
+ const dtf = new Intl.DateTimeFormat(this.locale, {
+ day: 'numeric',
+ timeZone: 'utc',
+ });
+ return fillArray(31, (i) =>
+ stripDirectionalityCharacters(
+ formatDate(dtf, new Date(2017, 0, i + 1)),
+ ),
+ );
+ }
+ return DEFAULT_DATE_NAMES;
+ }
+
+ format(date: Date, displayFormat: Object): string {
+ displayFormat = { ...displayFormat, timeZone: 'utc' };
+ const dtf = new Intl.DateTimeFormat(this.locale, displayFormat);
+ return stripDirectionalityCharacters(formatDate(dtf, date));
+ }
+
+ isValid(date: Date): boolean {
+ return !isNaN(date.getTime());
+ }
+
+ isDateInstance(obj: any): obj is Date {
+ return obj instanceof Date;
+ }
+
+ addCalendarYears(date: Date, years: number): Date {
+ return this.addCalendarMonths(date, years * 12);
+ }
+
+ addCalendarMonths(date: Date, months: number): Date {
+ let newDate = createDateWithOverflow(
+ this.getYear(date),
+ this.getMonth(date) + months,
+ this.getDate(date),
+ );
+
+ // It's possible to wind up in the wrong month if the original month has more days than the new
+ // month. In this case we want to go to the last day of the desired month.
+ // Note: the additional + 12 % 12 ensures we end up with a positive number, since JS % doesn't
+ // guarantee this.
+ if (
+ this.getMonth(newDate) !=
+ (((this.getMonth(date) + months) % 12) + 12) % 12
+ ) {
+ newDate = createDateWithOverflow(
+ this.getYear(newDate),
+ this.getMonth(newDate),
+ 0,
+ );
+ }
+
+ return newDate;
+ }
+
+ addCalendarDays(date: Date, days: number): Date {
+ return createDateWithOverflow(
+ this.getYear(date),
+ this.getMonth(date),
+ this.getDate(date) + days,
+ );
+ }
+}
+
+function fillArray(length: number, fillFn: (index: number) => T): T[] {
+ return new Array(length).fill(null).map((_, i) => fillFn(i));
+}
+
+/**
+ * Strip out unicode LTR and RTL characters. Edge and IE insert these into formatted dates while
+ * other browsers do not. We remove them to make output consistent and because they interfere with
+ * date parsing.
+ */
+function stripDirectionalityCharacters(str: string): string {
+ return str.replace(/[\u200e\u200f]/g, '');
+}
+
+/**
+ * When converting Date object to string, javascript built-in functions may return wrong
+ * results because it applies its internal DST rules. The DST rules around the world change
+ * very frequently, and the current valid rule is not always valid in previous years though.
+ * We work around this problem building a new Date object which has its internal UTC
+ * representation with the local date and time.
+ */
+function formatDate(dtf: Intl.DateTimeFormat, date: Date): string {
+ const d = new Date(
+ Date.UTC(
+ date.getFullYear(),
+ date.getMonth(),
+ date.getDate(),
+ date.getHours(),
+ date.getMinutes(),
+ date.getSeconds(),
+ date.getMilliseconds(),
+ ),
+ );
+ return dtf.format(d);
+}
+
+/** Creates a date but allows the month and date to overflow. */
+function createDateWithOverflow(
+ year: number,
+ month: number,
+ date: number,
+): Date {
+ const result = new Date(year, month, date);
+
+ // We need to correct for the fact that JS native Date treats years in range [0, 99] as
+ // abbreviations for 19xx.
+ if (year >= 0 && year < 100) {
+ result.setFullYear(this.getYear(result) - 1900);
+ }
+ return result;
+}
diff --git a/libs/barista-components/core/src/overlay/index.ts b/libs/barista-components/core/src/overlay/index.ts
index 45345f97f7..ee4074c640 100644
--- a/libs/barista-components/core/src/overlay/index.ts
+++ b/libs/barista-components/core/src/overlay/index.ts
@@ -15,3 +15,4 @@
*/
export * from './flexible-connected-position-strategy';
+export * from './overlay-theming-configuration';
diff --git a/libs/barista-components/core/src/overlay/overlay-theming-configuration.ts b/libs/barista-components/core/src/overlay/overlay-theming-configuration.ts
new file mode 100644
index 0000000000..ad689f2f3c
--- /dev/null
+++ b/libs/barista-components/core/src/overlay/overlay-theming-configuration.ts
@@ -0,0 +1,44 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { coerceElement } from '@angular/cdk/coercion';
+import { ElementRef, InjectionToken } from '@angular/core';
+
+export interface DtOverlayThemingConfiguration {
+ className: string;
+}
+
+export const DT_DEFAULT_DARK_THEMING_CONFIG: DtOverlayThemingConfiguration = {
+ className: 'dt-theme-dark',
+};
+
+export const DT_OVERLAY_THEMING_CONFIG = new InjectionToken<
+ DtOverlayThemingConfiguration
+>('DT_OVERLAY_THEMING_CONFIGURATION');
+
+export function dtSetOverlayThemeAttribute(
+ overlayElement: Element,
+ componentElement: ElementRef | Element,
+ config: DtOverlayThemingConfiguration,
+): void {
+ const element = coerceElement(componentElement) as Element;
+
+ if (!element) {
+ return;
+ }
+
+ overlayElement.classList.add(config.className);
+}
diff --git a/libs/barista-components/experimental/datepicker/BUILD.bazel b/libs/barista-components/experimental/datepicker/BUILD.bazel
new file mode 100644
index 0000000000..08d9a57b69
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/BUILD.bazel
@@ -0,0 +1,118 @@
+load("@io_bazel_rules_sass//:defs.bzl", "multi_sass_binary", "sass_library")
+load("@npm//@bazel/typescript:index.bzl", "ts_config")
+load("//tools/bazel_rules:index.bzl", "jest", "ng_module_view_engine", "stylelint")
+
+package(default_visibility = ["//visibility:public"])
+
+ng_module_view_engine(
+ name = "compile",
+ srcs = glob(
+ include = ["**/*.ts"],
+ exclude = [
+ "**/*.spec.ts",
+ "src/test-setup.ts",
+ ],
+ ),
+ angular_assets = [
+ ":styles",
+ "src/datepicker.html",
+ "src/calendar.html",
+ "src/calendar-body.html",
+ "src/timepicker.html",
+ "src/timeinput.html",
+ ],
+ module_name = "@dynatrace/barista-components/experimental/datepicker",
+ tsconfig = "tsconfig_lib",
+ deps = [
+ "//libs/barista-components/core:compile",
+ "//libs/barista-components/form-field:compile",
+ "//libs/barista-components/theming:compile",
+ "//libs/barista-components/button:compile",
+ "//libs/barista-components/icon:compile",
+ "//libs/barista-components/input:compile",
+ "//libs/barista-components/overlay:compile",
+ "@npm//@angular/core",
+ "@npm//@angular/common",
+ "@npm//@angular/cdk",
+ "@npm//@angular/forms",
+ "@npm//rxjs",
+ ],
+)
+
+filegroup(
+ name = "datepicker",
+ srcs = glob(
+ include = ["**/*.ts"],
+ exclude = [
+ "**/*.spec.ts",
+ "src/test-setup.ts",
+ ],
+ ) + glob([
+ "**/*.html",
+ "**/*.scss",
+ ]),
+)
+
+sass_library(
+ name = "theme",
+ srcs = ["src/_calendar-body-theme.scss", "src/_calendar-header-theme.scss", "src/_timeinput-theme.scss"]
+)
+
+multi_sass_binary(
+ name = "styles",
+ srcs = [
+ "src/datepicker.scss",
+ "src/calendar-body.scss",
+ "src/calendar.scss",
+ "src/timepicker.scss",
+ "src/timeinput.scss",
+ ":theme"
+ ]
+)
+
+stylelint(
+ name = "stylelint",
+ srcs = glob(["**/*.scss"]),
+)
+
+jest(
+ name = "test",
+ srcs = glob(include = ["**/*.spec.ts"]),
+ jest_config = ":jest.config.json",
+ setup_file = ":src/test-setup.ts",
+ ts_config = ":tsconfig_test",
+ deps = [
+ ":compile",
+ "//libs/testing/browser",
+ "//libs/barista-components/core:compile",
+ "//libs/barista-components/form-field:compile",
+ "//libs/barista-components/theming:compile",
+ "//libs/barista-components/button:compile",
+ "//libs/barista-components/icon:compile",
+ "//libs/barista-components/input:compile",
+ "//libs/barista-components/overlay:compile",
+ "@npm//@angular/common",
+ "@npm//@angular/core",
+ "@npm//@angular/platform-browser",
+ "@npm//@angular/cdk",
+ "@npm//@angular/forms",
+ ],
+)
+
+ts_config(
+ name = "tsconfig_lib",
+ src = "tsconfig.lib.json",
+ deps = [
+ "tsconfig.json",
+ "//libs/barista-components:tsconfig",
+ ],
+)
+
+ts_config(
+ name = "tsconfig_test",
+ src = "tsconfig.spec.json",
+ deps = [
+ "tsconfig.json",
+ "//libs/barista-components:tsconfig",
+ ],
+)
diff --git a/libs/barista-components/experimental/datepicker/README.md b/libs/barista-components/experimental/datepicker/README.md
new file mode 100644
index 0000000000..a37e269f83
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/README.md
@@ -0,0 +1,44 @@
+# Datepicker (experimental)
+
+## Imports
+
+You have to import the `DtDatepickerModule` and `DtNativeDateModule` (in case
+you would like to use the native date adapter) to use the `dt-datepicker`. The
+DtNativeDateModule is based off the functionality available in JavaScript's
+native Date object, which is limited when it comes to setting the parse format.
+Therefore, if necessary, a custom DateAdapter can be implemented in order to
+handle the formatting/parsing library of your choice.
+
+```typescript
+import { NgModule } from '@angular/core';
+import { DtDatepickerModule } from '@dynatrace/barista-components/experimental/datepicker';
+import { DtNativeDateModule } from '@dynatrace/barista-components/core';
+
+@NgModule({
+ imports: [DtDatepickerModule, DtNativeDateModule],
+})
+class MyModule {}
+```
+
+## Inputs
+
+| Name | Type | Default | Description |
+| -------------- | --------- | ------- | ----------------------------------------- |
+| value | `D | null` | `null` | The selected date. |
+| startAt | `D | null` | `null` | The date to open the calendar to initially. Is ignored if `selected` is set. Defaults to today's date internally for display only. |
+| min | `D | null` | `null` | The minimum valid date. |
+| max | `D | null` | `null` | The maximum valid date. |
+| disabled | `boolean` | `false` | Whether the datepicker is disabled. |
+| isTimeEnabled | `boolean` | `false` | Whether or not the time mode is enabled. |
+| isRangeEnabled | `boolean` | `false` | Whether or not the range mode is enabled. |
+
+#### Methods
+
+The following methods are on the `DtDatepicker` class:
+
+| Name | Description | Return value |
+| ----------- | --------------------------------------- | ------------ |
+| `open` | Opens the datepicker | `void` |
+| `close` | Closes the datepicker | `void` |
+| `toggle` | Toggles the datepicker | `void` |
+| `panelOpen` | Returns the open or closed panel state. | `boolean` |
diff --git a/libs/barista-components/experimental/datepicker/barista.json b/libs/barista-components/experimental/datepicker/barista.json
new file mode 100644
index 0000000000..17f6933b19
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/barista.json
@@ -0,0 +1,24 @@
+{
+ "title": "Datepicker",
+ "description": "ToDo",
+ "postid": "datepicker",
+ "identifier": "Da",
+ "category": "components",
+ "public": true,
+ "contributors": {
+ "dev": [
+ {
+ "name": "Thomas Pink",
+ "githubuser": "thomaspink"
+ }
+ ],
+ "ux": [
+ {
+ "name": "Ursula Wieshofer",
+ "githubuser": "ursula-wieshofer"
+ }
+ ]
+ },
+ "properties": ["work in progress", "experimental"],
+ "tags": ["datepicker", "component", "angular"]
+}
diff --git a/libs/barista-components/experimental/datepicker/index.ts b/libs/barista-components/experimental/datepicker/index.ts
new file mode 100644
index 0000000000..1d7841f2e3
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/index.ts
@@ -0,0 +1,23 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './src/calendar-body';
+export * from './src/calendar';
+export * from './src/timeinput';
+export * from './src/timepicker';
+export * from './src/timepicker';
+export * from './src/datepicker';
+export * from './src/datepicker-module';
diff --git a/libs/barista-components/experimental/datepicker/jest.config.json b/libs/barista-components/experimental/datepicker/jest.config.json
new file mode 100644
index 0000000000..5227d233fe
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/jest.config.json
@@ -0,0 +1,8 @@
+{
+ "name": "datepicker",
+ "snapshotSerializers": [
+ "jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js",
+ "jest-preset-angular/build/AngularSnapshotSerializer.js",
+ "jest-preset-angular/build/HTMLCommentSerializer.js"
+ ]
+}
diff --git a/libs/barista-components/experimental/datepicker/package.json b/libs/barista-components/experimental/datepicker/package.json
new file mode 100644
index 0000000000..dedb72ce9c
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/package.json
@@ -0,0 +1,7 @@
+{
+ "ngPackage": {
+ "lib": {
+ "entryFile": "index.ts"
+ }
+ }
+}
diff --git a/libs/barista-components/experimental/datepicker/src/_calendar-body-theme.scss b/libs/barista-components/experimental/datepicker/src/_calendar-body-theme.scss
new file mode 100644
index 0000000000..74eb31c3c3
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/_calendar-body-theme.scss
@@ -0,0 +1,16 @@
+:host-context(.dt-theme-dark),
+:host(.dt-theme-dark) {
+ background-color: $gray-700;
+ color: $white;
+
+ .dt-calendar-table-cell:not(.dt-calendar-active) {
+ .dt-calendar-table-cell-inner {
+ border-color: $gray-700;
+ }
+ }
+
+ .dt-calendar-table-cell:not(.dt-calendar-selected):hover
+ .dt-calendar-table-cell-inner {
+ background: $gray-500;
+ }
+}
diff --git a/libs/barista-components/experimental/datepicker/src/_calendar-header-theme.scss b/libs/barista-components/experimental/datepicker/src/_calendar-header-theme.scss
new file mode 100644
index 0000000000..0cc7d46cc1
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/_calendar-header-theme.scss
@@ -0,0 +1,20 @@
+:host-context(.dt-theme-dark),
+:host(.dt-theme-dark) {
+ background-color: $gray-700;
+ color: $white;
+
+ .dt-button.dt-button-nested:hover:not([disabled]),
+ .dt-button.dt-button-nested:active:not([disabled]) {
+ background: $gray-500;
+ }
+
+ .dt-button-nested.dt-calendar-header-button ::ng-deep svg,
+ .dt-button-nested.dt-calendar-header-button:hover:not([disabled])
+ ::ng-deep
+ svg,
+ .dt-button-nested.dt-calendar-header-button:active:not([disabled])
+ ::ng-deep
+ svg {
+ fill: var(--dt-button-default-color);
+ }
+}
diff --git a/libs/barista-components/experimental/datepicker/src/_timeinput-theme.scss b/libs/barista-components/experimental/datepicker/src/_timeinput-theme.scss
new file mode 100644
index 0000000000..a6d08ad100
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/_timeinput-theme.scss
@@ -0,0 +1,16 @@
+:host-context(.dt-theme-dark) {
+ border: none;
+
+ .dt-timeinput-input input,
+ .dt-timeinput-input input,
+ .dt-timeinput-separator {
+ background-color: $gray-800;
+ color: $white;
+ }
+
+ .dt-timeinput-separator-disabled,
+ .dt-timeinput-input input[disabled] {
+ background-color: var(--timeinput-separator-disabled-dark);
+ color: $disabledcolor;
+ }
+}
diff --git a/libs/barista-components/experimental/datepicker/src/calendar-body.html b/libs/barista-components/experimental/datepicker/src/calendar-body.html
new file mode 100644
index 0000000000..32ee2ed58a
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/calendar-body.html
@@ -0,0 +1,53 @@
+{{ _label }}
+
+
+
+
+
+ {{ day.short }}
+
+
+
+
+
+
+
+
+ {{ cell.displayValue }}
+
+
+
+
+
diff --git a/libs/barista-components/experimental/datepicker/src/calendar-body.scss b/libs/barista-components/experimental/datepicker/src/calendar-body.scss
new file mode 100644
index 0000000000..cc61ca9938
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/calendar-body.scss
@@ -0,0 +1,54 @@
+@import '../../../core/src/style/variables';
+@import './calendar-body-theme';
+
+:host {
+ display: block;
+ outline: none;
+}
+
+.dt-calendar-table {
+ border-spacing: 0;
+ border-collapse: collapse;
+ width: 100%;
+}
+
+.dt-calendar-table-header th {
+ text-align: center;
+ padding: 0 0 8px;
+ width: 14%;
+}
+
+.dt-calendar-table-cell {
+ position: relative;
+ text-align: center;
+ outline: none;
+ cursor: pointer;
+}
+
+.dt-calendar-table-cell:not(.dt-calendar-selected):hover
+ .dt-calendar-table-cell-inner {
+ background: $gray-200;
+}
+
+.dt-calendar-table-cell-inner {
+ padding: 2px 0;
+ border: 1px solid white;
+ border-radius: 3px;
+}
+
+.dt-calendar-active .dt-calendar-table-cell-inner {
+ border-color: $disabledcolor;
+}
+
+:host:focus .dt-calendar-active .dt-calendar-table-cell-inner {
+ border-color: $turquoise-600;
+}
+
+.dt-calendar-selected .dt-calendar-table-cell-inner {
+ background-color: $turquoise-600;
+ color: white;
+}
+
+.dt-calendar-body-header-label {
+ display: none;
+}
diff --git a/libs/barista-components/experimental/datepicker/src/calendar-body.ts b/libs/barista-components/experimental/datepicker/src/calendar-body.ts
new file mode 100644
index 0000000000..120ce89ffa
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/calendar-body.ts
@@ -0,0 +1,308 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ DOWN_ARROW,
+ ENTER,
+ LEFT_ARROW,
+ PAGE_DOWN,
+ PAGE_UP,
+ RIGHT_ARROW,
+ SPACE,
+ UP_ARROW,
+} from '@angular/cdk/keycodes';
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ EventEmitter,
+ Input,
+ Output,
+ ViewEncapsulation,
+} from '@angular/core';
+import {
+ DtDateAdapter,
+ _readKeyCode,
+} from '@dynatrace/barista-components/core';
+import { getValidDateOrNull } from './datepicker-utils/util';
+
+const DAYS_PER_WEEK = 7;
+let uniqueId = 0;
+
+interface DtCalendarCell {
+ displayValue: string;
+ value: number;
+ rawValue: D;
+ ariaLabel: string;
+}
+
+@Component({
+ selector: 'dt-calendar-body',
+ templateUrl: 'calendar-body.html',
+ styleUrls: ['calendar-body.scss'],
+ host: {
+ class: 'dt-calendar-body',
+ tabIndex: '0',
+ '(keyup)': '_onHostKeyup($event)',
+ },
+ encapsulation: ViewEncapsulation.Emulated,
+ preserveWhitespaces: false,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class DtCalendarBody {
+ /**
+ * The date to display in this month view
+ * (everything other than the month and year is ignored).
+ */
+ @Input()
+ get activeDate(): D {
+ return this._activeDate;
+ }
+ set activeDate(value: D) {
+ const validDate =
+ getValidDateOrNull(this._dateAdapter, value) || this._dateAdapter.today();
+ this._activeDate = this._dateAdapter.clampDate(
+ validDate,
+ this.minDate,
+ this.maxDate,
+ );
+ this._init();
+ this._label = this._dateAdapter.format(value, {
+ year: 'numeric',
+ month: 'short',
+ });
+ this._changeDetectorRef.markForCheck();
+ }
+ private _activeDate: D;
+
+ /** The currently selected date. */
+ @Input()
+ get selected(): D | null {
+ return this._selected;
+ }
+ set selected(value: D | null) {
+ this._selected = getValidDateOrNull(this._dateAdapter, value);
+ }
+ private _selected: D | null = null;
+
+ /** The minimum selectable date. */
+ @Input()
+ get minDate(): D | null {
+ return this._minDate;
+ }
+ set minDate(value: D | null) {
+ this._minDate = getValidDateOrNull(this._dateAdapter, value);
+ }
+ private _minDate: D | null = null;
+
+ /** The maximum selectable date. */
+ @Input()
+ get maxDate(): D | null {
+ return this._maxDate;
+ }
+ set maxDate(value: D | null) {
+ this._maxDate = getValidDateOrNull(this._dateAdapter, value);
+ }
+ private _maxDate: D | null = null;
+
+ /** Function used to filter whether a date is selectable or not. */
+ @Input() dateFilter: (date: D) => boolean;
+
+ @Input('aria-labelledby') ariaLabelledby: string | null;
+
+ /** Emits when a new value is selected. */
+ @Output() readonly selectedChange = new EventEmitter();
+
+ /** Emits when any date is activated. */
+ @Output() readonly activeDateChange = new EventEmitter();
+
+ /** The names of the weekdays. */
+ _weekdays: { long: string; short: string }[];
+
+ /** Grid of calendar cells representing the dates of the month. */
+ _weeks: DtCalendarCell[][];
+
+ /** The number of blank cells to put at the beginning for the first row. */
+ _firstRowOffset: number;
+
+ /** Unique id used for the aria-label. */
+ _labelid = `dt-calendar-body-label-${uniqueId++}`;
+
+ _label = '';
+
+ constructor(
+ private _dateAdapter: DtDateAdapter,
+ private _changeDetectorRef: ChangeDetectorRef,
+ private _elementRef: ElementRef,
+ ) {
+ this._activeDate = this._dateAdapter.today();
+ }
+
+ focus(): void {
+ this._elementRef.nativeElement.focus();
+ }
+
+ /** Checks whether the provided date cell has the same value as the provided compare value. */
+ _isSame(cell: DtCalendarCell, compareValue: D): boolean {
+ return (
+ compareValue !== null &&
+ cell.rawValue !== null &&
+ this._dateAdapter.compareDate(cell.rawValue, compareValue) === 0
+ );
+ }
+
+ _cellClicked(cell: DtCalendarCell): void {
+ this._setActiveDateAndEmit(cell.rawValue);
+ this._selectActiveDate();
+ this._changeDetectorRef.markForCheck();
+ }
+
+ _onHostKeyup(event: KeyboardEvent): void {
+ const keyCode = _readKeyCode(event);
+
+ switch (keyCode) {
+ case UP_ARROW:
+ // Goto previous week
+ this._setActiveDateAndEmit(
+ this._dateAdapter.addCalendarDays(this._activeDate, -7),
+ );
+ break;
+ case DOWN_ARROW:
+ // Goto next week
+ this._setActiveDateAndEmit(
+ this._dateAdapter.addCalendarDays(this._activeDate, 7),
+ );
+ break;
+ case LEFT_ARROW:
+ // Goto previous day
+ this._setActiveDateAndEmit(
+ this._dateAdapter.addCalendarDays(this._activeDate, -1),
+ );
+ break;
+ case RIGHT_ARROW:
+ // Goto next day
+ this._setActiveDateAndEmit(
+ this._dateAdapter.addCalendarDays(this._activeDate, 1),
+ );
+ break;
+ case PAGE_UP:
+ // Goto previous month. If ALT key is pressed goto previous year instead
+ this._setActiveDateAndEmit(
+ event.altKey
+ ? this._dateAdapter.addCalendarYears(this._activeDate, -1)
+ : this._dateAdapter.addCalendarMonths(this._activeDate, -1),
+ );
+ break;
+ case PAGE_DOWN:
+ // Goto next month. If ALT key is pressed goto next year instead
+ this._setActiveDateAndEmit(
+ event.altKey
+ ? this._dateAdapter.addCalendarYears(this._activeDate, 1)
+ : this._dateAdapter.addCalendarMonths(this._activeDate, 1),
+ );
+ break;
+ case ENTER:
+ case SPACE:
+ // Select the active date
+ this._selectActiveDate();
+ break;
+ }
+
+ // Prevent unexpected default actions such as form submission.
+ event.preventDefault();
+
+ this._changeDetectorRef.markForCheck();
+ }
+
+ private _init(): void {
+ this._initWeekdays();
+ this._initWeeks();
+
+ this._changeDetectorRef.markForCheck();
+ }
+
+ private _initWeekdays(): void {
+ const firstDayOfWeek = this._dateAdapter.getFirstDayOfWeek();
+ const shortWeekdays = this._dateAdapter.getDayOfWeekNames('short');
+ const longWeekdays = this._dateAdapter.getDayOfWeekNames('long');
+
+ const weekdays = longWeekdays.map((long, i) => ({
+ long,
+ short: shortWeekdays[i],
+ }));
+ this._weekdays = weekdays
+ .slice(firstDayOfWeek)
+ .concat(weekdays.slice(0, firstDayOfWeek));
+ }
+
+ private _initWeeks(): void {
+ const daysInMonth = this._dateAdapter.getNumDaysInMonth(this.activeDate);
+ const dateNames = this._dateAdapter.getDateNames();
+ const firstOfMonth = this._dateAdapter.createDate(
+ this._dateAdapter.getYear(this.activeDate),
+ this._dateAdapter.getMonth(this.activeDate),
+ 1,
+ );
+ const firstWeekOffset =
+ (DAYS_PER_WEEK +
+ this._dateAdapter.getDayOfWeek(firstOfMonth) -
+ this._dateAdapter.getFirstDayOfWeek()) %
+ DAYS_PER_WEEK;
+
+ let weeks: DtCalendarCell[][] = [[]];
+ for (let i = 0, cell = firstWeekOffset; i < daysInMonth; i++, cell++) {
+ if (cell == DAYS_PER_WEEK) {
+ weeks.push([]);
+ cell = 0;
+ }
+ const date = this._dateAdapter.createDate(
+ this._dateAdapter.getYear(this.activeDate),
+ this._dateAdapter.getMonth(this.activeDate),
+ i + 1,
+ );
+
+ weeks[weeks.length - 1].push({
+ value: i + 1,
+ displayValue: dateNames[i],
+ rawValue: date,
+ ariaLabel: this._dateAdapter.format(date, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ }),
+ });
+ }
+ this._weeks = weeks;
+ this._firstRowOffset =
+ weeks && weeks.length && weeks[0].length
+ ? DAYS_PER_WEEK - weeks[0].length
+ : 0;
+ }
+
+ private _selectActiveDate(): void {
+ if (!this.dateFilter || this.dateFilter(this._activeDate)) {
+ this.selectedChange.emit(this._activeDate);
+ }
+ }
+
+ private _setActiveDateAndEmit(date: D): void {
+ if (this._dateAdapter.compareDate(date, this.activeDate)) {
+ this._activeDate = date;
+ this.activeDateChange.emit(this.activeDate);
+ }
+ }
+}
diff --git a/libs/barista-components/experimental/datepicker/src/calendar.html b/libs/barista-components/experimental/datepicker/src/calendar.html
new file mode 100644
index 0000000000..e13889a068
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/calendar.html
@@ -0,0 +1,57 @@
+
+
+
+
+ {{ _label }}
+
+
+
+
+
+
+
+
diff --git a/libs/barista-components/experimental/datepicker/src/calendar.scss b/libs/barista-components/experimental/datepicker/src/calendar.scss
new file mode 100644
index 0000000000..341919be05
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/calendar.scss
@@ -0,0 +1,40 @@
+@import '../../../core/src/style/colors';
+@import './calendar-header-theme';
+
+:host {
+ display: block;
+}
+
+.dt-calendar-header {
+ display: flex;
+}
+
+.dt-calendar-header-button {
+ flex-grow: 0;
+ flex-shrink: 0;
+}
+
+.dt-calendar-header-label {
+ flex-grow: 1;
+ flex-shrink: 1;
+ text-align: center;
+ padding-top: 4px;
+}
+
+// Until we have the correct icons we need to rotate the arrows
+// of the buttons individually to get the correct appearance
+.dt-calendar-header-button-prev-year ::ng-deep svg {
+ transform: rotate(90deg);
+}
+
+.dt-calendar-header-button-prev-month ::ng-deep svg {
+ transform: rotate(180deg);
+}
+
+.dt-calendar-header-button-next-year ::ng-deep svg {
+ transform: rotate(-90deg);
+}
+
+.dt-calendar-body + .dt-button {
+ margin-top: 10px;
+}
diff --git a/libs/barista-components/experimental/datepicker/src/calendar.ts b/libs/barista-components/experimental/datepicker/src/calendar.ts
new file mode 100644
index 0000000000..1f593b2d95
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/calendar.ts
@@ -0,0 +1,146 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ Component,
+ ViewEncapsulation,
+ ChangeDetectionStrategy,
+ Input,
+ ChangeDetectorRef,
+ AfterContentInit,
+ Output,
+ EventEmitter,
+ ViewChild,
+} from '@angular/core';
+import { DtDateAdapter } from '@dynatrace/barista-components/core';
+import { getValidDateOrNull } from './datepicker-utils/util';
+import { DtCalendarBody } from './calendar-body';
+
+let uniqueId = 0;
+
+@Component({
+ selector: 'dt-calendar',
+ templateUrl: 'calendar.html',
+ styleUrls: ['calendar.scss'],
+ host: {
+ class: 'dt-calendar',
+ },
+ encapsulation: ViewEncapsulation.Emulated,
+ preserveWhitespaces: false,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class DtCalendar implements AfterContentInit {
+ /** A date representing the period (month or year) to start the calendar in. */
+ @Input()
+ get startAt(): D | null {
+ return this._startAt;
+ }
+ set startAt(value: D | null) {
+ this._startAt = getValidDateOrNull(this._dateAdapter, value);
+ }
+ private _startAt: D | null = null;
+
+ /** The currently selected date. */
+ @Input()
+ get selected(): D | null {
+ return this._selected;
+ }
+ set selected(value: D | null) {
+ this._selected = getValidDateOrNull(this._dateAdapter, value);
+ }
+ private _selected: D | null = null;
+
+ /** The minimum selectable date. */
+ @Input()
+ get minDate(): D | null {
+ return this._minDate;
+ }
+ set minDate(value: D | null) {
+ this._minDate = getValidDateOrNull(this._dateAdapter, value);
+ }
+ private _minDate: D | null = null;
+
+ /** The maximum selectable date. */
+ @Input()
+ get maxDate(): D | null {
+ return this._maxDate;
+ }
+ set maxDate(value: D | null) {
+ this._maxDate = getValidDateOrNull(this._dateAdapter, value);
+ }
+ private _maxDate: D | null = null;
+
+ /** Emits when the currently selected date changes. */
+ @Output() readonly selectedChange = new EventEmitter();
+
+ get activeDate(): D {
+ return this._activeDate;
+ }
+ set activeDate(value: D) {
+ this._activeDate = this._dateAdapter.clampDate(
+ value,
+ this.minDate,
+ this.maxDate,
+ );
+ this._label = this._dateAdapter.format(value, {
+ year: 'numeric',
+ month: 'short',
+ });
+ this._changeDetectorRef.markForCheck();
+ }
+ private _activeDate: D;
+
+ _label = '';
+
+ /** Unique id used for the aria-label. */
+ _labelid = `dt-calendar-label-${uniqueId++}`;
+
+ @ViewChild(DtCalendarBody) _calendarBody: DtCalendarBody;
+
+ constructor(
+ private _dateAdapter: DtDateAdapter,
+ private _changeDetectorRef: ChangeDetectorRef,
+ ) {}
+
+ ngAfterContentInit(): void {
+ this.activeDate = this.startAt || this._dateAdapter.today();
+ }
+
+ focus(): void {
+ if (this._calendarBody) {
+ this._calendarBody.focus();
+ }
+ }
+
+ _addMonths(months: number): void {
+ this.activeDate = this._dateAdapter.addCalendarMonths(
+ this.activeDate,
+ months,
+ );
+ this._changeDetectorRef.markForCheck();
+ }
+
+ _selectedValueChanged(value: D): void {
+ this.selectedChange.emit(value);
+ }
+
+ _setTodayDate(): void {
+ this.selected = this._dateAdapter.today();
+ this.activeDate = this.selected;
+ this._selectedValueChanged(this.selected);
+ this._changeDetectorRef.markForCheck();
+ }
+}
diff --git a/libs/barista-components/experimental/datepicker/src/datepicker-module.ts b/libs/barista-components/experimental/datepicker/src/datepicker-module.ts
new file mode 100644
index 0000000000..0186536c46
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/datepicker-module.ts
@@ -0,0 +1,52 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { A11yModule } from '@angular/cdk/a11y';
+import { OverlayModule } from '@angular/cdk/overlay';
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { DtButtonModule } from '@dynatrace/barista-components/button';
+import { DtIconModule } from '@dynatrace/barista-components/icon';
+import { DtInputModule } from '@dynatrace/barista-components/input';
+import { DtCalendar } from './calendar';
+import { DtCalendarBody } from './calendar-body';
+import { DtDatePicker } from './datepicker';
+import { DtTimeInput } from './timeinput';
+import { DtTimepicker } from './timepicker';
+
+const COMPONENTS = [
+ DtDatePicker,
+ DtCalendar,
+ DtCalendarBody,
+ DtTimepicker,
+ DtTimeInput,
+];
+
+@NgModule({
+ imports: [
+ CommonModule,
+ FormsModule,
+ OverlayModule,
+ DtButtonModule,
+ DtIconModule,
+ DtInputModule,
+ A11yModule,
+ ],
+ exports: COMPONENTS,
+ declarations: COMPONENTS,
+})
+export class DtDatepickerModule {}
diff --git a/libs/barista-components/experimental/datepicker/src/datepicker-utils/util.spec.ts b/libs/barista-components/experimental/datepicker/src/datepicker-utils/util.spec.ts
new file mode 100644
index 0000000000..4745d0f3ea
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/datepicker-utils/util.spec.ts
@@ -0,0 +1,138 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ valueTo2DigitString,
+ isValidHour,
+ isValidMinute,
+ isValid,
+} from './util';
+
+describe('timeinput', () => {
+ describe('valueTo2DigitString', () => {
+ it('should cast a number value to string', () => {
+ expect(valueTo2DigitString(0)).toBe('00');
+ expect(valueTo2DigitString(15)).toBe('15');
+ expect(valueTo2DigitString(20)).toBe('20');
+ });
+ it('should prepend zeros for numbers smaller than 10', () => {
+ expect(valueTo2DigitString(8)).toBe('08');
+ expect(valueTo2DigitString(0)).toBe('00');
+ });
+ });
+
+ describe('isValidHour', () => {
+ it('should return true with an integer between 0 and 23', () => {
+ expect(isValidHour(0)).toBeTruthy();
+ expect(isValidHour(10)).toBeTruthy();
+ expect(isValidHour(12)).toBeTruthy();
+ expect(isValidHour(23)).toBeTruthy();
+ });
+ it('should return true with a string representing an integer between 0 and 23', () => {
+ expect(isValidHour('0')).toBeTruthy();
+ expect(isValidHour('00')).toBeTruthy();
+ expect(isValidHour('10')).toBeTruthy();
+ expect(isValidHour('12')).toBeTruthy();
+ expect(isValidHour('23')).toBeTruthy();
+ });
+ it('should return false with a float', () => {
+ expect(isValidHour(0.3)).toBeFalsy();
+ expect(isValidHour(5.1)).toBeFalsy();
+ expect(isValidHour(20.1)).toBeFalsy();
+ expect(isValidHour(-5.1)).toBeFalsy();
+ expect(isValidHour(-5.0)).toBeFalsy();
+ expect(isValidHour(35.0)).toBeFalsy();
+ expect(isValidHour(35.1)).toBeFalsy();
+ });
+ it('should return false with a string representing a float', () => {
+ expect(isValidHour('0.3')).toBeFalsy();
+ expect(isValidHour('-5.1')).toBeFalsy();
+ expect(isValidHour('5.0')).toBeFalsy();
+ });
+ it('should return false with an integer outside the valid range', () => {
+ expect(isValidHour(-1)).toBeFalsy();
+ expect(isValidHour(24)).toBeFalsy();
+ expect(isValidHour(25)).toBeFalsy();
+ });
+ it('should return false with a string representing an integer outside the valid range or with invalid leading zeros', () => {
+ expect(isValidHour('0000008')).toBeFalsy();
+ expect(isValidHour('005')).toBeFalsy();
+ expect(isValidHour('25')).toBeFalsy();
+ expect(isValidHour('-1')).toBeFalsy();
+ });
+ });
+
+ describe('isValidMinute', () => {
+ it('should return true with an integer between 0 and 59', () => {
+ expect(isValidMinute(0)).toBeTruthy();
+ expect(isValidMinute(10)).toBeTruthy();
+ expect(isValidMinute(12)).toBeTruthy();
+ expect(isValidMinute(23)).toBeTruthy();
+ expect(isValidMinute(59)).toBeTruthy();
+ });
+ it('should return true with a string representing an integer between 0 and 59', () => {
+ expect(isValidMinute('0')).toBeTruthy();
+ expect(isValidMinute('00')).toBeTruthy();
+ expect(isValidMinute('20')).toBeTruthy();
+ expect(isValidMinute('45')).toBeTruthy();
+ expect(isValidMinute('59')).toBeTruthy();
+ });
+ it('should return false with a float', () => {
+ expect(isValidMinute(-5.1)).toBeFalsy();
+ expect(isValidMinute(-5.0)).toBeFalsy();
+ expect(isValidMinute(0.3)).toBeFalsy();
+ expect(isValidMinute(5.1)).toBeFalsy();
+ expect(isValidMinute(20.1)).toBeFalsy();
+ expect(isValidMinute(50.1)).toBeFalsy();
+ expect(isValidMinute(65.1)).toBeFalsy();
+ });
+ it('should return false with a string representing a float', () => {
+ expect(isValidMinute('0.3')).toBeFalsy();
+ expect(isValidMinute('5.0')).toBeFalsy();
+ expect(isValidMinute('15.0')).toBeFalsy();
+ expect(isValidMinute('25.0')).toBeFalsy();
+ expect(isValidMinute('66.0')).toBeFalsy();
+ });
+ it('should return false with an integer outside the valid range', () => {
+ expect(isValidMinute(-1)).toBeFalsy();
+ expect(isValidMinute(60)).toBeFalsy();
+ expect(isValidMinute(75)).toBeFalsy();
+ });
+ it('should return false with a string representing an integer outside the valid range or with invalid leading zeros', () => {
+ expect(isValidMinute('-1')).toBeFalsy();
+ expect(isValidMinute('0000008')).toBeFalsy();
+ expect(isValidMinute('005')).toBeFalsy();
+ expect(isValidMinute('65')).toBeFalsy();
+ expect(isValidMinute('75')).toBeFalsy();
+ });
+ });
+
+ describe('isValid', () => {
+ it('should return false with an empty input', () => {
+ expect(isValid('', -Infinity, Infinity)).toBeFalsy();
+ expect(isValid(' ', -Infinity, Infinity)).toBeFalsy();
+ expect(isValid('NaN', -Infinity, Infinity)).toBeFalsy();
+ expect(isValid(null, -Infinity, Infinity)).toBeFalsy();
+ expect(isValid(undefined, -Infinity, Infinity)).toBeFalsy();
+ });
+ it('should return false with invalid inputs containing special characters - or +', () => {
+ expect(isValid('-1', -Infinity, Infinity)).toBeFalsy();
+ expect(isValid('+1', -Infinity, Infinity)).toBeFalsy();
+ expect(isValid('1+1', -Infinity, Infinity)).toBeFalsy();
+ expect(isValid('0-0', -Infinity, Infinity)).toBeFalsy();
+ });
+ });
+});
diff --git a/libs/barista-components/experimental/datepicker/src/datepicker-utils/util.ts b/libs/barista-components/experimental/datepicker/src/datepicker-utils/util.ts
new file mode 100644
index 0000000000..0752bd98d2
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/datepicker-utils/util.ts
@@ -0,0 +1,81 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ DtDateAdapter,
+ isEmpty,
+ isNumberLike,
+ isString,
+} from '@dynatrace/barista-components/core';
+
+const MAX_HOURS = 23;
+const MAX_MINUTES = 59;
+const MIN_HOURS = 0;
+const MIN_MINUTES = 0;
+const INVALID_TIME_REGEX = /[0]{3,}|[.+-]|[0]{2}[0-9]/g;
+
+/** Checks whether the provided object is a valid date an returns it; null otherwise. */
+export function getValidDateOrNull(
+ dateAdapter: DtDateAdapter,
+ obj: any,
+): D | null {
+ return dateAdapter.isDateInstance(obj) && dateAdapter.isValid(obj)
+ ? obj
+ : null;
+}
+
+/** Check is the hour value is valid. */
+export function isValidHour(value: any): boolean {
+ return isValid(value, MIN_HOURS, MAX_HOURS);
+}
+
+/** Check if the minute value is valid. */
+export function isValidMinute(value: any): boolean {
+ return isValid(value, MIN_MINUTES, MAX_MINUTES);
+}
+
+/**
+ * Check if a value if a valid hour/minute number in the range
+ * Note that if a number is passed directly in with the format 'n.0', such as 5.0, it will be truncated to 5 and validation will fail.
+ * However, this cannot happen with the input event, since it will be passed as a string. Also, typing '.' is prevented on keydown.
+ */
+export function isValid(value: any, min: number, max: number): boolean {
+ if (isEmpty(value) || !isNumberLike(value)) {
+ return false;
+ }
+
+ // the regex is necessary for invalidating chars like '-' or '.', as well as multiple leading 0s.
+ const stringifiedVal = isString(value) ? value : value.toString();
+ if (stringifiedVal.match(INVALID_TIME_REGEX)) {
+ return false;
+ }
+
+ const parsedValue = parseInt(value, 10);
+ return parsedValue >= min && parsedValue <= max;
+}
+
+/** Check if a number has at least two digits or is null. */
+export function hasMininmumTwoDigits(input: number | null): boolean {
+ return input !== null && input >= 10;
+}
+
+/**
+ * Format a number with max two digits to always display two digits
+ * (with a leading 0 in case it is a single digit or convert it to string otherwise).
+ */
+export function valueTo2DigitString(value: number): string {
+ return value < 10 ? `0${value}` : value.toString();
+}
diff --git a/libs/barista-components/experimental/datepicker/src/datepicker.html b/libs/barista-components/experimental/datepicker/src/datepicker.html
new file mode 100644
index 0000000000..72d570ec6a
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/datepicker.html
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/libs/barista-components/experimental/datepicker/src/datepicker.scss b/libs/barista-components/experimental/datepicker/src/datepicker.scss
new file mode 100644
index 0000000000..04a1a10009
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/datepicker.scss
@@ -0,0 +1,39 @@
+@import '../../../core/src/style/variables';
+
+$dt-datepicker-panel-min-width: 300px;
+$dt-datepicker-panel-max-width: 350px;
+
+:host {
+ display: block;
+}
+
+.dt-theme-dark.dt-datepicker-panel {
+ background-color: $gray-700;
+}
+
+.dt-datepicker-panel {
+ min-width: $dt-datepicker-panel-min-width;
+ max-width: $dt-datepicker-panel-max-width;
+ background: $white;
+ box-sizing: border-box;
+ border: 1px solid $disabledcolor;
+ border-radius: 3px;
+ will-change: transform;
+
+ // Prevents the content from repainting on scroll.
+ backface-visibility: hidden;
+
+ // Makes sure the opening scale animation starts from the top
+ transform-origin: left top;
+ overflow: auto;
+ -webkit-overflow-scrolling: touch; // for momentum scroll on mobile
+}
+
+.dt-datepicker-content {
+ overflow: hidden;
+ padding: 8px;
+}
+
+.dt-calendar + .dt-timepicker {
+ margin-top: 10px;
+}
diff --git a/libs/barista-components/experimental/datepicker/src/datepicker.ts b/libs/barista-components/experimental/datepicker/src/datepicker.ts
new file mode 100644
index 0000000000..f22cbbee74
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/datepicker.ts
@@ -0,0 +1,457 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ animate,
+ animateChild,
+ group,
+ query,
+ state,
+ style,
+ transition,
+ trigger,
+} from '@angular/animations';
+import { CdkConnectedOverlay } from '@angular/cdk/overlay';
+import {
+ Attribute,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ Inject,
+ Input,
+ OnDestroy,
+ Optional,
+ Self,
+ SkipSelf,
+ ViewChild,
+ ViewEncapsulation,
+} from '@angular/core';
+import {
+ ControlValueAccessor,
+ FormGroupDirective,
+ NgControl,
+ NgForm,
+} from '@angular/forms';
+import {
+ CanDisable,
+ DtDateAdapter,
+ DtOverlayThemingConfiguration,
+ dtSetOverlayThemeAttribute,
+ dtSetUiTestAttribute,
+ DtUiTestConfiguration,
+ DT_OVERLAY_THEMING_CONFIG,
+ DT_UI_TEST_CONFIG,
+ ErrorStateMatcher,
+ HasTabIndex,
+ mixinDisabled,
+ mixinErrorState,
+ mixinTabIndex,
+} from '@dynatrace/barista-components/core';
+import { DtTheme } from '@dynatrace/barista-components/theming';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { DtCalendar } from './calendar';
+import { DtTimeChangeEvent } from './timeinput';
+import { DtTimepicker } from './timepicker';
+import { getValidDateOrNull } from './datepicker-utils/util';
+
+/**
+ * This position config ensures that the top "start" corner of the overlay
+ * is aligned with with the top "start" of the origin by default (overlapping
+ * the trigger completely). If the panel cannot fit below the trigger, it
+ * will fall back to a position above the trigger.
+ */
+const OVERLAY_POSITIONS = [
+ {
+ originX: 'start',
+ originY: 'bottom',
+ overlayX: 'start',
+ overlayY: 'top',
+ },
+ {
+ originX: 'start',
+ originY: 'top',
+ overlayX: 'start',
+ overlayY: 'bottom',
+ },
+ {
+ originX: 'end',
+ originY: 'bottom',
+ overlayX: 'end',
+ overlayY: 'top',
+ offsetX: 2,
+ },
+ {
+ originX: 'end',
+ originY: 'top',
+ overlayX: 'end',
+ overlayY: 'bottom',
+ offsetX: 2,
+ },
+];
+
+let uniqueId = 0;
+
+// Boilerplate for applying mixins to DtDatePicker.
+export class DtDatepickerBase {
+ constructor(
+ public _defaultErrorStateMatcher: ErrorStateMatcher,
+ public _parentForm: NgForm,
+ public _parentFormGroup: FormGroupDirective,
+ public ngControl: NgControl,
+ ) {}
+}
+export const _DtDatepickerBase = mixinTabIndex(
+ mixinDisabled(mixinErrorState(DtDatepickerBase)),
+);
+
+@Component({
+ selector: 'dt-datepicker',
+ templateUrl: 'datepicker.html',
+ styleUrls: ['datepicker.scss'],
+ host: {
+ class: 'dt-datepicker',
+ '[class.dt-select-invalid]': 'errorState',
+ '[attr.id]': 'id',
+ '[attr.aria-invalid]': 'errorState',
+ },
+ inputs: ['disabled', 'tabIndex'],
+ encapsulation: ViewEncapsulation.Emulated,
+ preserveWhitespaces: false,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ animations: [
+ trigger('transformPanel', [
+ state(
+ 'void',
+ style({
+ transform: 'scaleY(0) translateX(-1px)',
+ opacity: 0,
+ }),
+ ),
+ state(
+ 'showing',
+ style({
+ opacity: 1,
+ transform: 'scaleY(1) translateX(-1px)',
+ }),
+ ),
+ transition(
+ 'void => *',
+ group([
+ query('@fadeInContent', animateChild()),
+ animate('150ms cubic-bezier(0.25, 0.8, 0.25, 1)'),
+ ]),
+ ),
+ transition('* => void', [
+ animate('250ms 100ms linear', style({ opacity: 0 })),
+ ]),
+ ]),
+ trigger('fadeInContent', [
+ state('showing', style({ opacity: 1 })),
+ transition('void => showing', [
+ style({ opacity: 0 }),
+ animate('150ms 100ms cubic-bezier(0.55, 0, 0.55, 0.2)'),
+ ]),
+ ]),
+ ],
+})
+export class DtDatePicker
+ extends _DtDatepickerBase
+ implements ControlValueAccessor, CanDisable, HasTabIndex, OnDestroy {
+ /** Unique id of the element. */
+ @Input()
+ get id(): string {
+ return this._id;
+ }
+ set id(value: string) {
+ this._id = value || this._uid;
+ this.stateChanges.next();
+ }
+ private _id: string;
+ private _uid = `dt-datepicker-${uniqueId++}`;
+
+ /** Value of the datepicker control. */
+ @Input()
+ get value(): D | null {
+ return this._value;
+ }
+ set value(newValue: D | null) {
+ if (newValue !== this._value) {
+ this._value = newValue;
+ this._changeDetectorRef.markForCheck();
+ }
+ }
+ private _value: D | null = null;
+
+ /** The date to open the calendar to initially. */
+ @Input()
+ get startAt(): D | null {
+ return this._startAt || this._value;
+ }
+ set startAt(value: D | null) {
+ this._startAt = getValidDateOrNull(this._dateAdapter, value);
+ }
+ private _startAt: D | null;
+
+ /** Object used to control when error messages are shown. */
+ @Input() errorStateMatcher: ErrorStateMatcher;
+
+ /** Classes to be passed to the select panel. Supports the same syntax as `ngClass`. */
+ // tslint:disable-next-line:no-any
+ @Input() panelClass: string | string[] | Set | { [key: string]: any };
+
+ /** Property that enables the timepicker, so that a time can be entered as well. */
+ @Input() isTimeEnabled: boolean;
+
+ /** Property that enables the range mode. */
+ @Input() isRangeEnabled: boolean;
+
+ /** Whether or not the overlay panel is open. */
+ get panelOpen(): boolean {
+ return this._panelOpen;
+ }
+ private _panelOpen = false;
+
+ /** Overlay pane containing the options. */
+ @ViewChild(CdkConnectedOverlay) _overlayDir: CdkConnectedOverlay;
+
+ @ViewChild(DtCalendar) _calendar: DtCalendar;
+
+ @ViewChild(DtTimepicker) _timePicker: DtTimepicker;
+
+ @ViewChild('panel') _panel: ElementRef;
+
+ /** @internal Defines the positions the overlay can take relative to the button element. */
+ _positions = OVERLAY_POSITIONS;
+
+ /** @internal Whether the panel's animation is done. */
+ _panelDoneAnimating = false;
+
+ /** @internal Hour */
+ get hour(): number {
+ return this._hour === 0 ? null : this._hour;
+ }
+
+ private _hour;
+
+ /** @internal Minute */
+ get minute(): number {
+ return this._minute === 0 ? null : this._minute;
+ }
+
+ private _minute;
+
+ /** @internal `View -> model callback called when value changes` */
+ _onChange: (value: Date) => void = () => {};
+
+ /** @internal `View -> model callback called when select has been touched` */
+ _onTouched = () => {};
+
+ /**
+ * @internal Label used for displaying the date.
+ */
+ get valueLabel(): string {
+ return this._valueLabel || 'Select date';
+ }
+ set valueLabel(value: string) {
+ this._valueLabel = value;
+ }
+ private _valueLabel = '';
+
+ /**
+ * @internal Label used for displaying the time.
+ */
+ _timeLabel = '';
+
+ private _destroy$ = new Subject();
+
+ constructor(
+ private _dateAdapter: DtDateAdapter,
+ private readonly _changeDetectorRef: ChangeDetectorRef,
+ private readonly _elementRef: ElementRef,
+ readonly defaultErrorStateMatcher: ErrorStateMatcher,
+ @Optional() readonly parentForm: NgForm,
+ @Optional() readonly parentFormGroup: FormGroupDirective,
+ @Optional() @SkipSelf() private _theme: DtTheme,
+ @Self() @Optional() readonly ngControl: NgControl,
+ @Attribute('tabindex') tabIndex: string,
+ @Inject(DT_OVERLAY_THEMING_CONFIG)
+ private readonly _themeConfig: DtOverlayThemingConfiguration,
+ @Optional()
+ @Inject(DT_UI_TEST_CONFIG)
+ private readonly _config?: DtUiTestConfiguration,
+ ) {
+ super(defaultErrorStateMatcher, parentForm, parentFormGroup, ngControl);
+
+ this.tabIndex = parseInt(tabIndex, 10) || 0;
+
+ // Force setter to be called in case id was not specified.
+ this.id = this.id;
+ }
+
+ ngOnDestroy(): void {
+ this._destroy$.next();
+ this._destroy$.complete();
+ }
+
+ /** Opens or closes the overlay panel. */
+ toggle(): void {
+ if (this.panelOpen) {
+ this.close();
+ } else {
+ this.open();
+ }
+ }
+
+ /** Opens the overlay panel. */
+ open(): void {
+ if (!this.disabled && !this._panelOpen) {
+ this._panelOpen = true;
+ this._changeDetectorRef.markForCheck();
+ }
+ }
+
+ /** Closes the overlay panel and focuses the host element. */
+ close(): void {
+ if (this._panelOpen) {
+ this._panelOpen = false;
+ this._changeDetectorRef.markForCheck();
+ }
+ }
+
+ /** Sets the datepicker's value. Part of the ControlValueAccessor. */
+ writeValue(value: D): void {
+ this.value = value;
+ }
+
+ /**
+ * Saves a callback function to be invoked when the select's value
+ * changes from user input. Part of the ControlValueAccessor.
+ */
+ registerOnChange(fn: (value: Date) => void): void {
+ this._onChange = fn;
+ }
+
+ /**
+ * Saves a callback function to be invoked when the select is blurred
+ * by the user. Part of the ControlValueAccessor.
+ */
+ registerOnTouched(fn: () => {}): void {
+ this._onTouched = fn;
+ }
+
+ /** Disables the datepicker. Part of the ControlValueAccessor. */
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ this._changeDetectorRef.markForCheck();
+ this.stateChanges.next();
+ }
+
+ /** @internal Callback that is invoked when the overlay panel has been attached. */
+ _onAttached(): void {
+ dtSetUiTestAttribute(
+ this._overlayDir.overlayRef.overlayElement,
+ this._overlayDir.overlayRef.overlayElement.id,
+ this._elementRef,
+ this._config,
+ );
+ }
+
+ /**
+ * @internal
+ * When the panel content is done fading in, the _panelDoneAnimating property is
+ * set so the proper class can be added to the panel.
+ */
+ _onFadeInDone(): void {
+ this._panelDoneAnimating = this.panelOpen;
+
+ if (this.panelOpen) {
+ this._calendar.focus();
+
+ if (this.isTimeEnabled) {
+ this._handleTimepickerValues();
+ }
+ }
+
+ this._changeDetectorRef.markForCheck();
+ }
+
+ /**
+ * @internal Handle timepicker hour and minute values.
+ */
+ _handleTimepickerValues(): void {
+ this._timePicker.timeChange
+ .pipe(takeUntil(this._destroy$))
+ .subscribe((changed) => {
+ this._handleTimeInputChange(changed);
+ });
+
+ this._timePicker._timeInput.hour = this.hour;
+ this._timePicker._timeInput.minute = this.minute;
+ }
+
+ _getTimepickerVisibility(): boolean {
+ return this.isTimeEnabled && this._panelDoneAnimating;
+ }
+
+ /**
+ * @internal Add a theming class to the overlay only when dark mode is enabled
+ */
+ _onFadeInStart(): void {
+ if (this.panelOpen && this._theme.variant === 'dark')
+ dtSetOverlayThemeAttribute(
+ this._panel.nativeElement,
+ this._elementRef.nativeElement,
+ this._themeConfig,
+ );
+ }
+
+ /**
+ * @internal Set the selected date.
+ */
+ _setSelectedValue(value: D): void {
+ this._value = value;
+ this._valueLabel = value
+ ? this._dateAdapter.format(value, {
+ year: 'numeric',
+ month: 'numeric',
+ day: 'numeric',
+ })
+ : '';
+ this._changeDetectorRef.markForCheck();
+ }
+
+ /**
+ * @internal Handle the new values when there are time changes.
+ */
+ _handleTimeInputChange(event: DtTimeChangeEvent): void {
+ if (!event) {
+ return;
+ }
+
+ this._hour = event?.hour || 0;
+ this._minute = event?.minute || 0;
+ this._timeLabel = event?.format();
+ }
+
+ /**
+ * @internal Handle the new values when thre are time changes.
+ */
+ _isTimeLabelAvailable(): boolean {
+ return this.isTimeEnabled && (this._hour !== 0 || this._minute !== 0);
+ }
+}
diff --git a/libs/barista-components/experimental/datepicker/src/test-setup.ts b/libs/barista-components/experimental/datepicker/src/test-setup.ts
new file mode 100644
index 0000000000..3c66e43d72
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/test-setup.ts
@@ -0,0 +1,17 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'jest-preset-angular';
diff --git a/libs/barista-components/experimental/datepicker/src/timeinput.html b/libs/barista-components/experimental/datepicker/src/timeinput.html
new file mode 100644
index 0000000000..19a487e4e9
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/timeinput.html
@@ -0,0 +1,50 @@
+
+
+
+
+ :
+
+
+
+
diff --git a/libs/barista-components/experimental/datepicker/src/timeinput.scss b/libs/barista-components/experimental/datepicker/src/timeinput.scss
new file mode 100644
index 0000000000..d6b2b2695d
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/timeinput.scss
@@ -0,0 +1,73 @@
+@import '../../../style/font-mixins';
+@import '../../../core/src/style/form-control';
+@import '../../../core/src/style/interactive-common';
+@import './timeinput-theme';
+
+:host {
+ display: block;
+}
+
+.dt-timeinput-wrapper {
+ display: flex;
+ width: 100%;
+ @include dt-main-font();
+ @include dt-form-control();
+ --timeinput-separator-disabled: #{$gray-130};
+ --timeinput-separator-disabled-dark: #{$gray-500};
+}
+
+.dt-timeinput-input {
+ display: block;
+ flex-grow: 1;
+ flex-shrink: 1;
+ flex-basis: 100%;
+
+ input {
+ display: block;
+ width: 100%;
+ height: 100%;
+ @include dt-main-font();
+ appearance: none;
+ outline: none;
+ border: 0;
+ border-radius: 0;
+ text-align: center;
+
+ &[disabled] {
+ color: $disabledcolor;
+ }
+
+ &:focus {
+ @include dt-no-focus-style();
+ }
+ }
+
+ input[type='number'] {
+ appearance: textfield;
+ }
+
+ input[type='number']::-webkit-inner-spin-button,
+ input[type='number']::-webkit-outer-spin-button {
+ appearance: none;
+ }
+}
+
+.dt-timeinput-input::-webkit-inner-spin-button,
+.dt-timeinput-input::-webkit-outer-spin-button {
+ appearance: none;
+ margin: 0;
+}
+
+.dt-timeinput-separator {
+ display: block;
+ text-align: center;
+ flex-grow: 0;
+ flex-shrink: 0;
+ padding: 0 4px;
+}
+
+.dt-timeinput-separator-disabled {
+ background-color: var(--timeinput-separator-disabled);
+ color: $disabledcolor;
+ pointer-events: none;
+}
diff --git a/libs/barista-components/experimental/datepicker/src/timeinput.spec.ts b/libs/barista-components/experimental/datepicker/src/timeinput.spec.ts
new file mode 100644
index 0000000000..685bca058a
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/timeinput.spec.ts
@@ -0,0 +1,293 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ NUMPAD_MINUS,
+ NUMPAD_ONE,
+ NUMPAD_PERIOD,
+ NUMPAD_PLUS,
+} from '@angular/cdk/keycodes';
+import { Component, ElementRef, ViewChild } from '@angular/core';
+import {
+ ComponentFixture,
+ fakeAsync,
+ flush,
+ TestBed,
+ tick,
+ waitForAsync,
+} from '@angular/core/testing';
+import {
+ DtDatepickerModule,
+ DtTimeInput,
+} from '@dynatrace/barista-components/experimental/datepicker';
+import {
+ createComponent,
+ dispatchFakeEvent,
+ dispatchKeyboardEvent,
+ typeInElement,
+} from '@dynatrace/testing/browser';
+
+describe('DtTimeInput', () => {
+ beforeEach(
+ waitForAsync(() => {
+ TestBed.configureTestingModule({
+ imports: [DtDatepickerModule],
+ declarations: [SimpleTimeInputTestApp],
+ });
+
+ TestBed.compileComponents();
+ }),
+ );
+
+ describe('basic behavior', () => {
+ let fixture: ComponentFixture;
+ let component: SimpleTimeInputTestApp;
+
+ beforeEach(() => {
+ fixture = createComponent(SimpleTimeInputTestApp);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ describe('disabled input property', () => {
+ it('should not be usable when disabled', fakeAsync(() => {
+ const hourEl = component.timeInput._hourInput.nativeElement;
+ component.disabled = true;
+ fixture.detectChanges();
+ tick();
+ expect(hourEl.disabled).toBeTruthy();
+ expect(hourEl.getAttribute('aria-disabled')).toBeTruthy();
+ }));
+ });
+
+ describe('timeChange event', () => {
+ it('should emit a timechange event when the hour and minute inputs are changed', fakeAsync(() => {
+ const hourEl = component.timeInput._hourInput.nativeElement;
+ const changeSpy = jest.fn();
+ component.timeInput.timeChange.subscribe(changeSpy);
+ fixture.detectChanges();
+ expect(changeSpy).not.toHaveBeenCalled();
+ component.timeInput.hour = 23;
+ component.timeInput.minute = 55;
+ fixture.detectChanges();
+ dispatchFakeEvent(hourEl, 'blur');
+ expect(changeSpy).toHaveBeenCalledTimes(1);
+ }));
+ it('should emit a timechange event when the hour and minute inputs are reset to empty', fakeAsync(() => {
+ const minuteEl = component.timeInput._minuteInput.nativeElement;
+ const changeSpy = jest.fn();
+ component.timeInput.timeChange.subscribe(changeSpy);
+ fixture.detectChanges();
+ expect(changeSpy).not.toHaveBeenCalled();
+ component.timeInput.hour = null;
+ component.timeInput.minute = null;
+ fixture.detectChanges();
+ dispatchFakeEvent(minuteEl, 'blur');
+ expect(changeSpy).toHaveBeenCalledTimes(1);
+ }));
+ });
+
+ describe('Focus switch', () => {
+ it('should switch focus from the hour to the minute input when typing in 2 digits in the hour input', fakeAsync(() => {
+ const hourEl = component.timeInput._hourInput.nativeElement;
+ const minuteEl = component.timeInput._minuteInput.nativeElement;
+
+ component.timeInput.minute = null;
+ component.timeInput.hour = 14;
+ fixture.detectChanges();
+
+ dispatchKeyboardEvent(hourEl, 'keyup', NUMPAD_ONE);
+ fixture.detectChanges();
+ tick();
+
+ expect(document.activeElement).toBe(minuteEl);
+ }));
+ it('should not switch focus from the hour to the minute input when typing in only one digit in the hour input', fakeAsync(() => {
+ const hourEl = component.timeInput._hourInput.nativeElement;
+ const minuteEl = component.timeInput._minuteInput.nativeElement;
+
+ component.timeInput.minute = null;
+ component.timeInput.hour = 1;
+ fixture.detectChanges();
+
+ dispatchKeyboardEvent(hourEl, 'keyup', NUMPAD_ONE);
+ fixture.detectChanges();
+ tick();
+
+ expect(document.activeElement).not.toBe(minuteEl);
+ }));
+ it('should not switch focus from the hour to the minute input when typing in 2 digits in the hour input, but the minute input already had a valid value', fakeAsync(() => {
+ const hourEl = component.timeInput._hourInput.nativeElement;
+ const minuteEl = component.timeInput._minuteInput.nativeElement;
+
+ component.timeInput.hour = 15;
+ fixture.detectChanges();
+
+ dispatchKeyboardEvent(hourEl, 'keyup', NUMPAD_ONE);
+ fixture.detectChanges();
+ tick();
+
+ expect(document.activeElement).not.toBe(minuteEl);
+ }));
+ });
+
+ describe('Unwanted characters', () => {
+ it("should not allow typing in the '+' character", fakeAsync(() => {
+ const hourEl = component.timeInput._hourInput.nativeElement;
+ const minuteEl = component.timeInput._minuteInput.nativeElement;
+ dispatchKeyboardEvent(hourEl, 'keydown', NUMPAD_PLUS);
+ dispatchKeyboardEvent(minuteEl, 'keydown', NUMPAD_PLUS);
+ flush();
+ fixture.detectChanges();
+ tick();
+ expect(hourEl.value).not.toBe('+');
+ expect(minuteEl.value).not.toBe('+');
+ }));
+
+ it("should not allow typing in the '-' character", fakeAsync(() => {
+ const hourEl = component.timeInput._hourInput.nativeElement;
+ const minuteEl = component.timeInput._minuteInput.nativeElement;
+ dispatchKeyboardEvent(hourEl, 'keydown', NUMPAD_MINUS);
+ dispatchKeyboardEvent(minuteEl, 'keydown', NUMPAD_MINUS);
+ flush();
+ fixture.detectChanges();
+ tick();
+ expect(hourEl.value).not.toBe('-');
+ expect(minuteEl.value).not.toBe('-');
+ }));
+
+ it("should not allow typing in the '.' character", fakeAsync(() => {
+ const hourEl = component.timeInput._hourInput.nativeElement;
+ const minuteEl = component.timeInput._minuteInput.nativeElement;
+ dispatchKeyboardEvent(hourEl, 'keydown', NUMPAD_PERIOD);
+ dispatchKeyboardEvent(minuteEl, 'keydown', NUMPAD_PERIOD);
+ flush();
+ fixture.detectChanges();
+ tick();
+ expect(hourEl.value).not.toBe('.');
+ expect(minuteEl.value).not.toBe('.');
+ }));
+
+ it('should allow typing numbers/number-like strings', fakeAsync(() => {
+ const hourEl = component.timeInput._hourInput.nativeElement;
+ const minuteEl = component.timeInput._minuteInput.nativeElement;
+ typeInElement('4', hourEl);
+ typeInElement('5', minuteEl);
+ fixture.detectChanges();
+ tick();
+ expect(hourEl.value).toBe('4');
+ expect(component.timeInput.hour).toBe(4);
+ expect(minuteEl.value).toBe('5');
+ expect(component.timeInput.minute).toBe(5);
+ }));
+ });
+
+ describe('Time validity', () => {
+ it('should fall back to the previously set value if the newly typed hour is greater than 23', fakeAsync(() => {
+ const hourEl = component.timeInput._hourInput.nativeElement;
+ typeInElement('112', hourEl);
+ fixture.detectChanges();
+ tick();
+ expect(hourEl.value).toBe('11');
+ expect(component.timeInput.hour).toBe(11);
+ }));
+ it('should be empty if there is no previously entered value and the newly entered one is an invalid hour', fakeAsync(() => {
+ component.timeInput.hour = null;
+ fixture.detectChanges();
+ const hourEl = component.timeInput._hourInput.nativeElement;
+ typeInElement('a', hourEl);
+ fixture.detectChanges();
+ tick();
+ expect(hourEl.value).toBe('');
+ expect(component.timeInput.hour).toBe(null);
+ }));
+ it('should set the new value if it is a valid hour (between 0 and 23)', fakeAsync(() => {
+ const hourEl = component.timeInput._hourInput.nativeElement;
+ typeInElement('20', hourEl);
+ fixture.detectChanges();
+ expect(hourEl.value).toBe('20');
+ expect(component.timeInput.hour).toBe(20);
+ }));
+ it('should be empty if there the new value is reset to a null or empty value (eg: user deletes the whole value)', fakeAsync(() => {
+ const hourEl = component.timeInput._hourInput.nativeElement;
+ typeInElement('', hourEl);
+ fixture.detectChanges();
+ tick();
+ expect(hourEl.value).toBe('');
+ expect(component.timeInput.hour).toBe(null);
+
+ const minuteEl = component.timeInput._minuteInput.nativeElement;
+ typeInElement('', minuteEl);
+ fixture.detectChanges();
+ tick();
+ expect(minuteEl.value).toBe('');
+ expect(component.timeInput.minute).toBe(null);
+ }));
+ it('should fall back to the previously set value if the newly typed minute is greater than 59', fakeAsync(() => {
+ const minuteEl = component.timeInput._minuteInput.nativeElement;
+ typeInElement('533', minuteEl);
+ fixture.detectChanges();
+ expect(minuteEl.value).toBe('53');
+ expect(component.timeInput.minute).toBe(53);
+ }));
+ it('should be empty if there is no previously entered value and the newly entered one is an invalid minute', fakeAsync(() => {
+ component.timeInput.minute = null;
+ fixture.detectChanges();
+ const minuteEl = component.timeInput._minuteInput.nativeElement;
+ typeInElement('a', minuteEl);
+ fixture.detectChanges();
+ tick();
+ expect(minuteEl.value).toBe('');
+ expect(component.timeInput.minute).toBe(null);
+ }));
+ it('should set the new value if it is a valid minute (between 0 and 59)', fakeAsync(() => {
+ const minuteEl = component.timeInput._minuteInput.nativeElement;
+ typeInElement('45', minuteEl);
+ component.timeInput.minute = 45;
+ fixture.detectChanges();
+ tick();
+ expect(minuteEl.value).toBe('45');
+ expect(component.timeInput.minute).toBe(45);
+ }));
+ });
+ });
+});
+
+@Component({
+ selector: 'dt-test-app',
+ template: `
+
+ `,
+})
+class SimpleTimeInputTestApp {
+ hour: number | null = 11;
+ minute: number | null = 53;
+ disabled = false;
+
+ @ViewChild(DtTimeInput) timeInput: DtTimeInput;
+
+ @ViewChild('hours', { read: ElementRef }) _hourInput: ElementRef<
+ HTMLInputElement
+ >;
+
+ @ViewChild('minutes', { read: ElementRef }) _minuteInput: ElementRef<
+ HTMLInputElement
+ >;
+}
diff --git a/libs/barista-components/experimental/datepicker/src/timeinput.ts b/libs/barista-components/experimental/datepicker/src/timeinput.ts
new file mode 100644
index 0000000000..26cbdb4be2
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/timeinput.ts
@@ -0,0 +1,189 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { FocusOrigin } from '@angular/cdk/a11y';
+import { coerceBooleanProperty } from '@angular/cdk/coercion';
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ EventEmitter,
+ Input,
+ Output,
+ ViewChild,
+ ViewEncapsulation,
+} from '@angular/core';
+import { isDefined, isEmpty } from '@dynatrace/barista-components/core';
+import {
+ hasMininmumTwoDigits,
+ isValidHour,
+ isValidMinute,
+ valueTo2DigitString,
+} from './datepicker-utils/util';
+
+export class DtTimeChangeEvent {
+ format(): string {
+ return `${valueTo2DigitString(this.hour)}
+ : ${valueTo2DigitString(this.minute)}`;
+ }
+ constructor(public hour: number, public minute: number) {}
+}
+
+@Component({
+ selector: 'dt-timeinput',
+ templateUrl: 'timeinput.html',
+ styleUrls: ['timeinput.scss'],
+ host: {
+ class: 'dt-timeinput',
+ },
+ encapsulation: ViewEncapsulation.Emulated,
+ preserveWhitespaces: false,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class DtTimeInput {
+ /** Represents the hour value in the hour input element */
+ @Input()
+ get hour(): number | null {
+ return this._hour;
+ }
+ set hour(value: number | null) {
+ if (value === this._hour) {
+ return;
+ }
+
+ this._hour = value;
+ this._changeDetectorRef.markForCheck();
+ }
+ private _hour: number | null = null;
+
+ /** Represents the minute value in the minute input element */
+ @Input()
+ get minute(): number | null {
+ return this._minute;
+ }
+ set minute(value: number | null) {
+ if (value === this._minute) {
+ return;
+ }
+
+ this._minute = value;
+ this._changeDetectorRef.markForCheck();
+ }
+ private _minute: number | null = null;
+
+ /** Binding for the disabled state. */
+ @Input()
+ get disabled(): boolean {
+ return this._isDisabled;
+ }
+ set disabled(disabled: boolean) {
+ this._isDisabled = coerceBooleanProperty(disabled);
+ this._changeDetectorRef.markForCheck();
+ }
+ private _isDisabled: boolean = false;
+
+ /** Emits when the hour or minute value changed and the focus is not on the time input elements anymore. */
+ @Output() timeChange = new EventEmitter();
+
+ /** @internal Reference of the hour input element */
+ @ViewChild('hours', { read: ElementRef }) _hourInput: ElementRef<
+ HTMLInputElement
+ >;
+
+ /** @internal Reference of the minute input element */
+ @ViewChild('minutes', { read: ElementRef }) _minuteInput: ElementRef<
+ HTMLInputElement
+ >;
+
+ constructor(private _changeDetectorRef: ChangeDetectorRef) {}
+
+ /**
+ * @internal
+ * Emits the `time change` event.
+ */
+ _emitTimeChangeEvent(): void {
+ const event = new DtTimeChangeEvent(this._hour || 0, this._minute || 0);
+ this.timeChange.emit(event);
+ }
+
+ // Add the focus switch from the hour input to the minute input when the user typed in 2 digits.
+ _onHourKeyUp(): void {
+ if (
+ hasMininmumTwoDigits(this._hour) &&
+ !hasMininmumTwoDigits(this._minute)
+ ) {
+ this._minuteInput.nativeElement.focus();
+ }
+ }
+
+ /** Called on blur and emits the timeChange event if the time inputs contain valid values. */
+ _onInputBlur(origin: FocusOrigin): void {
+ if (origin === null) {
+ this._emitTimeChangeEvent();
+ }
+ }
+
+ /**
+ * @internal Handler for the user's hour input events.
+ * NOTE: If keydown event is used to prevent adding invalid input,
+ * we cannot access the whole value, just the last typed character, hence why we use the input event on the input elements
+ */
+ _handleHourInput(event: InputEvent): void {
+ const value = (event.currentTarget as HTMLInputElement).value;
+
+ if (isValidHour(value)) {
+ this._hour = parseInt(value, 10);
+ } else {
+ // reset the value to something valid - use fallback value if it exists and the new value is not empty, otherwise reset to empty
+ if (isEmpty(value)) {
+ this._hour = null;
+ }
+
+ this._hourInput.nativeElement.value = isDefined(this._hour)
+ ? `${this._hour}`
+ : '';
+ }
+
+ this._changeDetectorRef.markForCheck();
+ }
+
+ /** @internal Handler for the user's minute input events. */
+ _handleMinuteInput(event: InputEvent): void {
+ const value = (event.currentTarget as HTMLInputElement).value;
+
+ if (isValidMinute(value)) {
+ this._minute = parseInt(value, 10);
+ } else {
+ if (isEmpty(value)) {
+ this._minute = null;
+ }
+ this._minuteInput.nativeElement.value = isDefined(this._minute)
+ ? `${this._minute}`
+ : '';
+ }
+
+ this._changeDetectorRef.markForCheck();
+ }
+
+ /**
+ * @internal Prevent typing in '.', '+, and "-", since the input value will not reflect it on the change event
+ * (the event target value does not include trailing '.' or "-" -> or it does not trigger any event in case the user types in "-" for example)
+ */
+ _handleKeydown(event: KeyboardEvent): boolean {
+ return event.key !== '.' && event.key !== '-' && event.key !== '+';
+ }
+}
diff --git a/libs/barista-components/experimental/datepicker/src/timepicker.html b/libs/barista-components/experimental/datepicker/src/timepicker.html
new file mode 100644
index 0000000000..84dee4ab94
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/timepicker.html
@@ -0,0 +1,22 @@
+
+
+
+
{{ valueLabel }}
+
+
+
+
+
+
+
+
to
+
+
+
{{ valueLabel }}
+
+
+
diff --git a/libs/barista-components/experimental/datepicker/src/timepicker.scss b/libs/barista-components/experimental/datepicker/src/timepicker.scss
new file mode 100644
index 0000000000..8347ff5581
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/timepicker.scss
@@ -0,0 +1,20 @@
+:host {
+ display: flex;
+}
+
+.dt-timepicker-part {
+ display: block;
+ width: 100%;
+ flex-grow: 1;
+ flex-shrink: 1;
+}
+
+.dt-timepicker-separator {
+ flex-grow: 0;
+ flex-shrink: 0;
+ padding: 0 16px;
+}
+
+.dt-timepicker-date-label {
+ text-align: center;
+}
diff --git a/libs/barista-components/experimental/datepicker/src/timepicker.spec.ts b/libs/barista-components/experimental/datepicker/src/timepicker.spec.ts
new file mode 100644
index 0000000000..cbdac7c010
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/timepicker.spec.ts
@@ -0,0 +1,69 @@
+import { fakeAsync } from '@angular/core/testing';
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, ViewChild } from '@angular/core';
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { createComponent } from '@dynatrace/testing/browser';
+import { DtDatepickerModule, DtTimepicker } from '..';
+
+describe('DtTimePicker', () => {
+ beforeEach(
+ waitForAsync(() => {
+ TestBed.configureTestingModule({
+ imports: [DtDatepickerModule],
+ declarations: [SimpleTimePickerTestApp],
+ });
+
+ TestBed.compileComponents();
+ }),
+ );
+
+ describe('basic behavior', () => {
+ let fixture: ComponentFixture;
+ // let component: SimpleTimePickerTestApp;
+
+ beforeEach(() => {
+ fixture = createComponent(SimpleTimePickerTestApp);
+ // component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ /**
+ * Add tests for the range mode which will be added in a later version.
+ */
+ it('dummy test', fakeAsync(() => {}));
+ });
+});
+
+@Component({
+ selector: 'dt-test-app',
+ template: `
+
+ `,
+})
+class SimpleTimePickerTestApp {
+ hour = 11;
+ minute = 53;
+ disabled = false;
+
+ @ViewChild(DtTimepicker) timePicker: DtTimepicker;
+}
diff --git a/libs/barista-components/experimental/datepicker/src/timepicker.ts b/libs/barista-components/experimental/datepicker/src/timepicker.ts
new file mode 100644
index 0000000000..c184fa1e65
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/src/timepicker.ts
@@ -0,0 +1,87 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { coerceBooleanProperty } from '@angular/cdk/coercion';
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ Input,
+ NgZone,
+ Output,
+ ViewChild,
+ ViewEncapsulation,
+} from '@angular/core';
+import { Observable } from 'rxjs';
+import { switchMap, take } from 'rxjs/operators';
+import { DtTimeChangeEvent, DtTimeInput } from './timeinput';
+
+@Component({
+ selector: 'dt-timepicker',
+ templateUrl: 'timepicker.html',
+ styleUrls: ['timepicker.scss'],
+ host: {
+ class: 'dt-timepicker',
+ },
+ encapsulation: ViewEncapsulation.Emulated,
+ preserveWhitespaces: false,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class DtTimepicker {
+ /** Label used for displaying the date in range mode. */
+ @Input()
+ valueLabel: any;
+
+ /** Contains the hour value that is depicted in the timeInput component */
+ @Input()
+ hour: number | null;
+
+ /** Contains the minute value that is depicted in the timeInput component */
+ @Input()
+ minute: number | null;
+
+ /** Property used for enabling the time range mode. */
+ @Input()
+ isTimeRangeEnabled: boolean;
+
+ /** Binding for the disabled state. */
+ @Input()
+ get disabled(): boolean {
+ return this._isDisabled;
+ }
+ set disabled(disabled: boolean) {
+ this._isDisabled = coerceBooleanProperty(disabled);
+ this._changeDetectorRef.markForCheck();
+ }
+ private _isDisabled: boolean = false;
+
+ /** Reference to the timeInput component */
+ @ViewChild(DtTimeInput) _timeInput: DtTimeInput;
+
+ /** Provides an event when the time input has changed */
+ @Output()
+ timeChange: Observable;
+
+ constructor(
+ private _zone: NgZone,
+ private _changeDetectorRef: ChangeDetectorRef,
+ ) {
+ this.timeChange = this._zone.onMicrotaskEmpty.pipe(
+ take(1),
+ switchMap(() => this._timeInput.timeChange.asObservable()),
+ );
+ }
+}
diff --git a/libs/barista-components/experimental/datepicker/tsconfig.json b/libs/barista-components/experimental/datepicker/tsconfig.json
new file mode 100644
index 0000000000..13b323d35d
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "types": ["node", "jest"]
+ },
+ "include": [],
+ "files": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ]
+}
diff --git a/libs/barista-components/experimental/datepicker/tsconfig.lib.json b/libs/barista-components/experimental/datepicker/tsconfig.lib.json
new file mode 100644
index 0000000000..82c2a167cb
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/tsconfig.lib.json
@@ -0,0 +1,13 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../../dist/out-tsc",
+ "target": "es2015",
+ "declaration": true,
+ "inlineSources": true,
+ "types": [],
+ "lib": ["dom", "es2018"]
+ },
+ "exclude": ["src/test-setup.ts", "**/*.spec.ts"],
+ "include": ["**/*.ts"]
+}
diff --git a/libs/barista-components/experimental/datepicker/tsconfig.spec.json b/libs/barista-components/experimental/datepicker/tsconfig.spec.json
new file mode 100644
index 0000000000..aed68bc6bf
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/tsconfig.spec.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../../dist/out-tsc",
+ "module": "commonjs",
+ "types": ["jest", "node"]
+ },
+ "files": ["src/test-setup.ts"],
+ "include": ["**/*.spec.ts", "**/*.d.ts"]
+}
diff --git a/libs/barista-components/experimental/datepicker/tslint.json b/libs/barista-components/experimental/datepicker/tslint.json
new file mode 100644
index 0000000000..e9dbc536a6
--- /dev/null
+++ b/libs/barista-components/experimental/datepicker/tslint.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../../../tslint.json",
+ "rules": {},
+ "linterOptions": {
+ "exclude": ["!**/*"]
+ }
+}
diff --git a/libs/barista-components/tsconfig.json b/libs/barista-components/tsconfig.json
index 3e1d7b2e48..c49752cf7d 100644
--- a/libs/barista-components/tsconfig.json
+++ b/libs/barista-components/tsconfig.json
@@ -47,6 +47,9 @@
{
"path": "./core/tsconfig.json"
},
+ {
+ "path": "./datepicker/tsconfig.json"
+ },
{
"path": "./form-field/tsconfig.json"
},
diff --git a/libs/examples/src/datepicker/datepicker-dark-example/datepicker-dark-example.html b/libs/examples/src/datepicker/datepicker-dark-example/datepicker-dark-example.html
new file mode 100644
index 0000000000..9c9158258d
--- /dev/null
+++ b/libs/examples/src/datepicker/datepicker-dark-example/datepicker-dark-example.html
@@ -0,0 +1,25 @@
+
+
Full Dark Mode Datepicker
+
+
+ Disabled
+ Enabled Time Mode
+
+
Dark Mode Calendar
+
+
+
+
Dark Mode Calendar Body Only
+
+
+
+
Dark Mode Timepicker
+
+ Disabled
+
diff --git a/libs/examples/src/datepicker/datepicker-dark-example/datepicker-dark-example.scss b/libs/examples/src/datepicker/datepicker-dark-example/datepicker-dark-example.scss
new file mode 100644
index 0000000000..2fca28ddd8
--- /dev/null
+++ b/libs/examples/src/datepicker/datepicker-dark-example/datepicker-dark-example.scss
@@ -0,0 +1,7 @@
+.dt-checkbox {
+ margin-top: 10px;
+}
+
+.dt-example-dark h2 {
+ color: white;
+}
diff --git a/libs/examples/src/datepicker/datepicker-dark-example/datepicker-dark-example.ts b/libs/examples/src/datepicker/datepicker-dark-example/datepicker-dark-example.ts
new file mode 100644
index 0000000000..b9c02a0dac
--- /dev/null
+++ b/libs/examples/src/datepicker/datepicker-dark-example/datepicker-dark-example.ts
@@ -0,0 +1,29 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'dt-example-datepicker-dark',
+ templateUrl: 'datepicker-dark-example.html',
+ styleUrls: ['datepicker-dark-example.scss'],
+})
+export class DtExampleDatepickerDark {
+ startAt = new Date(2020, 7, 31);
+ isDatepickerDisabled = false;
+ isTimepickerDisabled = false;
+ isDatepickerTimeEnabled = true;
+}
diff --git a/libs/examples/src/datepicker/datepicker-default-example/datepicker-default-example.css b/libs/examples/src/datepicker/datepicker-default-example/datepicker-default-example.css
new file mode 100644
index 0000000000..02a048fd5d
--- /dev/null
+++ b/libs/examples/src/datepicker/datepicker-default-example/datepicker-default-example.css
@@ -0,0 +1,3 @@
+.dt-checkbox {
+ margin-top: 10px;
+}
diff --git a/libs/examples/src/datepicker/datepicker-default-example/datepicker-default-example.html b/libs/examples/src/datepicker/datepicker-default-example/datepicker-default-example.html
new file mode 100644
index 0000000000..40a5f2caa2
--- /dev/null
+++ b/libs/examples/src/datepicker/datepicker-default-example/datepicker-default-example.html
@@ -0,0 +1,26 @@
+
Full datepicker
+
+
+
+Disabled
+Enabled Time Mode
+
+
Calendar
+
+
+
+
Calendar body only
+
+
+
+
Timepicker
+
+
+
+Disabled
diff --git a/libs/examples/src/datepicker/datepicker-default-example/datepicker-default-example.ts b/libs/examples/src/datepicker/datepicker-default-example/datepicker-default-example.ts
new file mode 100644
index 0000000000..5a4a20b07c
--- /dev/null
+++ b/libs/examples/src/datepicker/datepicker-default-example/datepicker-default-example.ts
@@ -0,0 +1,29 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'dt-example-datepicker-default',
+ templateUrl: 'datepicker-default-example.html',
+ styleUrls: ['datepicker-default-example.css'],
+})
+export class DtExampleDatepickerDefault {
+ startAt = new Date(2020, 7, 31);
+ isDatepickerDisabled = false;
+ isTimepickerDisabled = false;
+ isDatepickerTimeEnabled = true;
+}
diff --git a/libs/examples/src/datepicker/datepicker-examples.module.ts b/libs/examples/src/datepicker/datepicker-examples.module.ts
new file mode 100644
index 0000000000..1500653e16
--- /dev/null
+++ b/libs/examples/src/datepicker/datepicker-examples.module.ts
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { NgModule } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { DtCheckboxModule } from '@dynatrace/barista-components/checkbox';
+import { DtNativeDateModule } from '@dynatrace/barista-components/core';
+import { DtDatepickerModule } from '@dynatrace/barista-components/experimental/datepicker';
+import { DtThemingModule } from '@dynatrace/barista-components/theming';
+import {
+ DT_DEFAULT_DARK_THEMING_CONFIG,
+ DT_OVERLAY_THEMING_CONFIG,
+} from './../../../barista-components/core/src/overlay/overlay-theming-configuration';
+import { DtExampleDatepickerDark } from './datepicker-dark-example/datepicker-dark-example';
+import { DtExampleDatepickerDefault } from './datepicker-default-example/datepicker-default-example';
+
+@NgModule({
+ imports: [
+ FormsModule,
+ DtDatepickerModule,
+ DtThemingModule,
+ DtCheckboxModule,
+ DtNativeDateModule,
+ ],
+ declarations: [DtExampleDatepickerDark, DtExampleDatepickerDefault],
+ providers: [
+ {
+ provide: DT_OVERLAY_THEMING_CONFIG,
+ useValue: DT_DEFAULT_DARK_THEMING_CONFIG,
+ },
+ ],
+})
+export class DtExamplesDatepickerModule {}
diff --git a/libs/examples/src/datepicker/index.ts b/libs/examples/src/datepicker/index.ts
new file mode 100644
index 0000000000..40f78badd6
--- /dev/null
+++ b/libs/examples/src/datepicker/index.ts
@@ -0,0 +1,19 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './datepicker-dark-example/datepicker-dark-example';
+export * from './datepicker-default-example/datepicker-default-example';
+export * from './datepicker-examples.module';
diff --git a/libs/examples/src/examples.module.ts b/libs/examples/src/examples.module.ts
index 95499ef038..2503e2c69b 100644
--- a/libs/examples/src/examples.module.ts
+++ b/libs/examples/src/examples.module.ts
@@ -83,6 +83,7 @@ import { DtToastExamplesModule } from './toast/toast-examples.module';
import { DtToggleButtonGroupExamplesModule } from './toggle-button-group/toggle-button-group-examples.module';
import { DtExamplesTopBarNavigationModule } from './top-bar-navigation/top-bar-navigation-examples.module';
import { DtExamplesTreeTableModule } from './tree-table/tree-table-examples.module';
+import { DtExamplesDatepickerModule } from './datepicker/datepicker-examples.module';
/**
* NgModule that includes all example components
@@ -151,6 +152,7 @@ import { DtExamplesTreeTableModule } from './tree-table/tree-table-examples.modu
DtToggleButtonGroupExamplesModule,
DtExamplesTopBarNavigationModule,
DtExamplesTreeTableModule,
+ DtExamplesDatepickerModule,
],
})
export class DtExamplesModule {}
diff --git a/libs/examples/src/index.ts b/libs/examples/src/index.ts
index c134e67ef2..7d33ecc71c 100644
--- a/libs/examples/src/index.ts
+++ b/libs/examples/src/index.ts
@@ -333,6 +333,9 @@ import { DtExampleTreeTableProblemIndicator } from './tree-table/tree-table-prob
import { DtExampleTreeTableSimple } from './tree-table/tree-table-simple-example/tree-table-simple-example';
import { DtExampleComboboxCustomOptionHeight } from './combobox/combobox-custom-option-height-example/combobox-custom-option-height-example';
import { DtExampleFilterFieldInfiniteDataDepth } from './filter-field/filter-field-infinite-data-depth-example/filter-field-infinite-data-depth-example';
+import { DtExampleDatepickerDark } from './datepicker/datepicker-dark-example/datepicker-dark-example';
+import { DtExampleDatepickerDefault } from './datepicker/datepicker-default-example/datepicker-default-example';
+
export { DtExamplesModule } from './examples.module';
export { DtAlertExamplesModule } from './alert/alert-examples.module';
export { DtAutocompleteExamplesModule } from './autocomplete/autocomplete-examples.module';
@@ -396,6 +399,7 @@ export { DtToastExamplesModule } from './toast/toast-examples.module';
export { DtToggleButtonGroupExamplesModule } from './toggle-button-group/toggle-button-group-examples.module';
export { DtExamplesTopBarNavigationModule } from './top-bar-navigation/top-bar-navigation-examples.module';
export { DtExamplesTreeTableModule } from './tree-table/tree-table-examples.module';
+export { DtExamplesDatepickerModule } from './datepicker/datepicker-examples.module';
export {
DtExampleAlertDarkError,
DtExampleAlertDark,
@@ -705,7 +709,12 @@ export {
DtExampleTreeTableProblemIndicator,
DtExampleTreeTableSimple,
DtExampleFilterFieldInfiniteDataDepth,
+<<<<<<< HEAD
DtExampleSelectCustomValueTemplate,
+=======
+ DtExampleDatepickerDark,
+ DtExampleDatepickerDefault,
+>>>>>>> d2067af36... feat(datepicker): Added datepicker component.
};
export const EXAMPLES_MAP = new Map>([
@@ -822,6 +831,8 @@ export const EXAMPLES_MAP = new Map>([
['DtExampleDrawerNested', DtExampleDrawerNested],
['DtExampleDrawerOver', DtExampleDrawerOver],
['DtExampleDrawerTableDefault', DtExampleDrawerTableDefault],
+ ['DtExampleDatepickerDark', DtExampleDatepickerDark],
+ ['DtExampleDatepickerDefault', DtExampleDatepickerDefault],
['DtExampleCustomEmptyStateTable', DtExampleCustomEmptyStateTable],
['DtExampleCustomEmptyState', DtExampleCustomEmptyState],
['DtExampleEmptyStateDefault', DtExampleEmptyStateDefault],
diff --git a/libs/examples/src/inline-editor/inline-editor-examples.module.ts b/libs/examples/src/inline-editor/inline-editor-examples.module.ts
index aba18d9df8..f69e02334a 100644
--- a/libs/examples/src/inline-editor/inline-editor-examples.module.ts
+++ b/libs/examples/src/inline-editor/inline-editor-examples.module.ts
@@ -36,7 +36,6 @@ import { DtExampleInlineEditorRequired } from './inline-editor-required-example/
DtExampleInlineEditorApi,
DtExampleInlineEditorDefault,
DtExampleInlineEditorFailing,
- DtExampleInlineEditorFailing,
DtExampleInlineEditorRequired,
DtExampleInlineEditorSuccessful,
DtExampleInlineEditorValidation,
diff --git a/nx.json b/nx.json
index d64d30d913..87e001a604 100644
--- a/nx.json
+++ b/nx.json
@@ -75,6 +75,7 @@
"context-dialog",
"copy-to-clipboard",
"core",
+ "datepicker",
"drawer",
"drawer-table",
"empty-state",
@@ -181,6 +182,9 @@
"core": {
"tags": ["scope:components", "type:library"]
},
+ "datepicker": {
+ "tags": ["scope:components", "type:library"]
+ },
"drawer": {
"tags": ["scope:components", "type:library"]
},
diff --git a/tsconfig.base.json b/tsconfig.base.json
index f51d6ac6e0..ea69cb4204 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -68,6 +68,9 @@
"@dynatrace/barista-components/core": [
"libs/barista-components/core/index.ts"
],
+ "@dynatrace/barista-components/experimental/datepicker": [
+ "libs/barista-components/experimental/datepicker/index.ts"
+ ],
"@dynatrace/barista-components/drawer": [
"libs/barista-components/drawer/index.ts"
],