diff --git a/components/datepicker/_datepicker.scss b/components/datepicker/_datepicker.scss index 0af94e5ba8..ad3d9b6245 100644 --- a/components/datepicker/_datepicker.scss +++ b/components/datepicker/_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/components/datepicker/datepicker.ts b/components/datepicker/datepicker.ts index 85f75e8406..939e8cd709 100644 --- a/components/datepicker/datepicker.ts +++ b/components/datepicker/datepicker.ts @@ -2,6 +2,7 @@ import { Utils } from '../../src/utils'; import { BaseOptions, Component, I18nOptions, InitElements, MElement } from '../../src/component'; import { FormSelect } from '../text-fields/select'; import { DockedDisplayPlugin } from '../../src/dockedDisplayPlugin'; +import { ModalDisplayPlugin } from '../../src/modalDisplayPlugin'; export interface DateI18nOptions extends I18nOptions { previousMonth: string; @@ -293,7 +294,7 @@ const _defaults: DatepickerOptions = { displayPlugin: null, displayPluginOptions: null, onConfirm: null, - onCancel: null + onCancel: null, }; export class Datepicker extends Component { @@ -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,46 +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() { @@ -510,12 +473,6 @@ 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, @@ -540,13 +497,7 @@ export class Datepicker extends Component { 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); } } @@ -723,6 +674,38 @@ export class Datepicker extends Component { ); } + /** + * Display plugin setup. + */ + _setupDisplayPlugin() { + const displayPluginOptions = { + ...this.options.displayPluginOptions, + ...{ + onOpen: () => { + document.querySelectorAll('.select-dropdown').forEach((e: HTMLInputElement) => { + e.tabIndex = 0; + }); + }, + onClose: () => { + document.querySelectorAll('.select-dropdown').forEach((e: HTMLInputElement) => { + e.tabIndex = -1; + }); + } + } + } + + if (this.options.displayPlugin === 'docked') this.displayPlugin = DockedDisplayPlugin.init(this.el, this.containerEl, displayPluginOptions); + if (this.options.displayPlugin === 'modal') { + this.displayPlugin = ModalDisplayPlugin.init(this.el, this.containerEl, { + ...displayPluginOptions, + ...{ classList: ['datepicker-modal'] } + }); + this.footer.remove(); + this.footer = this.displayPlugin.footer; + } + if (this.options.openByDefault) this.displayPlugin.show(); + } + /** * Renders the date in the modal head section. */ @@ -1163,12 +1146,6 @@ export class Datepicker extends Component { this.el.addEventListener('keydown', this._handleInputKeydown); this.el.addEventListener('change', this._handleInputChange); this.calendarEl.addEventListener('click', this._handleCalendarClick); - /* this.doneBtn.addEventListener('click', this._confirm); - this.cancelBtn.addEventListener('click', this._cancel); - - if (this.options.showClearBtn) { - this.clearBtn.addEventListener('click', this._handleClearClick); - }*/ } _setupVariables() { @@ -1180,11 +1157,6 @@ export class Datepicker extends Component { this.calendarEl = this.containerEl.querySelector('.datepicker-calendar'); this.yearTextEl = this.containerEl.querySelector('.year-text'); this.dateTextEl = this.containerEl.querySelector('.date-text'); - /* if (this.options.showClearBtn) { - this.clearBtn = this.containerEl.querySelector('.datepicker-clear'); - } - this.doneBtn = this.containerEl.querySelector('.datepicker-done'); - this.cancelBtn = this.containerEl.querySelector('.datepicker-cancel');*/ this.footer = this.containerEl.querySelector('.datepicker-footer'); this.formats = { @@ -1223,6 +1195,42 @@ 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); + } + } + _removeEventHandlers() { this.el.removeEventListener('click', this._handleInputClick); this.el.removeEventListener('keydown', this._handleInputKeydown); @@ -1276,10 +1284,15 @@ export class Datepicker extends Component { } if (this.options.isDateRange) { + const confirmAfterSelection = Datepicker._isDate(this.date) && this.options.autoSubmit; this._handleDateRangeCalendarClick(selectedDate); + + if(confirmAfterSelection) { + this._confirm(); + } } - if (this.options.autoSubmit) this._finishSelection(); + if (!this.options.isDateRange && this.options.autoSubmit) this._confirm(); } else if (target.closest('.month-prev')) { this.prevMonth(); } else if (target.closest('.month-next')) { @@ -1294,7 +1307,7 @@ export class Datepicker extends Component { return; } - this.setDate(date, false, Datepicker._isDate(this.date)); + this.setDate(date, true, Datepicker._isDate(this.date)); return; } @@ -1389,10 +1402,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/components/datepicker/datepickerSpec.js b/components/datepicker/datepickerSpec.js index 68a38a5978..0f0485909c 100644 --- a/components/datepicker/datepickerSpec.js +++ b/components/datepicker/datepickerSpec.js @@ -136,14 +136,14 @@ describe('Datepicker Plugin', () => { setTimeout(() => { expect(input.value) .withContext('value should change with confirm interaction') - .toEqual(`${month < 10 ? `0${month}` : month}/0${day}/${year}`); + .toEqual(`${month < 10 ? `0${month}` : month}/${day < 10 ? '0' + day : day}/${year}`); dayEl = document.querySelector('.datepicker-container button[data-day="1"]'); dayEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); cancelBtn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); setTimeout(() => { expect(input.value) .withContext('value should not change with cancel interaction') - .toEqual(`${month < 10 ? `0${month}` : month}/0${day}/${year}`); + .toEqual(`${month < 10 ? `0${month}` : month}/${day < 10 ? '0' + day : day}/${year}`); clearBtn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); setTimeout(() => { expect(input.value.length) @@ -156,7 +156,7 @@ describe('Datepicker Plugin', () => { }, 10); }, 10); }, 10); - }); + }, 10); }); }); }); diff --git a/components/dialog/_modal.scss b/components/dialog/_modal.scss index 237b7668f2..351bff34f1 100644 --- a/components/dialog/_modal.scss +++ b/components/dialog/_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/components/timepicker/_timepicker.scss b/components/timepicker/_timepicker.scss index e34790861c..b4cf26eafd 100644 --- a/components/timepicker/_timepicker.scss +++ b/components/timepicker/_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/components/timepicker/timepicker.ts b/components/timepicker/timepicker.ts index 0cba1b0ce0..5cfe3900e4 100644 --- a/components/timepicker/timepicker.ts +++ b/components/timepicker/timepicker.ts @@ -1,6 +1,7 @@ import { Utils } from '../../src/utils'; import { Component, BaseOptions, InitElements, MElement, I18nOptions } from '../../src/component'; import { DockedDisplayPlugin } from '../../src/dockedDisplayPlugin'; +import { ModalDisplayPlugin } from '../../src/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,16 +192,8 @@ export class Timepicker extends Component { this._setupVariables(); this._setupEventHandlers(); this._clockSetup(); + if (this.options.displayPlugin) this._setupDisplayPlugin(); this._pickerSetup(); - - if (this.options.displayPlugin) { - if (this.options.displayPlugin === 'docked') - this.displayPlugin = DockedDisplayPlugin.init( - this.el, - this.containerEl, - this.options.displayPluginOptions - ); - } } static get defaults(): TimepickerOptions { @@ -265,6 +259,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); @@ -415,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'); @@ -450,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 @@ -827,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); }; diff --git a/sass/_global.scss b/sass/_global.scss index 8decbf33fd..facd5f052b 100644 --- a/sass/_global.scss +++ b/sass/_global.scss @@ -478,6 +478,10 @@ $spacing-values: ("0": 0, "1": 0.25rem, "2": 0.5rem, "3": 0.75rem, "4": 1rem, "5 visibility: hidden; } +.confirmation-btns { + margin-left: auto; +} + /* This is needed for some mobile phones to display the Google Icon font properly */ .material-icons, .material-symbols-outlined, .material-symbols-rounded, .material-symbols-sharp { diff --git a/src/dockedDisplayPlugin.ts b/src/dockedDisplayPlugin.ts index fa2be7ca4b..c257502974 100644 --- a/src/dockedDisplayPlugin.ts +++ b/src/dockedDisplayPlugin.ts @@ -17,13 +17,28 @@ export interface DockedDisplayPluginOptions { * The alignment of the docked container */ align: string; + /** + * Title element. + */ + title: HTMLElement|null, + /** + * On open callback. + */ + onOpen: (() => void) | null; + /** + * On close callback. + */ + onClose: (() => void) | null; } const _defaults: DockedDisplayPluginOptions = { margin: 5, transition: 10, duration: 250, - align: 'left' + align: 'left', + title: null, + onOpen: null, + onClose: null }; export class DockedDisplayPlugin { @@ -94,7 +109,10 @@ export class DockedDisplayPlugin { setTimeout(() => { this.container.style.transform = `translateX(${coordinates.x}px) translateY(${coordinates.y}px)`; this.container.style.opacity = (1).toString(); - }, 1); + if (typeof this.options.onOpen == 'function') { + this.options.onOpen.call(this); + } + }, 100); }; hide = () => { @@ -108,6 +126,9 @@ export class DockedDisplayPlugin { setTimeout(() => { this.container.style.transform = `translateX(0px) translateY(0px)`; this.container.style.opacity = '0'; - }, 1); + if (typeof this.options.onClose == 'function') { + this.options.onClose.call(this); + } + }, 100); }; } diff --git a/src/modalDisplayPlugin.ts b/src/modalDisplayPlugin.ts new file mode 100644 index 0000000000..9d030998a4 --- /dev/null +++ b/src/modalDisplayPlugin.ts @@ -0,0 +1,100 @@ +export interface ModalDisplayPluginOptions { + /** + * Classes to add on modal container. + */ + classList: string[], + /** + * Title element. + */ + title: HTMLElement|null, + /** + * On open callback. + */ + onOpen: (() => void) | null; + /** + * On close callback. + */ + onClose: (() => void) | null; +} + +const _defaults: ModalDisplayPluginOptions = { + classList: ['modal'], + title: null, + onOpen: null, + onClose: 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(); + } + }, true); + } + + /** + * 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'); + setTimeout(() => { + if (typeof this.options.onOpen == 'function') { + this.options.onOpen.call(this); + } + }, 10); + }; + + hide = () => { + if (!this.visible) return; + this.visible = false; + this.container.removeAttribute('open'); + setTimeout(() => { + if (typeof this.options.onClose == 'function') { + this.options.onClose.call(this); + } + }, 10); + }; +}