diff --git a/apps/dev/src/datepicker/datepicker-demo.component.html b/apps/dev/src/datepicker/datepicker-demo.component.html index 78b71cf9c7..502fbc13da 100644 --- a/apps/dev/src/datepicker/datepicker-demo.component.html +++ b/apps/dev/src/datepicker/datepicker-demo.component.html @@ -1,5 +1,26 @@

Datepicker

+

Full datepicker

+ + + +Disabled +Enabled Time Mode + +

Calendar

+ + + +

Calendar body only

+ + +

Timepicker

@@ -7,6 +28,26 @@

Timepicker

Disabled
+

Full Dark Mode Datepicker

+ + + Disabled + Enabled Time Mode + +

Dark Mode Calendar

+ + + +

Dark Mode Calendar Body Only

+ + +

Dark Mode Timepicker

{{ _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 index a833e91701..0186536c46 100644 --- a/libs/barista-components/experimental/datepicker/src/datepicker-module.ts +++ b/libs/barista-components/experimental/datepicker/src/datepicker-module.ts @@ -22,10 +22,19 @@ 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 = [DtTimepicker, DtTimeInput]; +const COMPONENTS = [ + DtDatePicker, + DtCalendar, + DtCalendarBody, + DtTimepicker, + DtTimeInput, +]; @NgModule({ imports: [ 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/examples/src/datepicker/datepicker-dark-example/datepicker-dark-example.html b/libs/examples/src/datepicker/datepicker-dark-example/datepicker-dark-example.html index 6bf2fa908f..9c9158258d 100644 --- a/libs/examples/src/datepicker/datepicker-dark-example/datepicker-dark-example.html +++ b/libs/examples/src/datepicker/datepicker-dark-example/datepicker-dark-example.html @@ -1,4 +1,24 @@
+

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.ts b/libs/examples/src/datepicker/datepicker-dark-example/datepicker-dark-example.ts index 840db3c5a2..b9c02a0dac 100644 --- a/libs/examples/src/datepicker/datepicker-dark-example/datepicker-dark-example.ts +++ b/libs/examples/src/datepicker/datepicker-dark-example/datepicker-dark-example.ts @@ -22,5 +22,8 @@ import { Component } from '@angular/core'; 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 index eb7d0d8601..40a5f2caa2 100644 --- a/libs/examples/src/datepicker/datepicker-default-example/datepicker-default-example.html +++ b/libs/examples/src/datepicker/datepicker-default-example/datepicker-default-example.html @@ -1,3 +1,24 @@ +

Full datepicker

+ + + +Disabled +Enabled Time Mode + +

Calendar

+ + + +

Calendar body only

+ + +

Timepicker

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 index a69a9566a0..5a4a20b07c 100644 --- a/libs/examples/src/datepicker/datepicker-default-example/datepicker-default-example.ts +++ b/libs/examples/src/datepicker/datepicker-default-example/datepicker-default-example.ts @@ -19,8 +19,11 @@ import { Component } from '@angular/core'; @Component({ selector: 'dt-example-datepicker-default', templateUrl: 'datepicker-default-example.html', - styleUrls: ['datepicker-default-example.scss'], + styleUrls: ['datepicker-default-example.css'], }) export class DtExampleDatepickerDefault { + startAt = new Date(2020, 7, 31); + isDatepickerDisabled = false; isTimepickerDisabled = false; + isDatepickerTimeEnabled = true; }