diff --git a/sass/components/_datepicker.scss b/sass/components/_datepicker.scss index 0af94e5ba8..ad3d9b6245 100644 --- a/sass/components/_datepicker.scss +++ b/sass/components/_datepicker.scss @@ -1,18 +1,11 @@ -/* Modal */ -// @removed since v2.2.1 -/*.datepicker-modal { - max-width: 325px; - // @removed since v2.2.1-dev regarding Material M3 standards - min-width: 300px; - max-height: none; -}*/ +@use './mixins.module.scss' as *; .datepicker-container { display: flex; flex-direction: column; max-width: 325px; padding: 0; - background-color: var(--md-sys-color-surface); + background-color: var(--md-sys-color-inverse-on-surface); } .datepicker-controls { @@ -34,8 +27,8 @@ text-align: center; &:focus { - border-bottom: none; - background-color: var(--md-sys-color-primary-container); + color: var(--md-sys-color-primary); + background-color: color-mix(in srgb, transparent, var(--md-sys-color-primary) 20%); } &::selection { @@ -70,6 +63,12 @@ .month-next { display: inline-flex; align-items: center; + + @include btn($height: 49px); + + &:focus { + background-color: color-mix(in srgb, transparent, var(--md-sys-color-primary) 20%); + } } .month-prev > svg, @@ -261,3 +260,9 @@ color: var(--md-sys-color-error); } +/* Display modes */ +.datepicker-modal { + max-width: calc(325px + var(--modal-padding)*2); + max-height: none; + background-color: var(--md-sys-color-inverse-on-surface); +} diff --git a/sass/components/_global.scss b/sass/components/_global.scss index 96358b626b..5e2b761180 100644 --- a/sass/components/_global.scss +++ b/sass/components/_global.scss @@ -467,3 +467,6 @@ $spacing-values: ("0": 0, "1": 0.25rem, "2": 0.5rem, "3": 0.75rem, "4": 1rem, "5 visibility: hidden; } +.confirmation-btns { + margin-left: auto; +} diff --git a/sass/components/_modal.scss b/sass/components/_modal.scss index 440f5543a4..99b93143a5 100644 --- a/sass/components/_modal.scss +++ b/sass/components/_modal.scss @@ -40,19 +40,23 @@ flex-shrink: 0; position: sticky; top: 0; - background-color: var(--modal-background-color); + // disabled since background color inheritance from parent element + // background-color: var(--modal-background-color); } .modal-content { padding: 0 var(--modal-padding); } .modal-footer { + display: flex; border-radius: 0 0 var(--modal-border-radius) var(--modal-border-radius); padding: var(--modal-padding); text-align: right; flex-shrink: 0; position: sticky; bottom: 0; - background-color: var(--modal-background-color); + // disabled since background color inheritance from parent element + // background-color: var(--modal-background-color); + justify-content: space-between; } .modal-close { diff --git a/sass/components/_timepicker.scss b/sass/components/_timepicker.scss index 6fdde9911b..fba813580e 100644 --- a/sass/components/_timepicker.scss +++ b/sass/components/_timepicker.scss @@ -17,7 +17,7 @@ width: auto; flex: 1 auto; // background-color: var(--md-sys-color-surface); - padding: 2rem .67rem .67rem .67rem; + padding: 2rem .71rem .67rem .71rem; font-weight: 300; } @@ -51,7 +51,7 @@ .timepicker-input-hours-wrapper, .timepicker-input-minutes-wrapper { - width: 6.9rem; + width: 6.85rem; height: 5.75rem; } @@ -231,6 +231,38 @@ input[type=text].timepicker-input-minutes { padding: 0 20px; } +/* Display modes */ +.timepicker-modal { + max-width: 326px; + max-height: none; + background-color: var(--md-sys-color-inverse-on-surface); + overflow-x: hidden; + + // Reset margins and paddings since they are defined by the modal instance + .timepicker-container { + margin-left: calc(-1 * var(--modal-padding)); + margin-right: calc(-1 * var(--modal-padding)); + } + + .modal-header + .modal-content { + .timepicker-digital-display { + padding-top: 0; + } + + .timepicker-text-container { + padding-top: 4px; + } + } + + .timepicker-analog-display { + padding-bottom: 0; + } + + .timepicker-plate { + margin-bottom: 0; + } +} + /* Media Queries */ @media #{$large-and-up} { .timepicker-container { @@ -265,6 +297,11 @@ input[type=text].timepicker-input-minutes { display: flex; flex-grow: 1; max-width: unset; + /*width: 100%; + padding: 0 4%; + flex-wrap: wrap; + flex-direction: row; + display: flex;*/ } .timepicker-container .am-btn, @@ -291,4 +328,35 @@ input[type=text].timepicker-input-minutes { .timepicker-plate { margin-top: 1.6rem; } + + /* Display modes */ + .timepicker-modal { + width: 65%; + max-width: 605px; + + .modal-header + .modal-content { + .timepicker-digital-display, + .timepicker-analog-display { + padding-top: 0; + } + + .timepicker-text-container { + margin-top: 2.4rem; + } + + .timepicker-plate { + margin-top: 4px; + margin-bottom: 0; + } + } + + .timepicker-digital-display, + .timepicker-analog-display { + padding-bottom: 0; + } + + .timepicker-plate { + margin-bottom: 0; + } + } } diff --git a/src/datepicker.ts b/src/datepicker.ts index f7ccd3b219..3e191580f4 100644 --- a/src/datepicker.ts +++ b/src/datepicker.ts @@ -2,6 +2,7 @@ import { Utils } from './utils'; import { FormSelect } from './select'; import { BaseOptions, Component, I18nOptions, InitElements, MElement } from './component'; import { DockedDisplayPlugin } from './plugin/dockedDisplayPlugin'; +import { ModalDisplayPlugin } from './plugin/modalDisplayPlugin'; export interface DateI18nOptions extends I18nOptions { previousMonth: string; @@ -321,7 +322,7 @@ export class Datepicker extends Component { calendars: [{ month: number; year: number }]; private _y: number; private _m: number; - private displayPlugin: DockedDisplayPlugin; + private displayPlugin: DockedDisplayPlugin | ModalDisplayPlugin; private footer: HTMLElement; static _template: string; @@ -348,41 +349,8 @@ export class Datepicker extends Component { this._setupVariables(); this._insertHTMLIntoDOM(); this._setupEventHandlers(); - - if (!this.options.defaultDate) { - this.options.defaultDate = new Date(Date.parse(this.el.value)); - } - - const defDate = this.options.defaultDate; - if (Datepicker._isDate(defDate)) { - if (this.options.setDefaultDate) { - this.setDate(defDate, true); - this.setInputValue(this.el, defDate); - } else { - this.gotoDate(defDate); - } - } else { - this.gotoDate(new Date()); - } - if (this.options.isDateRange) { - this.multiple = true; - const defEndDate = this.options.defaultEndDate; - if (Datepicker._isDate(defEndDate)) { - if (this.options.setDefaultEndDate) { - this.setDate(defEndDate, true, true); - this.setInputValue(this.endDateEl, defEndDate); - } - } - } - if (this.options.isMultipleSelection) { - this.multiple = true; - this.dates = []; - this.dateEls = []; - this.dateEls.push(el); - } - if (this.options.displayPlugin) { - if (this.options.displayPlugin === 'docked') this.displayPlugin = DockedDisplayPlugin.init(this.el, this.containerEl, this.options.displayPluginOptions); - } + if (this.options.displayPlugin) this._setupDisplayPlugin(); + this._pickerSetup(); } static get defaults() { @@ -503,27 +471,13 @@ export class Datepicker extends Component { } } - /*if (this.options.showClearBtn) { - this.clearBtn.style.visibility = ''; - this.clearBtn.innerText = this.options.i18n.clear; - } - this.doneBtn.innerText = this.options.i18n.done; - this.cancelBtn.innerText = this.options.i18n.cancel;*/ - Utils.createButton(this.footer, this.options.i18n.clear, ['datepicker-clear'], this.options.showClearBtn, this._handleClearClick); - - if (!this.options.autoSubmit) { - Utils.createConfirmationContainer(this.footer, this.options.i18n.done, this.options.i18n.cancel, this._confirm, this._cancel); - } - if (this.options.container) { const optEl = this.options.container; this.options.container = optEl instanceof HTMLElement ? optEl : (document.querySelector(optEl) as HTMLElement); this.options.container.append(this.containerEl); } else { - //this.containerEl.before(this.el); const appendTo = !this.endDateEl ? this.el : this.endDateEl; - if (!this.options.openByDefault) (this.containerEl as HTMLElement).setAttribute('style', 'display: none; visibility: hidden;'); appendTo.parentElement.after(this.containerEl); } } @@ -700,6 +654,21 @@ export class Datepicker extends Component { ); } + /** + * Display plugin setup. + */ + _setupDisplayPlugin() { + if (this.options.displayPlugin === 'docked') this.displayPlugin = DockedDisplayPlugin.init(this.el, this.containerEl, this.options.displayPluginOptions); + if (this.options.displayPlugin === 'modal') { + this.displayPlugin = ModalDisplayPlugin.init(this.el, this.containerEl, { + ...this.options.displayPluginOptions, + ...{ classList: ['datepicker-modal'] } + }); + this.footer.remove(); + this.footer = this.displayPlugin.footer; + } + } + /** * Renders the date in the modal head section. */ @@ -1186,6 +1155,56 @@ export class Datepicker extends Component { }; } + _pickerSetup() { + if (!this.options.defaultDate) { + this.options.defaultDate = new Date(Date.parse(this.el.value)); + } + + const defDate = this.options.defaultDate; + if (Datepicker._isDate(defDate)) { + if (this.options.setDefaultDate) { + this.setDate(defDate, true); + this.setInputValue(this.el, defDate); + } else { + this.gotoDate(defDate); + } + } else { + this.gotoDate(new Date()); + } + + if (this.options.isDateRange) { + this.multiple = true; + const defEndDate = this.options.defaultEndDate; + if (Datepicker._isDate(defEndDate)) { + if (this.options.setDefaultEndDate) { + this.setDate(defEndDate, true, true); + this.setInputValue(this.endDateEl, defEndDate); + } + } + } + + if (this.options.isMultipleSelection) { + this.multiple = true; + this.dates = []; + this.dateEls = []; + this.dateEls.push(this.el); + } + + if (this.options.showClearBtn) { + Utils.createButton( + this.footer, + this.options.i18n.clear, + ['datepicker-clear'], + true, + this._handleClearClick + ); + } + + if (!this.options.autoSubmit) { + Utils.createConfirmationContainer(this.footer, this.options.i18n.done, this.options.i18n.cancel, this._confirm, this._cancel); + } + } + _removeEventHandlers() { this.el.removeEventListener('click', this._handleInputClick); this.el.removeEventListener('keydown', this._handleInputKeydown); @@ -1352,10 +1371,12 @@ export class Datepicker extends Component { _confirm = () => { this._finishSelection(); + if (this.displayPlugin) this.displayPlugin.hide(); if (typeof this.options.onConfirm === 'function') this.options.onConfirm.call(this); } _cancel = () => { + if (this.displayPlugin) this.displayPlugin.hide(); if (typeof this.options.onCancel === 'function') this.options.onCancel.call(this); }; diff --git a/src/plugin/modalDisplayPlugin.ts b/src/plugin/modalDisplayPlugin.ts new file mode 100644 index 0000000000..fac12ecc70 --- /dev/null +++ b/src/plugin/modalDisplayPlugin.ts @@ -0,0 +1,80 @@ +export interface ModalDisplayPluginOptions { + /** + * Classes to add on modal container. + */ + classList: string[], + /** + * Title element. + */ + title: HTMLElement|null +} + +const _defaults: ModalDisplayPluginOptions = { + classList: ['modal'], + title: null +} + +export class ModalDisplayPlugin { + private readonly el: HTMLElement; + private readonly container: HTMLDialogElement; + private options: Partial; + private visible: boolean; + footer: HTMLElement; + + constructor(el: HTMLElement, container: HTMLElement, options: Partial) { + this.el = el; + this.options = { + ..._defaults, + ...options, + }; + + this.container = document.createElement('dialog'); + this.container.classList.add('modal', 'display-modal', this.options.classList.join(' ')); + + if(options.title) { + const modalHeader = document.createElement('div'); + modalHeader.classList.add('modal-header'); + modalHeader.append(options.title); + this.container.append(modalHeader); + } + + const modalContent = document.createElement('div'); + modalContent.classList.add('modal-content'); + modalContent.append(container); + this.container.append(modalContent); + + this.footer = document.createElement('div'); + this.footer.classList.add('modal-footer'); + this.container.append(this.footer); + + document.body.append(this.container); + + document.addEventListener('click', (e) => { + if (this.visible && !(this.el === e.target) && !((e.target).closest('.display-modal'))) { + this.hide(); + } + }); + } + + /** + * Initializes instance of ModalDisplayPlugin + * @param el HTMLElement to position to + * @param container HTMLElement to be positioned + * @param options Plugin options + */ + static init(el: HTMLElement, container: HTMLElement, options?: Partial): ModalDisplayPlugin { + return new ModalDisplayPlugin(el, container, options); + } + + show = () => { + if (this.visible) return; + this.visible = true; + this.container.setAttribute('open', 'true'); + }; + + hide = () => { + if (!this.visible) return; + this.visible = false; + this.container.removeAttribute('open'); + }; +} diff --git a/src/timepicker.ts b/src/timepicker.ts index f3ef2dc04b..360b70ed82 100644 --- a/src/timepicker.ts +++ b/src/timepicker.ts @@ -1,6 +1,7 @@ import { Utils } from './utils'; import { Component, BaseOptions, InitElements, MElement, I18nOptions } from './component'; import { DockedDisplayPlugin } from './plugin/dockedDisplayPlugin'; +import { ModalDisplayPlugin } from './plugin/modalDisplayPlugin'; export type Views = 'hours' | 'minutes'; @@ -45,6 +46,7 @@ export interface TimepickerOptions extends BaseOptions { * Autosubmit timepicker selection to input field * @default true */ + // @todo this is only working on analog clock, should apply to hour/minute input fields and am/pm selector as well autoSubmit: true; /** * Default time to set on the timepicker 'now' or '13:14'. @@ -176,7 +178,7 @@ export class Timepicker extends Component { g: Element; toggleViewTimer: string | number | NodeJS.Timeout; vibrateTimer: NodeJS.Timeout | number; - private displayPlugin: DockedDisplayPlugin; + private displayPlugin: DockedDisplayPlugin | ModalDisplayPlugin; constructor(el: HTMLInputElement, options: Partial) { super(el, options, Timepicker); @@ -190,11 +192,10 @@ export class Timepicker extends Component { this._setupVariables(); this._setupEventHandlers(); this._clockSetup(); - this._pickerSetup(); - if (this.options.displayPlugin) { - if (this.options.displayPlugin === 'docked') this.displayPlugin = DockedDisplayPlugin.init(this.el, this.containerEl, this.options.displayPluginOptions); + this._setupDisplayPlugin(); } + this._pickerSetup(); } static get defaults(): TimepickerOptions { @@ -260,6 +261,7 @@ export class Timepicker extends Component { _setupEventHandlers() { this.el.addEventListener('click', this._handleInputClick); + // @todo allow input field to fill values from input field when container/modal opens this.el.addEventListener('keydown', this._handleInputKeydown); this.plate.addEventListener('mousedown', this._handleClockClickStart); this.plate.addEventListener('touchstart', this._handleClockClickStart); @@ -408,13 +410,15 @@ export class Timepicker extends Component { // clearButton.classList.add('timepicker-clear'); // clearButton.addEventListener('click', this.clear); // this.footer.appendChild(clearButton); - Utils.createButton( - this.footer, - this.options.i18n.clear, - ['timepicker-clear'], - this.options.showClearBtn, - this.clear - ); + if (this.options.showClearBtn) { + Utils.createButton( + this.footer, + this.options.i18n.clear, + ['timepicker-clear'], + true, + this.clear + ); + } if (!this.options.autoSubmit) { /*const confirmationBtnsContainer = document.createElement('div'); @@ -443,6 +447,18 @@ export class Timepicker extends Component { this.showView('hours'); } + private _setupDisplayPlugin() { + if (this.options.displayPlugin === 'docked') this.displayPlugin = DockedDisplayPlugin.init(this.el, this.containerEl, this.options.displayPluginOptions); + if (this.options.displayPlugin === 'modal') { + this.displayPlugin = ModalDisplayPlugin.init(this.el, this.containerEl, { + ...this.options.displayPluginOptions, + ...{ classList: ['timepicker-modal'] } + }); + this.footer.remove(); + this.footer = this.displayPlugin.footer; + } + } + _clockSetup() { if (this.options.twelveHour) { // AM Button @@ -820,16 +836,19 @@ export class Timepicker extends Component { confirm = () => { this.done(); + if (this.displayPlugin) this.displayPlugin.hide(); if (typeof this.options.onDone === 'function') this.options.onDone.call(this); } cancel = () => { // not logical clearing the input field on cancel, since the end user might want to make use of the previously submitted value // this.clear(); + if (this.displayPlugin) this.displayPlugin.hide(); if (typeof this.options.onCancel === 'function') this.options.onCancel.call(this); } clear = () => { + // @todo should clear timepicker hour/minute input elems and reset analog clock, currently clears input el this.done(true); };