From da54a0bfacfba773d63697a65652e54a560a954c Mon Sep 17 00:00:00 2001 From: AG <81616437+goremikins@users.noreply.github.com> Date: Mon, 25 Jul 2022 17:11:17 +0300 Subject: [PATCH 01/21] feat(datetime-field): make locale function public feat(datetime-picker): refactor element --- packages/elements/src/datetime-field/index.ts | 4 +- .../src/datetime-picker/__demo__/index.html | 57 +- .../elements/src/datetime-picker/index.ts | 626 +++++------------- .../elements/src/datetime-picker/utils.ts | 148 ++--- 4 files changed, 275 insertions(+), 560 deletions(-) diff --git a/packages/elements/src/datetime-field/index.ts b/packages/elements/src/datetime-field/index.ts index 1aa347b891..1d0ad43eb7 100644 --- a/packages/elements/src/datetime-field/index.ts +++ b/packages/elements/src/datetime-field/index.ts @@ -196,11 +196,11 @@ export class DatetimeField extends TextField { @translate({ mode: 'directive', scope: 'ef-datetime-field' }) protected t!: TranslateDirective; + private _locale: Locale | null = null; /** * Format, which is based on locale */ - private _locale: Locale | null = null; - protected get locale (): Locale { + public get locale (): Locale { if (!this._locale) { this._locale = this.resolveLocale(); } diff --git a/packages/elements/src/datetime-picker/__demo__/index.html b/packages/elements/src/datetime-picker/__demo__/index.html index 67a46f21fe..b5a4b302ca 100644 --- a/packages/elements/src/datetime-picker/__demo__/index.html +++ b/packages/elements/src/datetime-picker/__demo__/index.html @@ -40,7 +40,8 @@

- + +

@@ -82,6 +83,14 @@

+

+ + Default + Full + Short + Time Only + +

Weekdays Only Weekends Only @@ -128,7 +137,9 @@ weekendsOnly: dateTimePicker.weekendsOnly, inputTriggerDisabled: dateTimePicker.inputTriggerDisabled, inputDisabled: dateTimePicker.inputDisabled, - popupDisabled: dateTimePicker.popupDisabled + popupDisabled: dateTimePicker.popupDisabled, + isoFormat: dateTimePicker.locale ? dateTimePicker.locale.isoFormat : '', + formatOptions: dateTimePicker.locale ? dateTimePicker.locale.options : {} }; consoleEl.value = JSON.stringify(data, undefined, 4); @@ -145,6 +156,7 @@ dateFrom.value = ''; dateTo.value = ''; dateTimePicker.value = ''; + dateTimePicker.formatOptions = null; dateValue.view = ''; dateFrom.view = ''; dateTo.view = ''; @@ -228,9 +240,44 @@ setShowSeconds(value); }); document.getElementById('lang').addEventListener('value-changed', ({ detail: { value } }) => { - // resetValue(); dateTimePicker.lang = value ? value : ''; }); + document.getElementById('format-options').addEventListener('value-changed', ({ detail: { value } }) => { + let formatOptions = null; + switch (value) { + case 'full': + formatOptions = { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + hour12: true, + dayPeriod: 'long' + }; + break; + case 'short': + formatOptions = { + year: '2-digit', + month: '2-digit', + day: '2-digit', + hour: 'numeric', + minute: 'numeric' + }; + break; + case 'time': + formatOptions = { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }; + break; + // no default + } + dateTimePicker.value = ''; + dateTimePicker.formatOptions = formatOptions; + }); document.getElementById('placeholder').addEventListener('value-changed', ({ detail: { value } }) => { resetValue(); dateTimePicker.placeholder = value ? value : ''; @@ -312,6 +359,10 @@ dateTimePicker.addEventListener('opened-changed', onEvent); dateTimePicker.addEventListener('value-changed', onEvent); dateTimePicker.addEventListener('view-changed', onEvent); + document.getElementById('clear-events').addEventListener('click', () => { + eventLog.length = 0; + document.getElementById('events').value = eventLog.join('\n'); + }); diff --git a/packages/elements/src/datetime-picker/index.ts b/packages/elements/src/datetime-picker/index.ts index f0bcf8253a..350a6884a1 100644 --- a/packages/elements/src/datetime-picker/index.ts +++ b/packages/elements/src/datetime-picker/index.ts @@ -8,7 +8,7 @@ import { CSSResultGroup, TapEvent, WarningNotice, - DeprecationNotice + FocusedPropertyKey } from '@refinitiv-ui/core'; import { customElement } from '@refinitiv-ui/core/decorators/custom-element.js'; import { property } from '@refinitiv-ui/core/decorators/property.js'; @@ -29,32 +29,32 @@ import type { Icon } from '../icon'; import type { Calendar } from '../calendar'; import { translate, - TranslateDirective, - getLocale, - TranslatePropertyKey + TranslateDirective } from '@refinitiv-ui/translate'; import { addMonths, subMonths, isAfter, isBefore, - isValidDate, - getFormat, - DateFormat, - parse, format, - Locale + toSegment, + Locale, + DateFormat } from '@refinitiv-ui/utils/date.js'; import { - DateTimeSegment, + getCurrentSegment, formatToView, - getCurrentTime + formatToDate, + formatToTime, + hasTimePicker, + hasSeconds, + hasDatePicker, + hasAmPm } from './utils.js'; import { preload } from '../icon/index.js'; import type { TimePicker } from '../time-picker'; -import type { TextField } from '../text-field'; import type { Overlay } from '../overlay'; import type { DatetimeField } from '../datetime-field'; @@ -148,60 +148,22 @@ export class DatetimePicker extends ControlElement implements MultiValue { } private lazyRendered = false; /* speed up rendering by not populating popup window on first load */ - private calendarValues: string[] = []; /* used to store date information for calendars */ - private timepickerValues: string[] = []; /* used to store time information for timepickers */ - private inputValues: string[] = []; /* used to formatted datetime value for inputs */ - private inputSyncing = true; /* true when inputs and pickers are in sync. False while user types in input */ - private _min = ''; - private minDate = ''; /** - * Set minimum date - * @param min date - * @default - + * Set minimum date. + * This value must follow the `format` and be less + * than or equal to the value of the `max` attribute */ - @property({ type: String }) - public set min (min: string) { - if (!this.isValidValue(min)) { - this.warnInvalidValue(min); - min = ''; - } - - const oldMin = this.min; - if (oldMin !== min) { - this._min = min; - this.minDate = min ? format(parse(min), DateFormat.yyyyMMdd) : ''; - this.requestUpdate('min', oldMin); - } - } - public get min (): string { - return this._min; - } + @property({ type: String, reflect: true }) + public min: string | null = null; - private _max = ''; - private maxDate = ''; /** - * Set maximum date - * @param max date - * @default - + * Set maximum date. + * This value must follow the `format` and be greater + * than or equal to the value of the `min` attribute */ - @property({ type: String }) - public set max (max: string) { - if (!this.isValidValue(max)) { - this.warnInvalidValue(max); - max = ''; - } - - const oldMax = this.max; - if (oldMax !== max) { - this._max = max; - this.maxDate = max ? format(parse(max), DateFormat.yyyyMMdd) : ''; - this.requestUpdate('max', oldMax); - } - } - public get max (): string { - return this._max; - } + @property({ type: String, reflect: true }) + public max: string | null = null; /** * Only enable weekdays @@ -228,7 +190,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @param firstDayOfWeek The first day of the week */ @property({ type: Number, attribute: 'first-day-of-week' }) - public firstDayOfWeek?: number; + public firstDayOfWeek: number | null = null; /** * Set to switch to range select mode @@ -266,7 +228,6 @@ export class DatetimePicker extends ControlElement implements MultiValue { } private _values: string[] = []; /* list of values as passed by the user */ - private _segments: DateTimeSegment[] = []; /* filtered and processed list of values */ /** * Set multiple selected values * @param values Values to set @@ -284,12 +245,11 @@ export class DatetimePicker extends ControlElement implements MultiValue { const oldValues = this._values; if (String(oldValues) !== String(values)) { this._values = values; - this.valuesToSegments(); - this.requestUpdate('_values', oldValues); /* segments are populated in update */ + this.requestUpdate('values', oldValues); } } public get values (): string[] { - return this._segments.map(segment => segment.value); + return this._values; } /** @@ -305,23 +265,11 @@ export class DatetimePicker extends ControlElement implements MultiValue { @property({ type: Boolean, attribute: 'show-seconds', reflect: true }) public showSeconds = false; - private _placeholder = ''; /** - * Placeholder to display when no value is set - * @param placeholder Placeholder - * @default - + * Set placeholder text */ @property({ type: String }) - public set placeholder (placeholder: string) { - const oldPlaceholder = this._placeholder; - if (oldPlaceholder !== placeholder) { - this._placeholder = placeholder; - this.requestUpdate('placeholder', oldPlaceholder); - } - } - public get placeholder (): string { - return this._placeholder; - } + public placeholder = ''; /** * Toggles the opened state of the list @@ -360,24 +308,6 @@ export class DatetimePicker extends ControlElement implements MultiValue { @property({ type: Boolean, attribute: 'popup-disabled', reflect: true }) public popupDisabled = false; - /** - * Set the datetime format - * Based on dane-fns datetime formats - * @ignore - * @param format Date format - */ - @property({ type: String }) - public set format (format: string) { - new DeprecationNotice('`format` attribute and property are deprecated. Use `formatOptions` property instead.').show(); - } - /** - * @ignore - */ - public get format (): string { - return ''; - } - - private _formatOptions: Intl.DateTimeFormatOptions | null = null; /** * Set the datetime format options based on * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat @@ -386,18 +316,8 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @param formatOptions Format options * @default - null */ - @property({ attribute: false }) - public set formatOptions (formatOptions: Intl.DateTimeFormatOptions | null) { - const oldFormatOptions = this._formatOptions; - if (oldFormatOptions !== formatOptions) { - this._formatOptions = formatOptions; - this._locale = null; - this.requestUpdate('formatOptions', oldFormatOptions); - } - } - public get formatOptions (): Intl.DateTimeFormatOptions | null { - return this._formatOptions; - } + @property({ attribute: false, type: Object }) + public formatOptions: Intl.DateTimeFormatOptions | null = null; /** * Toggle to display the time picker @@ -437,7 +357,6 @@ export class DatetimePicker extends ControlElement implements MultiValue { @property({ attribute: false }) public set views (views: string[]) { const oldViews = this._views; - views = this.filterAndWarnInvalidViews(views); if (oldViews.toString() !== views.toString()) { this._views = views; this.requestUpdate('views', oldViews); @@ -448,60 +367,42 @@ export class DatetimePicker extends ControlElement implements MultiValue { return this._views; } - const now = new Date(); - const from = this.values[0]; + const now = format(new Date(), DateFormat.yyyyMM); + const from = formatToView(this.values[0]); if (!this.isDuplex()) { - return [formatToView(from || now)]; + return [from || now]; } - const to = this.values[1]; + const to = formatToView(this.values[1]); // default duplex mode - if (this.isDuplexConsecutive() || !from || !to || formatToView(from) === formatToView(to) || isBefore(to, from)) { - return this.composeViews(formatToView(from || to || now), !from && to ? 1 : 0, []); + if (this.isDuplexConsecutive() || !from || !to || from === to || isBefore(to, from)) { + return this.composeViews(from || to || now, !from && to ? 1 : 0, []); } // duplex split if as from and to - return [formatToView(from), formatToView(to)]; + return [from, to]; } /** - * Format, which is based on locale + * Returns true if an input element contains valid data. + * @returns true if input is valid */ - private _locale: Locale | null = null; - protected get locale (): Locale { - if (!this._locale) { - this._locale = this.resolveLocale(); - } - return this._locale; - } - - /** - * Resolve locale based on element parameters - * @returns locale Resolved locale - */ - protected resolveLocale (): Locale { - const hasTimePicker = this.hasTimePicker; - // TODO: Do not use dateStyle and timeStyle as these are supported only in modern browsers - return Locale.fromOptions(this.formatOptions || { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: hasTimePicker ? 'numeric' : undefined, - minute: hasTimePicker ? 'numeric' : undefined, - second: this.showSeconds ? 'numeric' : undefined, - hour12: this.amPm ? true : undefined // force am-pm if provided, otherwise rely on locale - }, `${getLocale(this)}`); + public checkValidity (): boolean { + return (this.inputEl ? this.inputEl.checkValidity() : true) + && (this.inputToEl ? this.inputToEl.checkValidity() : true) + && this.isFromBeforeTo(); } /** - * Validates the input, marking the element as invalid if its value does not meet the validation criteria. - * @returns {void} + * Validate input. Mark as error if input is invalid + * @returns false if there is an error */ - public validateInput (): void { - const hasError = !this.isFromBeforeTo(); - this.setErrorAndNotify(hasError); + public reportValidity (): boolean { + const hasError = !this.checkValidity(); + this.notifyErrorChange(hasError); + return !hasError; } /** @@ -519,142 +420,113 @@ export class DatetimePicker extends ControlElement implements MultiValue { @query('#input-to') private inputToEl?: DatetimeField | null; /** - * Updates the element - * @param changedProperties Properties that has changed - * @returns {void} + * @ignore + * TODO: needs more elegant solution + * Format, which is based on locale */ - protected update (changedProperties: PropertyValues): void { - if (changedProperties.has('opened') && this.opened) { - this.lazyRendered = true; - } - // make sure to close popup for disabled - if (this.opened && !this.canOpenPopup) { - this.opened = false; /* this cannot be nor stopped nor listened */ - } - - if (changedProperties.has('_values') || changedProperties.has(TranslatePropertyKey)) { - this.syncInputValues(); - } - - // re-validation - if (changedProperties.has('_values') && changedProperties.get('_values') !== undefined) { - this.validateInput(); - } - - super.update(changedProperties); + public get locale (): Locale { + return this.inputEl ? this.inputEl.locale : Locale.fromOptions({}); } /** - * Called after the component is first rendered - * @param changedProperties Properties which have changed - * @returns {void} + * Returns true if the datetime field has timepicker + * @returns hasTimePicker */ - protected firstUpdated (changedProperties: PropertyValues): void { - super.firstUpdated(changedProperties); - this.addEventListener('keydown', this.onKeyDown); - this.addEventListener('tap', this.onTap); + protected get hasTimePicker (): boolean { + return hasTimePicker(this.locale.options); } - /** - * Overwrite validation method for value - * - * @param value value - * @returns {boolean} result - */ - protected isValidValue (value: string): boolean { - if (value === '') { - return true; - } - // value format depends on locale. - return getFormat(value) === this.locale.isoFormat; + protected get hasSeconds (): boolean { + return hasSeconds(this.locale.options); } /** * Returns true if the datetime field has timepicker * @returns hasTimePicker */ - protected get hasTimePicker (): boolean { - // need to check for attribute to resolve the value correctly until the first lifecycle is run - return this.timepicker || this.hasAttribute('timepicker') || this.hasAmPm || this.hasSeconds; + protected get hasDatePicker (): boolean { + return hasDatePicker(this.locale.options); } - /** - * Returns true if the datetime field has seconds - * @returns hasSeconds - */ - protected get hasSeconds (): boolean { - return this.showSeconds || this.hasAttribute('show-seconds'); + protected get hasAmPm (): boolean { + return hasAmPm(this.locale.options); } - + /** - * Returns true if the datetime field has am-pm - * @returns hasAmPm + * Called after the component is first rendered + * @param changedProperties Properties which have changed + * @returns {void} */ - protected get hasAmPm (): boolean { - return this.amPm || this.hasAttribute('am-pm'); + protected firstUpdated (changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + this.addEventListener('keydown', this.onKeyDown); } /** - * Used to show a warning when the value does not pass the validation - * @param value that is invalid + * Updates the element + * @param changedProperties Properties that has changed * @returns {void} */ - protected warnInvalidValue (value: string): void { - new WarningNotice(`${this.localName}: the specified value "${value}" does not conform to the required format. The format is '${this.locale.isoFormat}'.`).show(); + public willUpdate (changedProperties: PropertyValues): void { + super.willUpdate(changedProperties); + + if (changedProperties.has('opened') && this.opened) { + this.lazyRendered = true; + } + // make sure to close popup for disabled + if (this.opened && !this.canOpenPopup) { + this.opened = false; /* this cannot be nor stopped nor listened */ + } + + if (this.shouldValidateInput(changedProperties)) { + this.validateInput(); + } } /** - * Show invalid view message - * @param value Invalid value - * @returns {void} + * Check if input should be re-validated + * @param changedProperties Properties that has changed + * @returns True if input should be re-validated */ - protected warnInvalidView (value: string): void { - new WarningNotice(`The specified value "${value}" does not conform to the required format. The format is "yyyy-MM".`).show(); + protected shouldValidateInput (changedProperties: PropertyValues): boolean { + // TODO: this needs refactoring with all other fields to support common validation patterns + return (changedProperties.has(FocusedPropertyKey) && !this.focused); } /** - * Convert value string array to date segments - * Warn invalid value if passed value does not confirm a segment + * Validate input according `pattern`, `minLength` and `maxLength` properties + * change state of `error` property according pattern validation * @returns {void} */ - private valuesToSegments (): void { - const newSegments = this.filterAndWarnInvalidValues(this._values).map(value => DateTimeSegment.fromString(value)); - this._segments = newSegments; - this.interimSegments = newSegments; + protected validateInput (): void { + this.reportValidity(); } /** - * A helper method to make sure that only valid values are passed - * Warn if passed value is invalid - * @param values Values to check - * @returns Filtered collection of values + * Reset error state on input + * @returns {void} */ - private filterAndWarnInvalidValues (values: string[]): string[] { - return values.map(value => { - if (this.isValidValue(value)) { - return value; - } - - this.warnInvalidValue(value); - return ''; - }); + protected resetError (): void { + if (this.error && this.checkValidity()) { + this.reportValidity(); + } } /** - * A helper method to make sure that only valid views are passed - * Warn if passed view is invalid - * @param views Views to check - * @returns Filtered collection of values + * Check if `from` is before or the same as `to` + * @returns true if `from` is before or the same as `to` */ - private filterAndWarnInvalidViews (views: string[]): string[] { - for (let i = 0; i < views.length; i += 1) { - const view = views[i]; - if (!isValidDate(view, DateFormat.yyyyMM)) { - this.warnInvalidView(view); - return []; /* if at least one view is invalid, do not care about the rest to avoid empty views */ + protected isFromBeforeTo (): boolean { + if (this.range) { + const from = this.values[0]; + const to = this.values[1]; + + if (from && to && from !== to) { + return isBefore(from, to); } } - return views; + + return true; } /** @@ -681,34 +553,6 @@ export class DatetimePicker extends ControlElement implements MultiValue { return this.duplex === '' || this.duplex === 'consecutive'; } - /** - * Stop syncing input values and picker values - * @returns {void} - */ - private disableInputSync (): void { - this.inputSyncing = false; - } - - /** - * Start syncing input values and picker values - * @returns {void} - */ - private enableInputSync (): void { - this.inputSyncing = true; - } - - /** - * Synchronise input values and values - * @return {void} - */ - private syncInputValues (): void { - if (!this.inputSyncing) { - return; - } - // input values cannot be populated off interim segments as require a valid date - this.inputValues = this._segments.map(segment => segment.value); - } - /** * Construct view collection * @param view The view that has changed @@ -725,10 +569,10 @@ export class DatetimePicker extends ControlElement implements MultiValue { if (this.isDuplexConsecutive()) { if (index === 0) { /* from */ - return [view, formatToView(addMonths(view, 1))]; + return [view, addMonths(view, 1)]; } else { /* to */ - return [formatToView(subMonths(view, 1)), view]; + return [subMonths(view, 1), view]; } } @@ -739,7 +583,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { after = view; } - return [view, formatToView(after)]; + return [view, after]; } if (index === 1) { /* to. from must be before or the same */ @@ -748,66 +592,18 @@ export class DatetimePicker extends ControlElement implements MultiValue { before = view; } - return [formatToView(before), view]; + return [before, view]; } return []; } - private _interimSegments: DateTimeSegment[] = []; - /** - * An interim collection of segments to push values when all parts are populated - * and validated - * @param segments Segments - */ - private set interimSegments (segments: DateTimeSegment[]) { - const interimSegments = segments.map(segment => DateTimeSegment.fromDateTimeSegment(segment)); - this._interimSegments = interimSegments; - // cannot populate calendar if from is after to, it looks broken - this.calendarValues = this.isFromBeforeTo() ? interimSegments.map(segment => segment.dateSegment) : []; - this.timepickerValues = interimSegments.map(segment => segment.timeSegment); - } - /** - * Get interim segments. These are free to modify - * @returns interim segments - */ - private get interimSegments (): DateTimeSegment[] { - return this._interimSegments; - } - - /** - * Submit interim segments to values. - * Notify value-changed event. - * @returns true if values have changed. False otherwise - */ - private submitInterimSegments (): boolean { - const oldSegments = this._segments; - const newSegments = this.interimSegments; - - // compare if different - if (oldSegments.toString() === newSegments.toString()) { - return false; - } - - const newValues = newSegments.map(segment => segment.value); - - // validate - for (let i = 0; i < newValues.length; i += 1) { /* need this step in case timepicker is not populated */ - if (!this.isValidValue(newValues[i])) { - return false; - } - } - - this.notifyValuesChange(newValues); - return true; - } - /** * Notify error if it has changed * @param hasError true if the element has an error * @returns {void} */ - protected setErrorAndNotify (hasError: boolean): void { + protected notifyErrorChange (hasError: boolean): void { if (this.error !== hasError) { this.error = hasError; this.notifyPropertyChange('error', this.error); @@ -847,17 +643,17 @@ export class DatetimePicker extends ControlElement implements MultiValue { switch (event.key) { case 'Down': case 'ArrowDown': - this.setOpened(true); + // this.setOpened(true); break; case 'Up': case 'ArrowUp': - !event.defaultPrevented && this.setOpened(false); + // !event.defaultPrevented && this.setOpened(false); break; default: return; } - event.preventDefault(); + // event.preventDefault(); } /** @@ -865,7 +661,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @param event Key down event object * @returns {void} */ - private onCalendarKeyDown (event: KeyboardEvent): void { + private onPopupKeyDown (event: KeyboardEvent): void { switch (event.key) { case 'Esc': case 'Escape': @@ -880,44 +676,12 @@ export class DatetimePicker extends ControlElement implements MultiValue { } /** - * Handles key input on text field - * @param event Key down event object - * @returns {void} - */ - private onInputKeyDown (event: KeyboardEvent): void { - switch (event.key) { - case 'Esc': - case 'Escape': - !this.opened && this.blur(); - this.setOpened(false); - break; - case 'Enter': - this.toggleOpened(); - break; - default: - return; - } - - event.preventDefault(); - } - - /** - * Run on tap event + * Run on icon tap event * @param event Tap event * @returns {void} */ - private onTap (event: TapEvent): void { - const path = event.composedPath(); - if (this.popupEl && path.includes(this.popupEl)) { - return; /* popup is managed separately */ - } - - if (path.includes(this.iconEl)) { - this.toggleOpened(); - } - else if (!this.inputTriggerDisabled) { - this.setOpened(true); - } + private onIconTap (event: TapEvent): void { + this.setOpened(true); } /** @@ -948,18 +712,8 @@ export class DatetimePicker extends ControlElement implements MultiValue { */ private onCalendarValueChanged (event: ValueChangedEvent): void { const values = (event.target as Calendar).values; - this.interimSegments = values.map((value, index) => { - const segment = this.interimSegments[index] || new DateTimeSegment(); - segment.dateSegment = value; + this.synchroniseCalendarValues(values); - if (this.timepicker && !segment.timeSegment) { - segment.timeSegment = getCurrentTime(this.showSeconds); /* populate time, as otherwise time picker looks broken */ - } - - return segment; - }); - - this.submitInterimSegments(); // in duplex mode, avoid jumping on views // Therefore if any of values have changed, save the current view @@ -983,10 +737,18 @@ export class DatetimePicker extends ControlElement implements MultiValue { private onTimePickerValueChanged (event: ValueChangedEvent): void { const target = event.target as TimePicker; const index = target === this.timepickerToEl ? 1 : 0; /* 0 - from, single; 1 - to */ - const segment = this.interimSegments[index] || new DateTimeSegment(); - segment.timeSegment = target.value; - this.interimSegments[index] = segment; - this.submitInterimSegments(); + const values = [...this._values]; + values[index] = target.value; + this.synchroniseCalendarValues(values); + } + + private synchroniseCalendarValues (values: string[]): void { + const segments = values.map(value => value ? toSegment(value) : null); + const oldSegments = this._values.map(value => value ? toSegment(value) : null); + const newValues = segments.map((segment, idx) => segment ? format(Object.assign(getCurrentSegment(), oldSegments[idx] || {}, segment), this.locale.isoFormat) : ''); + + this.notifyValuesChange(newValues); + this.resetError(); } /** @@ -996,23 +758,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { */ private onInputErrorChanged (event: ErrorChangedEvent): void { const hasError = event.detail.value; - this.setErrorAndNotify(hasError); - } - - /** - * Run on input focus - * @returns {void} - */ - private onInputFocus (): void { - this.disableInputSync(); - } - - /** - * Run on input blur - * @returns {void} - */ - private onInputBlur (): void { - this.enableInputSync(); + this.notifyErrorChange(hasError); } /** @@ -1021,40 +767,18 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @returns {void} */ private onInputValueChanged (event: ValueChangedEvent): void { - const target = event.target as TextField; + const target = event.target as DatetimeField; const index = target === this.inputToEl ? 1 : 0; /* 0 - from, single; 1 - to */ - const segment = this.interimSegments[index] || new DateTimeSegment(); - this.resetViews(); - segment.dateSegment = target.value; - this.interimSegments[index] = segment; - this.submitInterimSegments(); - } - - /** - * Check if `from` is before or the same as `to` - * @returns true if `from` is before or the same as `to` - */ - private isFromBeforeTo (): boolean { - if (this.range) { - const from = this.values[0]; - const to = this.values[1]; + const newValues = [...this._values]; + newValues[index] = target.value; - if (from && to) { - if (parse(from).getTime() > parse(to).getTime()) { - return false; - } - } + // Set values silently, because the input already has the value + if (this._values.toString() !== newValues.toString()) { + this._values = newValues; + this.notifyPropertyChange('value', this.value); } - return true; - } - - /** - * Toggles the opened state of the list - * @returns {void} - */ - private toggleOpened (): void { - this.setOpened(!this.opened); + this.resetError(); } /** @@ -1097,9 +821,9 @@ export class DatetimePicker extends ControlElement implements MultiValue { return html``; } @@ -1117,15 +841,14 @@ export class DatetimePicker extends ControlElement implements MultiValue { .fillCells=${!this.isDuplex()} .range=${this.range} .multiple=${this.multiple} - .min=${this.minDate} - .max=${this.maxDate} + .min=${ifDefined(formatToDate(this.min) || undefined)} + .max=${ifDefined(formatToDate(this.max) || undefined)} .weekdaysOnly=${this.weekdaysOnly} .weekendsOnly=${this.weekendsOnly} - .firstDayOfWeek=${ifDefined(this.firstDayOfWeek)} - .values=${this.calendarValues} + .firstDayOfWeek=${ifDefined(this.firstDayOfWeek === null ? undefined : this.firstDayOfWeek)} + .values=${this.values.map(value => formatToDate(value))} .filter=${this.filter} .view=${view} - @keydown=${this.onCalendarKeyDown} @view-changed=${this.onCalendarViewChanged} @value-changed=${this.onCalendarValueChanged}>`; } @@ -1145,11 +868,10 @@ export class DatetimePicker extends ControlElement implements MultiValue { */ private get timepickersTemplate (): TemplateResult { // TODO: how can we add support timepicker with multiple? - const values = this.timepickerValues; return html` - ${this.getTimepickerTemplate('timepicker', values[0])} + ${this.getTimepickerTemplate('timepicker', this.values[0])} ${this.range ? html`

` : undefined} - ${this.range ? this.getTimepickerTemplate('timepicker-to', values[1]) : undefined} + ${this.range ? this.getTimepickerTemplate('timepicker-to', this.values[1]) : undefined} `; } @@ -1165,8 +887,8 @@ export class DatetimePicker extends ControlElement implements MultiValue { part="input" transparent id=${id} - min=${this.min} - max=${this.max} + min=${ifDefined(this.min || undefined)} + max=${ifDefined(this.max || undefined)} lang=${ifDefined(this.lang || undefined)} ?am-pm=${this.amPm} ?show-seconds=${this.showSeconds} @@ -1176,9 +898,6 @@ export class DatetimePicker extends ControlElement implements MultiValue { .value=${value} .placeholder=${this.placeholder} .formatOptions=${this.formatOptions} - @focus=${this.onInputFocus} - @keydown=${this.onInputKeyDown} - @blur=${this.onInputBlur} @value-changed=${this.onInputValueChanged} @error-changed=${this.onInputErrorChanged}>`; } @@ -1188,7 +907,16 @@ export class DatetimePicker extends ControlElement implements MultiValue { */ private get iconTemplate (): TemplateResult { return html` - + `; } @@ -1197,13 +925,11 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @returns inputTemplate */ private get inputTemplates (): TemplateResult { - const values = this.inputValues; - return html`
- ${this.getInputTemplate('input', values[0])} + ${this.getInputTemplate('input', this.values[0] || '')} ${this.range ? html`
` : undefined} - ${this.range ? this.getInputTemplate('input-to', values[1]) : undefined} + ${this.range ? this.getInputTemplate('input-to', this.values[1] || '') : undefined}
`; } @@ -1214,25 +940,21 @@ export class DatetimePicker extends ControlElement implements MultiValue { private get popupTemplate (): TemplateResult | undefined { if (this.lazyRendered) { return html` + @opened-changed=${this.onPopupOpenedChanged} + @keydown=${this.onPopupKeyDown}>
-
- ${this.calendarsTemplate} -
- ${this.timepicker ? html`
${this.timepickersTemplate}
` : undefined} + ${this.hasDatePicker ? html`
${this.calendarsTemplate}
` : undefined} + ${this.hasTimePicker ? html`
${this.timepickersTemplate}
` : undefined}
diff --git a/packages/elements/src/datetime-picker/utils.ts b/packages/elements/src/datetime-picker/utils.ts index 8aec73792b..a24fee9bf9 100644 --- a/packages/elements/src/datetime-picker/utils.ts +++ b/packages/elements/src/datetime-picker/utils.ts @@ -1,123 +1,65 @@ import { format, DateFormat, - parse, TimeFormat, - toTimeSegment + toSegment, + toDateTimeSegment, + DateTimeSegment } from '@refinitiv-ui/utils/date.js'; /** - * A helper class to split date time string into date and time segments + * Get current datetime segment + * @returns segment Date time segment */ -class DateTimeSegment { - /** - * Create DateTimeSegment from value string - * @param value Date time value - * @returns date time segment - */ - static fromString = (value: string): DateTimeSegment => { - const valueSplit = value.split('T'); - return new DateTimeSegment(valueSplit[0], valueSplit[1]); - }; - - /** - * Create DateTimeSegment from another DateTimeSegment - * @param segment DateTimeSegment - * @returns cloned date time segment - */ - static fromDateTimeSegment = (segment: DateTimeSegment): DateTimeSegment => { - return new DateTimeSegment(segment.dateSegment, segment.timeSegment); - }; - - /** - * Create new date time segment - * @param dateSegment Date segment - * @param timeSegment Time segment - */ - constructor (dateSegment = '', timeSegment = '') { - this.dateSegment = dateSegment; - this.timeSegment = timeSegment; - } - - /** - * Date segment in a format '2020-12-31' - */ - public dateSegment!: string; - - /** - * Time segment in a format '14:59' or '14:59:59' - */ - public timeSegment!: string; - - /** - * Get string value - */ - public get value (): string { - const timeSegment = this.timeSegment; - return `${this.dateSegment}${timeSegment ? `T${timeSegment}` : ''}`; - } - - /** - * Get time - * @returns {number} time - */ - public getTime (): number { - const date = this.dateSegment ? parse(this.dateSegment) : new Date(0); - const timeSegment = toTimeSegment(this.timeSegment); - date.setHours(timeSegment.hours); - date.setMinutes(timeSegment.minutes); - date.setSeconds(timeSegment.seconds); - return date.getTime(); - } - - public toString (): string { - return this.value; - } -} - -/** -* Check if passed Date object is valid -* @param date Date to check -* @returns is valid -*/ -const isValid = (date: Date): boolean => { - return !isNaN(date.getTime()); +const getCurrentSegment = (): DateTimeSegment => { + const date = new Date(); + date.setHours(12); + date.setMinutes(0); + date.setSeconds(0); + date.setMilliseconds(0); + return toDateTimeSegment(date); }; /** -* Convert date to Date object -* @param date Date to convert -* @returns Date object -*/ -const toDate = (date: string | Date | number): Date => { - if (typeof date === 'string') { - return parse(date); - } - return typeof date === 'number' ? new Date(date) : date; -}; + * Get Date fraction from Date or DateTime string + * Output format: "yyyy-MM-dd". + * @param value Value string + * @returns date Date string + */ +const formatToDate = (value?: string | null): string => value ? format(toSegment(value), DateFormat.yyyyMMdd) : ''; /** - * Format Date object to local date string. - * Output format: "yyyy-MM". - * @param date A Date object - * @returns A formatted date or empty string if invalid + * Get Time fraction from DateTime string + * Output format: "HH:mm" or "HH:mm:ss". + * @param value Value string + * @param [includeSeconds=false] true to include seconds + * @returns time Time string */ -const formatToView = (date: Date | number | string): string => { - date = toDate(date); - return isValid(date) ? format(date, DateFormat.yyyyMM) : ''; -}; +const formatToTime = (value?: string | null, includeSeconds = false): string => value ? format(toSegment(value), includeSeconds ? TimeFormat.HHmmss : TimeFormat.HHmm) : ''; /** - * Get current time string, e.g. "15:36" or "15:36:04" - * @param [includeSeconds=false] true to include seconds - * @returns A formatted time string + * Get Date View fraction from Date or DateTime string + * Output format: "yyyy-MM". + * @param value Value string + * @returns date Date string */ -const getCurrentTime = (includeSeconds = false): string => { - return format(new Date(), includeSeconds ? TimeFormat.HHmmss : TimeFormat.HHmm); -}; +const formatToView = (value?: string | null): string => value ? format(toSegment(value), DateFormat.yyyyMM) : ''; + +const hasTimePicker = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.hour; + +const hasSeconds = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.second; + +const hasAmPm = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.hour12; + +const hasDatePicker = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.year; export { - DateTimeSegment, - getCurrentTime, - formatToView + getCurrentSegment, + formatToDate, + formatToTime, + formatToView, + hasTimePicker, + hasSeconds, + hasDatePicker, + hasAmPm }; From 6da917eb43ae676a83369e98d53fa321ef0a3e1f Mon Sep 17 00:00:00 2001 From: AG <81616437+goremikins@users.noreply.github.com> Date: Tue, 26 Jul 2022 12:04:50 +0300 Subject: [PATCH 02/21] fix(datetime-picker): incorrect format detection in some scenarios --- .../elements/src/datetime-picker/utils.ts | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/elements/src/datetime-picker/utils.ts b/packages/elements/src/datetime-picker/utils.ts index a24fee9bf9..485273081d 100644 --- a/packages/elements/src/datetime-picker/utils.ts +++ b/packages/elements/src/datetime-picker/utils.ts @@ -8,7 +8,7 @@ import { } from '@refinitiv-ui/utils/date.js'; /** - * Get current datetime segment + * Get current datetime segment at midday Local time * @returns segment Date time segment */ const getCurrentSegment = (): DateTimeSegment => { @@ -45,13 +45,33 @@ const formatToTime = (value?: string | null, includeSeconds = false): string => */ const formatToView = (value?: string | null): string => value ? format(toSegment(value), DateFormat.yyyyMM) : ''; -const hasTimePicker = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.hour; +/** + * Check if options have second information + * @param options Intl DateTime format options + * @return hasSeconds true if options have second or millisecond + */ +const hasSeconds = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.second || !!options.fractionalSecondDigits; -const hasSeconds = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.second; +/** + * Check if options have timepicker information + * @param options Intl DateTime format options + * @return hasSeconds true if options have hour, minute, second or millisecond + */ +const hasTimePicker = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.hour || !!options.minute || hasTimePicker(options); +/** + * Check if options use 12h format + * @param options Intl DateTime format options + * @return hasSeconds true if options use 12h format + */ const hasAmPm = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.hour12; -const hasDatePicker = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.year; +/** + * Check if options have date information + * @param options Intl DateTime format options + * @return hasSeconds true if options have year, month, day or weekday + */ +const hasDatePicker = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.year || !!options.month || !!options.day || !!options.weekday; export { getCurrentSegment, From 93261077d1da81284bfaaf41a16b5dfba07f4c51 Mon Sep 17 00:00:00 2001 From: AG <81616437+goremikins@users.noreply.github.com> Date: Tue, 26 Jul 2022 12:07:29 +0300 Subject: [PATCH 03/21] fix(datetime-picker): circular function call --- packages/elements/src/datetime-picker/utils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/elements/src/datetime-picker/utils.ts b/packages/elements/src/datetime-picker/utils.ts index 485273081d..00825a4623 100644 --- a/packages/elements/src/datetime-picker/utils.ts +++ b/packages/elements/src/datetime-picker/utils.ts @@ -48,28 +48,28 @@ const formatToView = (value?: string | null): string => value ? format(toSegment /** * Check if options have second information * @param options Intl DateTime format options - * @return hasSeconds true if options have second or millisecond + * @returns hasSeconds true if options have second or millisecond */ const hasSeconds = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.second || !!options.fractionalSecondDigits; /** * Check if options have timepicker information * @param options Intl DateTime format options - * @return hasSeconds true if options have hour, minute, second or millisecond + * @returns hasTimePicker true if options have hour, minute, second or millisecond */ -const hasTimePicker = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.hour || !!options.minute || hasTimePicker(options); +const hasTimePicker = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.hour || !!options.minute || hasSeconds(options); /** * Check if options use 12h format * @param options Intl DateTime format options - * @return hasSeconds true if options use 12h format + * @returns hasAmPm true if options use 12h format */ const hasAmPm = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.hour12; /** * Check if options have date information * @param options Intl DateTime format options - * @return hasSeconds true if options have year, month, day or weekday + * @returns hasDatePicker true if options have year, month, day or weekday */ const hasDatePicker = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.year || !!options.month || !!options.day || !!options.weekday; From 6c61974ff951aa8063f617c6844d9d198ec1d537 Mon Sep 17 00:00:00 2001 From: AG <81616437+goremikins@users.noreply.github.com> Date: Wed, 27 Jul 2022 14:32:06 +0300 Subject: [PATCH 04/21] style(datetime-picker): use part button refactor(datetime-field): make resolveLocale function reusable refactor(datetime-picker): use ref instead of query fix(datetime-picker): when error value is entered, the value is reset on blur fix(datetime-picker): inputTriggerDisabled does not work fix(datetime-picker): cannot reset error value via API --- .../custom-elements/ef-datetime-picker.less | 15 +- packages/elements/src/datetime-field/index.ts | 139 ++------ .../src/datetime-field/resolvedLocale.ts | 118 +++++++ .../src/datetime-picker/__demo__/index.html | 60 +++- .../elements/src/datetime-picker/index.ts | 300 ++++++++---------- .../custom-elements/ef-datetime-picker.less | 10 +- 6 files changed, 341 insertions(+), 301 deletions(-) create mode 100644 packages/elements/src/datetime-field/resolvedLocale.ts diff --git a/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less b/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less index 97ee89ab88..371e7f00de 100644 --- a/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less +++ b/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less @@ -32,6 +32,17 @@ } // #endregion + padding: 0; + + [part~=button] { + height: 100%; + width: @button-height; + display: flex; + justify-content: center; + align-items: center; + flex: none; + } + [part=calendar] { width: @calendar-width; padding: 0 calc((@input-width - @calendar-width) / 2); @@ -57,10 +68,6 @@ color: inherit; } - [part='icon'] { - color: @control-text-color; - } - [part=input-separator] { line-height: @control-height; background: @global-text-color; diff --git a/packages/elements/src/datetime-field/index.ts b/packages/elements/src/datetime-field/index.ts index 1d0ad43eb7..41b407eba6 100644 --- a/packages/elements/src/datetime-field/index.ts +++ b/packages/elements/src/datetime-field/index.ts @@ -21,7 +21,6 @@ import { } from '@refinitiv-ui/utils/date.js'; import { translate, - getLocale, TranslateDirective, TranslatePropertyKey } from '@refinitiv-ui/translate'; @@ -31,6 +30,7 @@ import type { DateTimeFormatPart, InputSelection } from './types'; +import { resolvedLocale } from './resolvedLocale.js'; import { TextField } from '../text-field/index.js'; import { getSelectedPartIndex, @@ -111,101 +111,45 @@ export class DatetimeField extends TextField { @property({ type: String, reflect: true }) public max: string | null = null; - private _timepicker = false; /** * Toggle to display the time picker - * @param timepicker true to set timepicker mode - * @default false */ @property({ type: Boolean, reflect: true }) - public set timepicker (timepicker: boolean) { - const oldTimepicker = this._timepicker; - if (timepicker !== oldTimepicker) { - this._timepicker = timepicker; - this._locale = null; - this.requestUpdate('timepicker', oldTimepicker); - } - } - public get timepicker (): boolean { - return this._timepicker; - } + public timepicker = false; - private _showSeconds = false; /** * Toggle to display the seconds - * @param showSeconds true to show seconds - * @default false */ @property({ type: Boolean, attribute: 'show-seconds', reflect: true }) - public set showSeconds (showSeconds: boolean) { - const oldShowSeconds = this._showSeconds; - if (oldShowSeconds !== showSeconds) { - this._showSeconds = showSeconds; - this._locale = null; - this.requestUpdate('showSeconds', oldShowSeconds); - } - } - public get showSeconds (): boolean { - return this._showSeconds; - } + public showSeconds = false; - private _amPm = false; /** * Overrides 12hr time display format - * @param amPm true to show 12hr time format - * @default false */ @property({ type: Boolean, attribute: 'am-pm', reflect: true }) - public set amPm (amPm: boolean) { - const oldAmPm = this._amPm; - if (oldAmPm !== amPm) { - this._amPm = amPm; - this._locale = null; - this.requestUpdate('amPm', oldAmPm); - } - } - public get amPm (): boolean { - return this._amPm; - } + public amPm = false; - private _formatOptions: Intl.DateTimeFormatOptions | null = null; /** * Set the datetime format options based on * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat * `formatOptions` overrides `timepicker` and `showSeconds` properties. * Note: time-zone is not supported - * @param formatOptions Format options - * @default - null */ @property({ attribute: false }) - public set formatOptions (formatOptions: Intl.DateTimeFormatOptions | null) { - const oldFormatOptions = this._formatOptions; - if (oldFormatOptions !== formatOptions) { - this._formatOptions = formatOptions; - this._locale = null; - this.requestUpdate('formatOptions', oldFormatOptions); - } - } - public get formatOptions (): Intl.DateTimeFormatOptions | null { - return this._formatOptions; - } + public formatOptions: Intl.DateTimeFormatOptions | null = null; /** - * Used for translations + * Set the Locale object. + * `Locale` overrides `formatOptions`, `timepicker` and `showSeconds` properties. */ - @translate({ mode: 'directive', scope: 'ef-datetime-field' }) - protected t!: TranslateDirective; + @property({ attribute: false }) + public locale: Locale | null = null; - private _locale: Locale | null = null; /** - * Format, which is based on locale + * Used for translations */ - public get locale (): Locale { - if (!this._locale) { - this._locale = this.resolveLocale(); - } - return this._locale; - } + @translate({ mode: 'directive', scope: 'ef-datetime-field' }) + protected t!: TranslateDirective; private interimValueState = false; // make sure that internal input field value is updated only on external value change /** @@ -270,32 +214,7 @@ export class DatetimeField extends TextField { * @returns dateSting */ protected dateToString (value: Date): string { - return isNaN(value.getTime()) ? '' : utcFormat(value, this.locale.isoFormat); - } - - /** - * Returns true if the datetime field has timepicker - * @returns hasTimePicker - */ - protected get hasTimePicker (): boolean { - // need to check for attribute to resolve the value correctly until the first lifecycle is run - return this.timepicker || this.hasAttribute('timepicker') || this.hasAmPm || this.hasSeconds; - } - - /** - * Returns true if the datetime field has seconds - * @returns hasSeconds - */ - protected get hasSeconds (): boolean { - return this.showSeconds || this.hasAttribute('show-seconds'); - } - - /** - * Returns true if the datetime field has am-pm - * @returns hasAmPm - */ - protected get hasAmPm (): boolean { - return this.amPm || this.hasAttribute('am-pm'); + return isNaN(value.getTime()) ? '' : utcFormat(value, resolvedLocale(this).isoFormat); } /** @@ -320,10 +239,6 @@ export class DatetimeField extends TextField { public willUpdate (changedProperties: PropertyValues): void { super.willUpdate(changedProperties); - if (changedProperties.has(TranslatePropertyKey)) { - this._locale = null; // Locale is updated on next call via getter - } - if (changedProperties.has(FocusedPropertyKey) && !this.focused) { this.partLabel = ''; } @@ -343,6 +258,7 @@ export class DatetimeField extends TextField { || changedProperties.has('timepicker') || changedProperties.has('showSeconds') || changedProperties.has('amPm') + || changedProperties.has('locale') || (changedProperties.has(FocusedPropertyKey) && this.value !== '' && !this.focused); } @@ -392,7 +308,7 @@ export class DatetimeField extends TextField { return true; } // value format depends on locale. - return getFormat(value) === this.locale.isoFormat; + return getFormat(value) === resolvedLocale(this).isoFormat; } /** @@ -401,33 +317,14 @@ export class DatetimeField extends TextField { * @returns {void} */ protected override warnInvalidValue (value: string): void { - new WarningNotice(`${this.localName}: the specified value "${value}" does not conform to the required format. The format is '${this.locale.isoFormat}'.`).show(); - } - - /** - * Resolve locale based on element parameters - * @returns locale Resolved locale - */ - protected resolveLocale (): Locale { - const hasTimePicker = this.hasTimePicker; - - // TODO: Do not use dateStyle and timeStyle as these are supported only in modern browsers - return Locale.fromOptions(this.formatOptions || { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: hasTimePicker ? 'numeric' : undefined, - minute: hasTimePicker ? 'numeric' : undefined, - second: this.hasSeconds ? 'numeric' : undefined, - hour12: this.hasAmPm ? true : undefined // force am-pm if provided, otherwise rely on locale - }, `${getLocale(this)}`); + new WarningNotice(`${this.localName}: the specified value "${value}" does not conform to the required format. The format is '${resolvedLocale(this).isoFormat}'.`).show(); } /** * Get Intl.DateTimeFormat object from locale */ protected get formatter (): Intl.DateTimeFormat { - return this.locale.formatter; + return resolvedLocale(this).formatter; } /** @@ -458,7 +355,7 @@ export class DatetimeField extends TextField { protected toValue (inputValue: string): string { let value = ''; try { - value = inputValue ? this.locale.parse(inputValue, this.value || this.startDate) : ''; + value = inputValue ? resolvedLocale(this).parse(inputValue, this.value || this.startDate) : ''; } catch (error) { // do nothing diff --git a/packages/elements/src/datetime-field/resolvedLocale.ts b/packages/elements/src/datetime-field/resolvedLocale.ts new file mode 100644 index 0000000000..68291c87fe --- /dev/null +++ b/packages/elements/src/datetime-field/resolvedLocale.ts @@ -0,0 +1,118 @@ +import { Locale } from '@refinitiv-ui/utils/date.js'; +import { getLocale } from '@refinitiv-ui/translate'; + +const LocaleMap = new WeakMap(); + +/** + * Used for date elements to construct Locale object + */ +type LocaleDateElement = HTMLElement & { + formatOptions: Intl.DateTimeFormatOptions | null; + amPm: boolean; + showSeconds: boolean; + timepicker: boolean; + locale: Locale | null; +}; + +/** + * Returns true if the datetime field has seconds + * @param element Locale Date element + * @returns hasSeconds + */ +const hasSeconds = (element: LocaleDateElement): boolean => { + return element.showSeconds || element.hasAttribute('show-seconds'); +}; + +/** + * Returns true if the datetime field has am-pm + * @param element Locale Date element + * @returns hasAmPm + */ +const hasAmPm = (element: LocaleDateElement): boolean => { + return element.amPm || element.hasAttribute('am-pm'); +}; + +/** + * Returns true if the datetime field has timepicker + * @param element Locale Date element + * @returns hasTimepicker + */ +const hasTimepicker = (element: LocaleDateElement): boolean => { + // need to check for attribute to resolve the value correctly until the first lifecycle is run + return element.timepicker || hasAmPm(element) || hasSeconds(element) || element.hasAttribute('timepicker'); +}; + +/** + * Resolve locale based on element parameters + * @param lang Resolved language (locale) + * @param formatOptions Format options + * @param timepicker Has time info + * @param amPm Has amPm info + * @param showSeconds Has seconds info + * @returns locale Resolved locale + */ +const resolveLocaleFromElement = (lang: string, formatOptions: Intl.DateTimeFormatOptions | null, timepicker: boolean, amPm: boolean, showSeconds: boolean): Locale => { + // TODO: Do not use dateStyle and timeStyle as these are supported only in modern browsers + return Locale.fromOptions(formatOptions || { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: timepicker ? 'numeric' : undefined, + minute: timepicker ? 'numeric' : undefined, + second: showSeconds ? 'numeric' : undefined, + hour12: amPm ? true : undefined // force am-pm if provided, otherwise rely on locale + }, lang); +}; + +/** + * Resolve locale based on element parameters + * @param element Locale Date element + * @returns locale Resolved Locale object + */ +const resolvedLocale = (element: LocaleDateElement): Locale => { + const localeMap = LocaleMap.get(element); + if (localeMap) { + + const { resolvedLocale, locale, formatOptions, amPm, showSeconds, timepicker, lang } = localeMap; + // calculate Diff with cache to check if the object has changed + if ((locale && locale === element.locale) || (!locale && !element.locale && lang === getLocale(element) && ( + (formatOptions && formatOptions === element.formatOptions) + || (!formatOptions && !element.formatOptions && timepicker === hasTimepicker(element) && amPm === hasAmPm(element) && showSeconds === hasSeconds(element)) + ))) { + return resolvedLocale; + } + } + + const lang = getLocale(element); + const formatOptions = element.formatOptions; + const timepicker = hasTimepicker(element); + const showSeconds = hasSeconds(element); + const amPm = hasAmPm(element); + const locale = element.locale; + const resolvedLocale = locale || resolveLocaleFromElement(lang, formatOptions, timepicker, amPm, showSeconds); + + LocaleMap.set(element, { + resolvedLocale, + locale, + formatOptions, + amPm, + timepicker, + showSeconds, + lang + }); + + return resolvedLocale; +}; + +export { + LocaleDateElement, + resolvedLocale +}; diff --git a/packages/elements/src/datetime-picker/__demo__/index.html b/packages/elements/src/datetime-picker/__demo__/index.html index b5a4b302ca..0f559aaaf2 100644 --- a/packages/elements/src/datetime-picker/__demo__/index.html +++ b/packages/elements/src/datetime-picker/__demo__/index.html @@ -89,6 +89,8 @@ Full Short Time Only + Month and Day + Year and Month

@@ -100,7 +102,6 @@ Error

- Input Trigger Disabled Input Disabled Popup Disabled

@@ -135,11 +136,8 @@ yearsDesc: dateTimePicker.yearsDesc, weekdaysOnly: dateTimePicker.weekdaysOnly, weekendsOnly: dateTimePicker.weekendsOnly, - inputTriggerDisabled: dateTimePicker.inputTriggerDisabled, inputDisabled: dateTimePicker.inputDisabled, - popupDisabled: dateTimePicker.popupDisabled, - isoFormat: dateTimePicker.locale ? dateTimePicker.locale.isoFormat : '', - formatOptions: dateTimePicker.locale ? dateTimePicker.locale.options : {} + popupDisabled: dateTimePicker.popupDisabled }; consoleEl.value = JSON.stringify(data, undefined, 4); @@ -156,7 +154,6 @@ dateFrom.value = ''; dateTo.value = ''; dateTimePicker.value = ''; - dateTimePicker.formatOptions = null; dateValue.view = ''; dateFrom.view = ''; dateTo.view = ''; @@ -230,13 +227,42 @@ dateTo.showSeconds = value; }; + const resetFormatOptions = () => { + document.getElementById('format-options').value = ''; + dateValue.formatOptions = null; + dateFrom.formatOptions = null; + dateTo.formatOptions = null; + dateTimePicker.formatOptions = null; + }; + + const resetFormatAttributes = () => { + document.getElementById('timepicker').checked = false; + document.getElementById('am-pm').checked = false; + document.getElementById('show-seconds').checked = false; + dateValue.timepicker = false; + dateFrom.timepicker = false; + dateTo.timepicker = false; + dateTimePicker.timepicker = false; + dateValue.showSeconds = false; + dateFrom.showSeconds = false; + dateTo.showSeconds = false; + dateTimePicker.showSeconds = false; + dateValue.amPm = false; + dateFrom.amPm = false; + dateTo.amPm = false; + dateTimePicker.amPm = false; + }; + document.getElementById('timepicker').addEventListener('checked-changed', ({ detail: { value } }) => { + resetFormatOptions(); setTimePicker(value); }); document.getElementById('am-pm').addEventListener('checked-changed', ({ detail: { value } }) => { + resetFormatOptions(); setAmPm(value); }); document.getElementById('show-seconds').addEventListener('checked-changed', ({ detail: { value } }) => { + resetFormatOptions(); setShowSeconds(value); }); document.getElementById('lang').addEventListener('value-changed', ({ detail: { value } }) => { @@ -273,9 +299,26 @@ second: '2-digit' }; break; + case 'month-day': + formatOptions = { + month: 'long', + day: 'numeric' + }; + break; + case 'year-month': + formatOptions = { + year: 'numeric', + month: 'long' + }; + break; // no default } - dateTimePicker.value = ''; + resetValue(); + resetFormatAttributes(); + + dateValue.formatOptions = formatOptions; + dateFrom.formatOptions = formatOptions; + dateTo.formatOptions = formatOptions; dateTimePicker.formatOptions = formatOptions; }); document.getElementById('placeholder').addEventListener('value-changed', ({ detail: { value } }) => { @@ -325,9 +368,6 @@ dateFrom.weekendsOnly = value; dateTo.weekendsOnly = value; }); - document.getElementById('inputTriggerDisabled').addEventListener('checked-changed', ({ detail: { value } }) => { - dateTimePicker.inputTriggerDisabled = value || undefined; - }); document.getElementById('disabled').addEventListener('checked-changed', ({ detail: { value } }) => { dateTimePicker.disabled = value || undefined; }); diff --git a/packages/elements/src/datetime-picker/index.ts b/packages/elements/src/datetime-picker/index.ts index 350a6884a1..fe188a97de 100644 --- a/packages/elements/src/datetime-picker/index.ts +++ b/packages/elements/src/datetime-picker/index.ts @@ -12,8 +12,9 @@ import { } from '@refinitiv-ui/core'; import { customElement } from '@refinitiv-ui/core/decorators/custom-element.js'; import { property } from '@refinitiv-ui/core/decorators/property.js'; -import { query } from '@refinitiv-ui/core/decorators/query.js'; +import { ref, createRef, Ref } from '@refinitiv-ui/core/directives/ref.js'; import { ifDefined } from '@refinitiv-ui/core/directives/if-defined.js'; +import { live } from '@refinitiv-ui/core/directives/live.js'; import { VERSION } from '../version.js'; import type { OpenedChangedEvent, ViewChangedEvent, ValueChangedEvent, ErrorChangedEvent } from '../events'; import type { @@ -25,7 +26,6 @@ import '../icon/index.js'; import '../overlay/index.js'; import '../datetime-field/index.js'; import '../time-picker/index.js'; -import type { Icon } from '../icon'; import type { Calendar } from '../calendar'; import { translate, @@ -55,8 +55,8 @@ import { import { preload } from '../icon/index.js'; import type { TimePicker } from '../time-picker'; -import type { Overlay } from '../overlay'; import type { DatetimeField } from '../datetime-field'; +import { resolvedLocale } from '../datetime-field/resolvedLocale.js'; preload('calendar', 'down', 'left', 'right'); /* preload calendar icons for faster loading */ @@ -132,16 +132,14 @@ export class DatetimePicker extends ControlElement implements MultiValue { flex: 1; width: auto; height: auto; - padding: 0; - margin: 0; } [part=calendar-wrapper] { display: inline-flex; } - [part=icon] { + [part=button] { cursor: pointer; } - :host([popup-disabled]) [part=icon], :host([readonly]) [part=icon] { + :host([popup-disabled]) [part=button], :host([readonly]) [part=button] { pointer-events: none; } `; @@ -165,6 +163,40 @@ export class DatetimePicker extends ControlElement implements MultiValue { @property({ type: String, reflect: true }) public max: string | null = null; + /** + * Toggle to display the time picker + */ + @property({ type: Boolean, reflect: true }) + public timepicker = false; + + /** + * Toggle to display the seconds + */ + @property({ type: Boolean, attribute: 'show-seconds', reflect: true }) + public showSeconds = false; + + /** + * Overrides 12hr time display format + */ + @property({ type: Boolean, attribute: 'am-pm', reflect: true }) + public amPm = false; + + /** + * Set the datetime format options based on + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat + * `formatOptions` overrides `timepicker` and `showSeconds` properties. + * Note: time-zone is not supported + */ + @property({ attribute: false }) + public formatOptions: Intl.DateTimeFormatOptions | null = null; + + /** + * Set the Locale object. + * `Locale` overrides `formatOptions`, `timepicker` and `showSeconds` properties. + */ + @property({ attribute: false }) + public locale: Locale | null = null; + /** * Only enable weekdays */ @@ -243,28 +275,13 @@ export class DatetimePicker extends ControlElement implements MultiValue { }) public set values (values: string[]) { const oldValues = this._values; - if (String(oldValues) !== String(values)) { - this._values = values; - this.requestUpdate('values', oldValues); - } + this._values = values; + this.requestUpdate('values', oldValues); } public get values (): string[] { return this._values; } - /** - * Toggles 12hr time display - */ - @property({ type: Boolean, attribute: 'am-pm', reflect: true }) - public amPm = false; - - /** - * Flag to show seconds time segment in display. - * Seconds are automatically shown when `hh:mm:ss` time format is provided as a value. - */ - @property({ type: Boolean, attribute: 'show-seconds', reflect: true }) - public showSeconds = false; - /** * Set placeholder text */ @@ -289,13 +306,6 @@ export class DatetimePicker extends ControlElement implements MultiValue { @property({ type: Boolean, reflect: true }) public warning = false; - /** - * Only open picker panel when calendar icon is clicked. - * Clicking on the input will no longer open the picker. - */ - @property({ type: Boolean, attribute: 'input-trigger-disabled' }) - public inputTriggerDisabled = false; - /** * Disable input part of the picker */ @@ -308,23 +318,6 @@ export class DatetimePicker extends ControlElement implements MultiValue { @property({ type: Boolean, attribute: 'popup-disabled', reflect: true }) public popupDisabled = false; - /** - * Set the datetime format options based on - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat - * `formatOptions` overrides `timepicker` and `showSeconds` properties. - * Note: time-zone is not supported - * @param formatOptions Format options - * @default - null - */ - @property({ attribute: false, type: Object }) - public formatOptions: Intl.DateTimeFormatOptions | null = null; - - /** - * Toggle to display the time picker - */ - @property({ type: Boolean, reflect: true }) - public timepicker = false; - /** * Display two calendar pickers. * @type {"" | "consecutive" | "split"} @@ -390,8 +383,8 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @returns true if input is valid */ public checkValidity (): boolean { - return (this.inputEl ? this.inputEl.checkValidity() : true) - && (this.inputToEl ? this.inputToEl.checkValidity() : true) + return (this.inputRef.value ? this.inputRef.value.checkValidity() : true) + && (this.inputToRef.value ? this.inputToRef.value.checkValidity() : true) && this.isFromBeforeTo(); } @@ -410,56 +403,63 @@ export class DatetimePicker extends ControlElement implements MultiValue { */ @translate({ mode: 'directive', scope: 'ef-datetime-picker' }) protected t!: TranslateDirective; - @query('[part=icon]', true) private iconEl!: Icon; - @query('[part=list]') private popupEl?: Overlay | null; - @query('#timepicker') private timepickerEl?: TimePicker | null; - @query('#timepicker-to') private timepickerToEl?: TimePicker | null; - @query('#calendar') private calendarEl?: Calendar | null; - @query('#calendar-to') private calendarToEl?: Calendar | null; - @query('#input') private inputEl?: DatetimeField | null; - @query('#input-to') private inputToEl?: DatetimeField | null; + private timepickerRef: Ref = createRef(); + private timepickerToRef: Ref = createRef(); + private calendarRef: Ref = createRef(); + private calendarToRef: Ref = createRef(); + private inputRef: Ref = createRef(); + private inputToRef: Ref = createRef(); /** - * @ignore - * TODO: needs more elegant solution - * Format, which is based on locale + * Returns true if Locale has time picker */ - public get locale (): Locale { - return this.inputEl ? this.inputEl.locale : Locale.fromOptions({}); + protected get hasTimePicker (): boolean { + return hasTimePicker(resolvedLocale(this).options); } /** - * Returns true if the datetime field has timepicker - * @returns hasTimePicker + * Returns true if Locale has seconds */ - protected get hasTimePicker (): boolean { - return hasTimePicker(this.locale.options); - } - protected get hasSeconds (): boolean { - return hasSeconds(this.locale.options); + return hasSeconds(resolvedLocale(this).options); } /** - * Returns true if the datetime field has timepicker - * @returns hasTimePicker + * Returns true if Locale has date picker */ protected get hasDatePicker (): boolean { - return hasDatePicker(this.locale.options); + return hasDatePicker(resolvedLocale(this).options); } + /** + * Returns true if Locale has 12h time format + */ protected get hasAmPm (): boolean { - return hasAmPm(this.locale.options); + return hasAmPm(resolvedLocale(this).options); } /** - * Called after the component is first rendered + * Called after render life-cycle finished * @param changedProperties Properties which have changed + * @return {void} + */ + protected updated (changedProperties: PropertyValues): void { + super.updated(changedProperties); + + // When the value is set externally it must override input values. + // Do force value update + if (changedProperties.has('values')) { + this.syncInputValues(); + } + } + + /** + * Force synchronise input values with picker values * @returns {void} */ - protected firstUpdated (changedProperties: PropertyValues): void { - super.firstUpdated(changedProperties); - this.addEventListener('keydown', this.onKeyDown); + protected syncInputValues (): void { + this.inputRef.value && (this.inputRef.value.value = this.values[0] || ''); + this.inputToRef.value && (this.inputToRef.value.value = this.values[1] || ''); } /** @@ -467,7 +467,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @param changedProperties Properties that has changed * @returns {void} */ - public willUpdate (changedProperties: PropertyValues): void { + protected willUpdate (changedProperties: PropertyValues): void { super.willUpdate(changedProperties); if (changedProperties.has('opened') && this.opened) { @@ -616,8 +616,11 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @returns {void} */ private notifyValuesChange (values: string[]): void { - if (this.values.toString() !== values.toString()) { - this.values = values; + const oldValues = this.values; + if (oldValues.toString() !== values.toString()) { + // Silently set values, as in this case the value of inputs must not be updated + this._values = values; + this.requestUpdate('_values', oldValues); this.notifyPropertyChange('value', this.value); } } @@ -634,28 +637,6 @@ export class DatetimePicker extends ControlElement implements MultiValue { } } - /** - * Handles key input on datetime picker - * @param event Key down event object - * @returns {void} - */ - private onKeyDown (event: KeyboardEvent): void { - switch (event.key) { - case 'Down': - case 'ArrowDown': - // this.setOpened(true); - break; - case 'Up': - case 'ArrowUp': - // !event.defaultPrevented && this.setOpened(false); - break; - default: - return; - } - - // event.preventDefault(); - } - /** * Handles key input on calendar picker * @param event Key down event object @@ -700,7 +681,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @returns {void} */ private onCalendarViewChanged (event: ViewChangedEvent): void { - const index = event.target === this.calendarToEl ? 1 : 0; /* 0 - from, single; 1 - to */ + const index = event.target === this.calendarToRef.value ? 1 : 0; /* 0 - from, single; 1 - to */ const view = event.detail.value; this.notifyViewsChange(this.composeViews(view, index)); } @@ -712,16 +693,14 @@ export class DatetimePicker extends ControlElement implements MultiValue { */ private onCalendarValueChanged (event: ValueChangedEvent): void { const values = (event.target as Calendar).values; - this.synchroniseCalendarValues(values); - + void this.synchroniseCalendarValues(values); // in duplex mode, avoid jumping on views // Therefore if any of values have changed, save the current view - if (this.isDuplex() && this.calendarEl && this.calendarToEl) { - this.notifyViewsChange([this.calendarEl?.view, this.calendarToEl?.view]); + if (this.isDuplex() && this.calendarRef.value && this.calendarToRef.value) { + this.notifyViewsChange([this.calendarRef.value.view, this.calendarToRef.value.view]); } - // Close popup if there is no time picker const newValues = this.values; if (!this.timepicker && newValues[0] && (this.range ? newValues[1] : true)) { @@ -736,18 +715,27 @@ export class DatetimePicker extends ControlElement implements MultiValue { */ private onTimePickerValueChanged (event: ValueChangedEvent): void { const target = event.target as TimePicker; - const index = target === this.timepickerToEl ? 1 : 0; /* 0 - from, single; 1 - to */ - const values = [...this._values]; + const index = target === this.timepickerToRef.value ? 1 : 0; /* 0 - from, single; 1 - to */ + const values = [...this.values]; values[index] = target.value; - this.synchroniseCalendarValues(values); + void this.synchroniseCalendarValues(values); } - private synchroniseCalendarValues (values: string[]): void { + /** + * Make sure that calendar and time-picker values + * are merged together + * @param values New values + * @returns {void} + */ + private async synchroniseCalendarValues (values: string[]): Promise { const segments = values.map(value => value ? toSegment(value) : null); - const oldSegments = this._values.map(value => value ? toSegment(value) : null); - const newValues = segments.map((segment, idx) => segment ? format(Object.assign(getCurrentSegment(), oldSegments[idx] || {}, segment), this.locale.isoFormat) : ''); + const oldSegments = this.values.map(value => value ? toSegment(value) : null); + const newValues = segments.map((segment, idx) => segment ? format(Object.assign(getCurrentSegment(), oldSegments[idx] || {}, segment), resolvedLocale(this).isoFormat) : ''); this.notifyValuesChange(newValues); + // this.syncInputValues(); + + await this.updateComplete; this.resetError(); } @@ -768,16 +756,11 @@ export class DatetimePicker extends ControlElement implements MultiValue { */ private onInputValueChanged (event: ValueChangedEvent): void { const target = event.target as DatetimeField; - const index = target === this.inputToEl ? 1 : 0; /* 0 - from, single; 1 - to */ - const newValues = [...this._values]; + const index = target === this.inputToRef.value ? 1 : 0; /* 0 - from, single; 1 - to */ + const newValues = [...this.values]; newValues[index] = target.value; - // Set values silently, because the input already has the value - if (this._values.toString() !== newValues.toString()) { - this._values = newValues; - this.notifyPropertyChange('value', this.value); - } - + this.notifyValuesChange(newValues); this.resetError(); } @@ -813,30 +796,28 @@ export class DatetimePicker extends ControlElement implements MultiValue { /** * Get time picker template - * @param id Timepicker identifier - * @param value Time picker value + * @param [isTo=false] True for range to template * @returns template result */ - private getTimepickerTemplate (id: 'timepicker' | 'timepicker-to', value = ''): TemplateResult { + private getTimepickerTemplate (isTo = false): TemplateResult { return html``; } /** * Get calendar template - * @param id Calendar identifier - * @param view Calendar view + * @param [isTo=false] True for range to template * @returns template result */ - private getCalendarTemplate (id: 'calendar' | 'calendar-to', view = ''): TemplateResult { + private getCalendarTemplate (isTo = false): TemplateResult { return html` formatToDate(value))} .filter=${this.filter} - .view=${view} + .view=${isTo ? (this.views[1] || '') : (this.views[0] || '')} @view-changed=${this.onCalendarViewChanged} @value-changed=${this.onCalendarValueChanged}>`; } @@ -858,8 +839,8 @@ export class DatetimePicker extends ControlElement implements MultiValue { */ private get calendarsTemplate (): TemplateResult { return html` - ${this.getCalendarTemplate('calendar', this.views[0])} - ${this.isDuplex() ? this.getCalendarTemplate('calendar-to', this.views[1]) : undefined} + ${this.getCalendarTemplate()} + ${this.isDuplex() ? this.getCalendarTemplate(true) : undefined} `; } @@ -869,35 +850,31 @@ export class DatetimePicker extends ControlElement implements MultiValue { private get timepickersTemplate (): TemplateResult { // TODO: how can we add support timepicker with multiple? return html` - ${this.getTimepickerTemplate('timepicker', this.values[0])} - ${this.range ? html`
` : undefined} - ${this.range ? this.getTimepickerTemplate('timepicker-to', this.values[1]) : undefined} + ${this.getTimepickerTemplate()} + ${this.range + ? html`
${this.getTimepickerTemplate(true)}` + : undefined} `; } /** * Get input template - * @param id Input identifier - * @param value Input value + * @param [isTo=false] True for range to template * @returns template result */ - private getInputTemplate (id: 'input' | 'input-to', value = ''): TemplateResult { + private getInputTemplate (isTo = false): TemplateResult { return html` `; } @@ -907,16 +884,14 @@ export class DatetimePicker extends ControlElement implements MultiValue { */ private get iconTemplate (): TemplateResult { return html` - +
+ +
`; } @@ -927,9 +902,10 @@ export class DatetimePicker extends ControlElement implements MultiValue { private get inputTemplates (): TemplateResult { return html`
- ${this.getInputTemplate('input', this.values[0] || '')} - ${this.range ? html`
` : undefined} - ${this.range ? this.getInputTemplate('input-to', this.values[1] || '') : undefined} + ${this.getInputTemplate()} + ${this.range + ? html`
${this.getInputTemplate(true)}` + : undefined}
`; } @@ -940,6 +916,8 @@ export class DatetimePicker extends ControlElement implements MultiValue { private get popupTemplate (): TemplateResult | undefined { if (this.lazyRendered) { return html` Date: Fri, 29 Jul 2022 11:01:44 +0300 Subject: [PATCH 05/21] fix(datetime-picker): setting incorrect value throws an error feat(datetime-picker): add accessibility support feat(phrasebook): add datetime-picker strings --- .../custom-elements/ef-datetime-picker.less | 6 ++ .../src/datetime-picker/__demo__/index.html | 2 +- .../elements/src/datetime-picker/index.ts | 94 ++++++++++++++++--- packages/phrasebook/package.json | 7 +- .../src/locale/de/datetime-picker.ts | 20 ++++ .../src/locale/en/datetime-picker.ts | 20 ++++ .../src/locale/ja/datetime-picker.ts | 20 ++++ .../src/locale/zh-hant/datetime-picker.ts | 20 ++++ .../src/locale/zh/datetime-picker.ts | 20 ++++ 9 files changed, 192 insertions(+), 17 deletions(-) create mode 100644 packages/phrasebook/src/locale/de/datetime-picker.ts create mode 100644 packages/phrasebook/src/locale/en/datetime-picker.ts create mode 100644 packages/phrasebook/src/locale/ja/datetime-picker.ts create mode 100644 packages/phrasebook/src/locale/zh-hant/datetime-picker.ts create mode 100644 packages/phrasebook/src/locale/zh/datetime-picker.ts diff --git a/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less b/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less index 371e7f00de..96c6ea0b99 100644 --- a/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less +++ b/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less @@ -41,6 +41,12 @@ justify-content: center; align-items: center; flex: none; + padding: 0; + margin: 0; + border: 0; + background: none; + color: inherit; + font-size: inherit; } [part=calendar] { diff --git a/packages/elements/src/datetime-picker/__demo__/index.html b/packages/elements/src/datetime-picker/__demo__/index.html index 0f559aaaf2..875580cb57 100644 --- a/packages/elements/src/datetime-picker/__demo__/index.html +++ b/packages/elements/src/datetime-picker/__demo__/index.html @@ -32,7 +32,7 @@

- +

diff --git a/packages/elements/src/datetime-picker/index.ts b/packages/elements/src/datetime-picker/index.ts index fe188a97de..26dedb0f62 100644 --- a/packages/elements/src/datetime-picker/index.ts +++ b/packages/elements/src/datetime-picker/index.ts @@ -39,7 +39,9 @@ import { format, toSegment, Locale, - DateFormat + DateFormat, + getFormat, + utcParse } from '@refinitiv-ui/utils/date.js'; import { @@ -57,6 +59,7 @@ import { preload } from '../icon/index.js'; import type { TimePicker } from '../time-picker'; import type { DatetimeField } from '../datetime-field'; import { resolvedLocale } from '../datetime-field/resolvedLocale.js'; +import '@refinitiv-ui/phrasebook/locale/en/datetime-picker.js'; preload('calendar', 'down', 'left', 'right'); /* preload calendar icons for faster loading */ @@ -275,7 +278,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { }) public set values (values: string[]) { const oldValues = this._values; - this._values = values; + this._values = this.filterAndWarnInvalidValues(values); this.requestUpdate('values', oldValues); } public get values (): string[] { @@ -529,6 +532,40 @@ export class DatetimePicker extends ControlElement implements MultiValue { return true; } + /** + * A helper method to make sure that only valid values are passed + * Warn if passed value is invalid + * @param values Values to check + * @returns Filtered collection of values + */ + private filterAndWarnInvalidValues (values: string[]): string[] { + return values.map(value => { + if (this.isValidValue(value)) { + return value; + } + this.warnInvalidValue(value); + return ''; + }); + } + + /** + * Show invalid value message + * @param value Invalid value + * @returns {void} + */ + protected override warnInvalidValue (value: string): void { + new WarningNotice(`The specified value "${value}" does not conform to the required format. The format is ${resolvedLocale(this).isoFormat}.`).once(); + } + + /** + * Check if passed value is valid + * @param value Value + * @returns valid Validity + */ + protected isValidValue (value: string): boolean { + return value === '' ? true : getFormat(value) === resolvedLocale(this).isoFormat; + } + /** * Return true if calendar is in duplex mode * @returns duplex @@ -661,7 +698,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @param event Tap event * @returns {void} */ - private onIconTap (event: TapEvent): void { + private onButtonTap (event: TapEvent): void { this.setOpened(true); } @@ -880,18 +917,35 @@ export class DatetimePicker extends ControlElement implements MultiValue { } /** - * Template for rendering an icon + * Template for rendering a button */ - private get iconTemplate (): TemplateResult { + private get buttonTemplate (): TemplateResult { + const formatter = resolvedLocale(this).formatter; + const values = this.values; + const from = values[0] ? formatter.format(utcParse(values[0])) : ''; + const to = values[1] ? formatter.format(utcParse(values[1])) : ''; + const hasValue = !!from || !!to; + return html` -
+
+ `; } @@ -915,9 +969,19 @@ export class DatetimePicker extends ControlElement implements MultiValue { */ private get popupTemplate (): TemplateResult | undefined { if (this.lazyRendered) { + const hasTime = this.hasTimePicker; + const hasDate = this.hasDatePicker; + return html`
- ${this.hasDatePicker ? html`
${this.calendarsTemplate}
` : undefined} - ${this.hasTimePicker ? html`
${this.timepickersTemplate}
` : undefined} + ${hasDate ? html`
${this.calendarsTemplate}
` : undefined} + ${hasTime ? html`
${this.timepickersTemplate}
` : undefined}
@@ -949,7 +1013,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { protected render (): TemplateResult { return html` ${this.inputTemplates} - ${this.iconTemplate} + ${this.buttonTemplate} ${this.popupTemplate} `; } diff --git a/packages/phrasebook/package.json b/packages/phrasebook/package.json index f954a8db7f..1cb712b036 100644 --- a/packages/phrasebook/package.json +++ b/packages/phrasebook/package.json @@ -28,6 +28,7 @@ "./locale/de/color-dialog.js": "./lib/locale/de/color-dialog.js", "./locale/de/combo-box.js": "./lib/locale/de/combo-box.js", "./locale/de/datetime-field.js": "./lib/locale/de/datetime-field.js", + "./locale/de/datetime-picker.js": "./lib/locale/de/datetime-picker.js", "./locale/de/dialog.js": "./lib/locale/de/dialog.js", "./locale/de/pagination.js": "./lib/locale/de/pagination.js", "./locale/de/password-field.js": "./lib/locale/de/password-field.js", @@ -45,6 +46,7 @@ "./locale/en/color-dialog.js": "./lib/locale/en/color-dialog.js", "./locale/en/combo-box.js": "./lib/locale/en/combo-box.js", "./locale/en/datetime-field.js": "./lib/locale/en/datetime-field.js", + "./locale/en/datetime-picker.js": "./lib/locale/en/datetime-picker.js", "./locale/en/dialog.js": "./lib/locale/en/dialog.js", "./locale/en/pagination.js": "./lib/locale/en/pagination.js", "./locale/en/password-field.js": "./lib/locale/en/password-field.js", @@ -62,6 +64,7 @@ "./locale/ja/color-dialog.js": "./lib/locale/ja/color-dialog.js", "./locale/ja/combo-box.js": "./lib/locale/ja/combo-box.js", "./locale/ja/datetime-field.js": "./lib/locale/ja/datetime-field.js", + "./locale/ja/datetime-picker.js": "./lib/locale/ja/datetime-picker.js", "./locale/ja/dialog.js": "./lib/locale/ja/dialog.js", "./locale/ja/pagination.js": "./lib/locale/ja/pagination.js", "./locale/ja/password-field.js": "./lib/locale/ja/password-field.js", @@ -79,6 +82,7 @@ "./locale/zh/color-dialog.js": "./lib/locale/zh/color-dialog.js", "./locale/zh/combo-box.js": "./lib/locale/zh/combo-box.js", "./locale/zh/datetime-field.js": "./lib/locale/zh/datetime-field.js", + "./locale/zh/datetime-picker.js": "./lib/locale/zh/datetime-picker.js", "./locale/zh/dialog.js": "./lib/locale/zh/dialog.js", "./locale/zh/pagination.js": "./lib/locale/zh/pagination.js", "./locale/zh/password-field.js": "./lib/locale/zh/password-field.js", @@ -96,6 +100,7 @@ "./locale/zh-hant/color-dialog.js": "./lib/locale/zh-hant/color-dialog.js", "./locale/zh-hant/combo-box.js": "./lib/locale/zh-hant/combo-box.js", "./locale/zh-hant/datetime-field.js": "./lib/locale/zh-hant/datetime-field.js", + "./locale/zh-hant/datetime-picker.js": "./lib/locale/zh-hant/datetime-picker.js", "./locale/zh-hant/dialog.js": "./lib/locale/zh-hant/dialog.js", "./locale/zh-hant/pagination.js": "./lib/locale/zh-hant/pagination.js", "./locale/zh-hant/password-field.js": "./lib/locale/zh-hant/password-field.js", @@ -128,4 +133,4 @@ "dependencies": { "tslib": "^2.3.1" } -} \ No newline at end of file +} diff --git a/packages/phrasebook/src/locale/de/datetime-picker.ts b/packages/phrasebook/src/locale/de/datetime-picker.ts new file mode 100644 index 0000000000..6fd8c8d9fa --- /dev/null +++ b/packages/phrasebook/src/locale/de/datetime-picker.ts @@ -0,0 +1,20 @@ +import { Phrasebook } from '../../translation.js'; + +const translations = { + CHOOSE_DATE: 'Choose date', + CHOOSE_DATE_TIME: 'Choose date and time', + CHOOSE_TIME: 'Choose time', + CHANGE_DATE: 'Change date: {from}', + CHANGE_DATE_TIME: 'Change date and time: {from}', + CHANGE_TIME: 'Change time: {from}', + CHOOSE_DATE_RANGE: 'Choose date range', + CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', + CHOOSE_TIME_RANGE: 'Choose time range', + CHANGE_DATE_RANGE: 'Change date range: from {from} to {to}', + CHANGE_DATE_TIME_RANGE: 'Change date and time range: from {from} to {to}', + CHANGE_TIME_RANGE: 'Change time range: from {from} to {to}' +}; + +Phrasebook.define('de', 'ef-datetime-picker', translations); + +export default translations; diff --git a/packages/phrasebook/src/locale/en/datetime-picker.ts b/packages/phrasebook/src/locale/en/datetime-picker.ts new file mode 100644 index 0000000000..c13175b49d --- /dev/null +++ b/packages/phrasebook/src/locale/en/datetime-picker.ts @@ -0,0 +1,20 @@ +import { Phrasebook } from '../../translation.js'; + +const translations = { + CHOOSE_DATE: 'Choose date', + CHOOSE_DATE_TIME: 'Choose date and time', + CHOOSE_TIME: 'Choose time', + CHANGE_DATE: 'Change date: {from}', + CHANGE_DATE_TIME: 'Change date and time: {from}', + CHANGE_TIME: 'Change time: {from}', + CHOOSE_DATE_RANGE: 'Choose date range', + CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', + CHOOSE_TIME_RANGE: 'Choose time range', + CHANGE_DATE_RANGE: 'Change date range: from {from} to {to}', + CHANGE_DATE_TIME_RANGE: 'Change date and time range: from {from} to {to}', + CHANGE_TIME_RANGE: 'Change time range: from {from} to {to}' +}; + +Phrasebook.define('en', 'ef-datetime-picker', translations); + +export default translations; diff --git a/packages/phrasebook/src/locale/ja/datetime-picker.ts b/packages/phrasebook/src/locale/ja/datetime-picker.ts new file mode 100644 index 0000000000..fa5dfe2b4b --- /dev/null +++ b/packages/phrasebook/src/locale/ja/datetime-picker.ts @@ -0,0 +1,20 @@ +import { Phrasebook } from '../../translation.js'; + +const translations = { + CHOOSE_DATE: 'Choose date', + CHOOSE_DATE_TIME: 'Choose date and time', + CHOOSE_TIME: 'Choose time', + CHANGE_DATE: 'Change date: {from}', + CHANGE_DATE_TIME: 'Change date and time: {from}', + CHANGE_TIME: 'Change time: {from}', + CHOOSE_DATE_RANGE: 'Choose date range', + CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', + CHOOSE_TIME_RANGE: 'Choose time range', + CHANGE_DATE_RANGE: 'Change date range: from {from} to {to}', + CHANGE_DATE_TIME_RANGE: 'Change date and time range: from {from} to {to}', + CHANGE_TIME_RANGE: 'Change time range: from {from} to {to}' +}; + +Phrasebook.define('ja', 'ef-datetime-picker', translations); + +export default translations; diff --git a/packages/phrasebook/src/locale/zh-hant/datetime-picker.ts b/packages/phrasebook/src/locale/zh-hant/datetime-picker.ts new file mode 100644 index 0000000000..8aa3527ce4 --- /dev/null +++ b/packages/phrasebook/src/locale/zh-hant/datetime-picker.ts @@ -0,0 +1,20 @@ +import { Phrasebook } from '../../translation.js'; + +const translations = { + CHOOSE_DATE: 'Choose date', + CHOOSE_DATE_TIME: 'Choose date and time', + CHOOSE_TIME: 'Choose time', + CHANGE_DATE: 'Change date: {from}', + CHANGE_DATE_TIME: 'Change date and time: {from}', + CHANGE_TIME: 'Change time: {from}', + CHOOSE_DATE_RANGE: 'Choose date range', + CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', + CHOOSE_TIME_RANGE: 'Choose time range', + CHANGE_DATE_RANGE: 'Change date range: from {from} to {to}', + CHANGE_DATE_TIME_RANGE: 'Change date and time range: from {from} to {to}', + CHANGE_TIME_RANGE: 'Change time range: from {from} to {to}' +}; + +Phrasebook.define('zh-Hant', 'ef-datetime-picker', translations); + +export default translations; diff --git a/packages/phrasebook/src/locale/zh/datetime-picker.ts b/packages/phrasebook/src/locale/zh/datetime-picker.ts new file mode 100644 index 0000000000..5782522d3e --- /dev/null +++ b/packages/phrasebook/src/locale/zh/datetime-picker.ts @@ -0,0 +1,20 @@ +import { Phrasebook } from '../../translation.js'; + +const translations = { + CHOOSE_DATE: 'Choose date', + CHOOSE_DATE_TIME: 'Choose date and time', + CHOOSE_TIME: 'Choose time', + CHANGE_DATE: 'Change date: {from}', + CHANGE_DATE_TIME: 'Change date and time: {from}', + CHANGE_TIME: 'Change time: {from}', + CHOOSE_DATE_RANGE: 'Choose date range', + CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', + CHOOSE_TIME_RANGE: 'Choose time range', + CHANGE_DATE_RANGE: 'Change date range: from {from} to {to}', + CHANGE_DATE_TIME_RANGE: 'Change date and time range: from {from} to {to}', + CHANGE_TIME_RANGE: 'Change time range: from {from} to {to}' +}; + +Phrasebook.define('zh', 'ef-datetime-picker', translations); + +export default translations; From 1e59bd217787c3274f059bcdc1634d02c5ea8d9b Mon Sep 17 00:00:00 2001 From: AG <81616437+goremikins@users.noreply.github.com> Date: Tue, 2 Aug 2022 11:54:40 +0300 Subject: [PATCH 06/21] fix(datetime-picker): filter out invalid type values --- packages/elements/src/datetime-picker/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/elements/src/datetime-picker/index.ts b/packages/elements/src/datetime-picker/index.ts index 26dedb0f62..893fee1317 100644 --- a/packages/elements/src/datetime-picker/index.ts +++ b/packages/elements/src/datetime-picker/index.ts @@ -256,7 +256,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { */ @property({ type: String }) public set value (value: string) { - this.values = value ? [value] : []; + this.values = value === '' ? [] : [value]; } public get value (): string { return this.values[0] || ''; @@ -563,7 +563,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @returns valid Validity */ protected isValidValue (value: string): boolean { - return value === '' ? true : getFormat(value) === resolvedLocale(this).isoFormat; + return value === '' ? true : typeof value === 'string' && getFormat(value) === resolvedLocale(this).isoFormat; } /** @@ -770,7 +770,6 @@ export class DatetimePicker extends ControlElement implements MultiValue { const newValues = segments.map((segment, idx) => segment ? format(Object.assign(getCurrentSegment(), oldSegments[idx] || {}, segment), resolvedLocale(this).isoFormat) : ''); this.notifyValuesChange(newValues); - // this.syncInputValues(); await this.updateComplete; this.resetError(); From 48a3eac2cc060a746bcedb36ba38412e65785e34 Mon Sep 17 00:00:00 2001 From: AG <81616437+goremikins@users.noreply.github.com> Date: Tue, 2 Aug 2022 15:51:39 +0300 Subject: [PATCH 07/21] fix(datetime-picker): remove unused arguments fix(datetime-picker): popup is not closed on all scenarios refactor(datetime-picker): simplify translation logic refactor(datetime-picker): simplify closing logic --- .../elements/src/datetime-picker/index.ts | 51 ++++--------------- .../src/locale/de/datetime-picker.ts | 9 ++-- .../src/locale/en/datetime-picker.ts | 9 ++-- .../src/locale/ja/datetime-picker.ts | 9 ++-- .../src/locale/zh-hant/datetime-picker.ts | 9 ++-- .../src/locale/zh/datetime-picker.ts | 9 ++-- 6 files changed, 24 insertions(+), 72 deletions(-) diff --git a/packages/elements/src/datetime-picker/index.ts b/packages/elements/src/datetime-picker/index.ts index 893fee1317..2b4d999c0c 100644 --- a/packages/elements/src/datetime-picker/index.ts +++ b/packages/elements/src/datetime-picker/index.ts @@ -6,7 +6,6 @@ import { MultiValue, PropertyValues, CSSResultGroup, - TapEvent, WarningNotice, FocusedPropertyKey } from '@refinitiv-ui/core'; @@ -674,31 +673,11 @@ export class DatetimePicker extends ControlElement implements MultiValue { } } - /** - * Handles key input on calendar picker - * @param event Key down event object - * @returns {void} - */ - private onPopupKeyDown (event: KeyboardEvent): void { - switch (event.key) { - case 'Esc': - case 'Escape': - this.resetViews(); - this.setOpened(false); - break; - default: - return; - } - - event.preventDefault(); - } - /** * Run on icon tap event - * @param event Tap event * @returns {void} */ - private onButtonTap (event: TapEvent): void { + private onButtonTap (): void { this.setOpened(true); } @@ -818,6 +797,11 @@ export class DatetimePicker extends ControlElement implements MultiValue { } if (this.opened !== opened && this.notifyPropertyChange('opened', opened, true)) { + if (!opened) { + // Reset view when calendar closes. + // On re-open it should re-focus on current dates + this.resetViews(); + } this.opened = opened; } } @@ -902,6 +886,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { return html` @@ -983,13 +951,12 @@ export class DatetimePicker extends ControlElement implements MultiValue { }`)}" part="list" with-shadow - no-cancel-on-esc-key .delegatesFocus=${true} .positionTarget=${this} + lock-position-target .position=${POPUP_POSITION} ?opened=${this.opened} @opened-changed=${this.onPopupOpenedChanged} - @keydown=${this.onPopupKeyDown}>
diff --git a/packages/phrasebook/src/locale/de/datetime-picker.ts b/packages/phrasebook/src/locale/de/datetime-picker.ts index 6fd8c8d9fa..382772782d 100644 --- a/packages/phrasebook/src/locale/de/datetime-picker.ts +++ b/packages/phrasebook/src/locale/de/datetime-picker.ts @@ -4,15 +4,12 @@ const translations = { CHOOSE_DATE: 'Choose date', CHOOSE_DATE_TIME: 'Choose date and time', CHOOSE_TIME: 'Choose time', - CHANGE_DATE: 'Change date: {from}', - CHANGE_DATE_TIME: 'Change date and time: {from}', - CHANGE_TIME: 'Change time: {from}', CHOOSE_DATE_RANGE: 'Choose date range', CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', CHOOSE_TIME_RANGE: 'Choose time range', - CHANGE_DATE_RANGE: 'Change date range: from {from} to {to}', - CHANGE_DATE_TIME_RANGE: 'Change date and time range: from {from} to {to}', - CHANGE_TIME_RANGE: 'Change time range: from {from} to {to}' + VALUE_FROM: 'From', + VALUE_TO: 'To', + OPEN_CALENDAR: 'Open calendar' }; Phrasebook.define('de', 'ef-datetime-picker', translations); diff --git a/packages/phrasebook/src/locale/en/datetime-picker.ts b/packages/phrasebook/src/locale/en/datetime-picker.ts index c13175b49d..74bd216ff4 100644 --- a/packages/phrasebook/src/locale/en/datetime-picker.ts +++ b/packages/phrasebook/src/locale/en/datetime-picker.ts @@ -4,15 +4,12 @@ const translations = { CHOOSE_DATE: 'Choose date', CHOOSE_DATE_TIME: 'Choose date and time', CHOOSE_TIME: 'Choose time', - CHANGE_DATE: 'Change date: {from}', - CHANGE_DATE_TIME: 'Change date and time: {from}', - CHANGE_TIME: 'Change time: {from}', CHOOSE_DATE_RANGE: 'Choose date range', CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', CHOOSE_TIME_RANGE: 'Choose time range', - CHANGE_DATE_RANGE: 'Change date range: from {from} to {to}', - CHANGE_DATE_TIME_RANGE: 'Change date and time range: from {from} to {to}', - CHANGE_TIME_RANGE: 'Change time range: from {from} to {to}' + VALUE_FROM: 'From', + VALUE_TO: 'To', + OPEN_CALENDAR: 'Open calendar' }; Phrasebook.define('en', 'ef-datetime-picker', translations); diff --git a/packages/phrasebook/src/locale/ja/datetime-picker.ts b/packages/phrasebook/src/locale/ja/datetime-picker.ts index fa5dfe2b4b..c63ce065bd 100644 --- a/packages/phrasebook/src/locale/ja/datetime-picker.ts +++ b/packages/phrasebook/src/locale/ja/datetime-picker.ts @@ -4,15 +4,12 @@ const translations = { CHOOSE_DATE: 'Choose date', CHOOSE_DATE_TIME: 'Choose date and time', CHOOSE_TIME: 'Choose time', - CHANGE_DATE: 'Change date: {from}', - CHANGE_DATE_TIME: 'Change date and time: {from}', - CHANGE_TIME: 'Change time: {from}', CHOOSE_DATE_RANGE: 'Choose date range', CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', CHOOSE_TIME_RANGE: 'Choose time range', - CHANGE_DATE_RANGE: 'Change date range: from {from} to {to}', - CHANGE_DATE_TIME_RANGE: 'Change date and time range: from {from} to {to}', - CHANGE_TIME_RANGE: 'Change time range: from {from} to {to}' + VALUE_FROM: 'From', + VALUE_TO: 'To', + OPEN_CALENDAR: 'Open calendar' }; Phrasebook.define('ja', 'ef-datetime-picker', translations); diff --git a/packages/phrasebook/src/locale/zh-hant/datetime-picker.ts b/packages/phrasebook/src/locale/zh-hant/datetime-picker.ts index 8aa3527ce4..442fb4418c 100644 --- a/packages/phrasebook/src/locale/zh-hant/datetime-picker.ts +++ b/packages/phrasebook/src/locale/zh-hant/datetime-picker.ts @@ -4,15 +4,12 @@ const translations = { CHOOSE_DATE: 'Choose date', CHOOSE_DATE_TIME: 'Choose date and time', CHOOSE_TIME: 'Choose time', - CHANGE_DATE: 'Change date: {from}', - CHANGE_DATE_TIME: 'Change date and time: {from}', - CHANGE_TIME: 'Change time: {from}', CHOOSE_DATE_RANGE: 'Choose date range', CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', CHOOSE_TIME_RANGE: 'Choose time range', - CHANGE_DATE_RANGE: 'Change date range: from {from} to {to}', - CHANGE_DATE_TIME_RANGE: 'Change date and time range: from {from} to {to}', - CHANGE_TIME_RANGE: 'Change time range: from {from} to {to}' + VALUE_FROM: 'From', + VALUE_TO: 'To', + OPEN_CALENDAR: 'Open calendar' }; Phrasebook.define('zh-Hant', 'ef-datetime-picker', translations); diff --git a/packages/phrasebook/src/locale/zh/datetime-picker.ts b/packages/phrasebook/src/locale/zh/datetime-picker.ts index 5782522d3e..c39129cdc5 100644 --- a/packages/phrasebook/src/locale/zh/datetime-picker.ts +++ b/packages/phrasebook/src/locale/zh/datetime-picker.ts @@ -4,15 +4,12 @@ const translations = { CHOOSE_DATE: 'Choose date', CHOOSE_DATE_TIME: 'Choose date and time', CHOOSE_TIME: 'Choose time', - CHANGE_DATE: 'Change date: {from}', - CHANGE_DATE_TIME: 'Change date and time: {from}', - CHANGE_TIME: 'Change time: {from}', CHOOSE_DATE_RANGE: 'Choose date range', CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', CHOOSE_TIME_RANGE: 'Choose time range', - CHANGE_DATE_RANGE: 'Change date range: from {from} to {to}', - CHANGE_DATE_TIME_RANGE: 'Change date and time range: from {from} to {to}', - CHANGE_TIME_RANGE: 'Change time range: from {from} to {to}' + VALUE_FROM: 'From', + VALUE_TO: 'To', + OPEN_CALENDAR: 'Open calendar' }; Phrasebook.define('zh', 'ef-datetime-picker', translations); From 99ea3d16795c37292e6d55408654524fafe71eb5 Mon Sep 17 00:00:00 2001 From: AG <81616437+goremikins@users.noreply.github.com> Date: Tue, 2 Aug 2022 16:14:57 +0300 Subject: [PATCH 08/21] fix(datetime-picker): remove unused import --- .../src/datetime-field/resolvedLocale.ts | 31 ++++++++++++++----- .../elements/src/datetime-picker/index.ts | 12 ++++--- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/packages/elements/src/datetime-field/resolvedLocale.ts b/packages/elements/src/datetime-field/resolvedLocale.ts index 68291c87fe..71c761a3b9 100644 --- a/packages/elements/src/datetime-field/resolvedLocale.ts +++ b/packages/elements/src/datetime-field/resolvedLocale.ts @@ -1,5 +1,5 @@ import { Locale } from '@refinitiv-ui/utils/date.js'; -import { getLocale } from '@refinitiv-ui/translate'; +import { getLocale as getLang } from '@refinitiv-ui/translate'; const LocaleMap = new WeakMap { +const getLocale = (element: LocaleDateElement): Locale | null => { const localeMap = LocaleMap.get(element); if (localeMap) { - const { resolvedLocale, locale, formatOptions, amPm, showSeconds, timepicker, lang } = localeMap; // calculate Diff with cache to check if the object has changed - if ((locale && locale === element.locale) || (!locale && !element.locale && lang === getLocale(element) && ( + if ((locale && locale === element.locale) || (!locale && !element.locale && lang === getLang(element) && ( (formatOptions && formatOptions === element.formatOptions) - || (!formatOptions && !element.formatOptions && timepicker === hasTimepicker(element) && amPm === hasAmPm(element) && showSeconds === hasSeconds(element)) + || (!formatOptions && !element.formatOptions && timepicker === hasTimepicker(element) && amPm === hasAmPm(element) && showSeconds === hasSeconds(element)) ))) { return resolvedLocale; } } - const lang = getLocale(element); + return null; +}; + +/** + * Set Locale object in LocaleMap cache + * @param element Locale Date element + * @returns locale Resolved Locale object + */ +const setLocale = (element: LocaleDateElement): Locale => { + const lang = getLang(element); const formatOptions = element.formatOptions; const timepicker = hasTimepicker(element); const showSeconds = hasSeconds(element); @@ -112,6 +120,13 @@ const resolvedLocale = (element: LocaleDateElement): Locale => { return resolvedLocale; }; +/** + * Resolve locale based on element parameters + * @param element Locale Date element + * @returns locale Resolved Locale object + */ +const resolvedLocale = (element: LocaleDateElement): Locale => getLocale(element) || setLocale(element); + export { LocaleDateElement, resolvedLocale diff --git a/packages/elements/src/datetime-picker/index.ts b/packages/elements/src/datetime-picker/index.ts index 2b4d999c0c..8f0c079a62 100644 --- a/packages/elements/src/datetime-picker/index.ts +++ b/packages/elements/src/datetime-picker/index.ts @@ -39,8 +39,7 @@ import { toSegment, Locale, DateFormat, - getFormat, - utcParse + getFormat } from '@refinitiv-ui/utils/date.js'; import { @@ -697,7 +696,8 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @returns {void} */ private onCalendarViewChanged (event: ViewChangedEvent): void { - const index = event.target === this.calendarToRef.value ? 1 : 0; /* 0 - from, single; 1 - to */ + // 0 - from, single; 1 - to + const index = event.target === this.calendarToRef.value ? 1 : 0; const view = event.detail.value; this.notifyViewsChange(this.composeViews(view, index)); } @@ -731,7 +731,8 @@ export class DatetimePicker extends ControlElement implements MultiValue { */ private onTimePickerValueChanged (event: ValueChangedEvent): void { const target = event.target as TimePicker; - const index = target === this.timepickerToRef.value ? 1 : 0; /* 0 - from, single; 1 - to */ + // 0 - from, single; 1 - to + const index = target === this.timepickerToRef.value ? 1 : 0; const values = [...this.values]; values[index] = target.value; void this.synchroniseCalendarValues(values); @@ -771,7 +772,8 @@ export class DatetimePicker extends ControlElement implements MultiValue { */ private onInputValueChanged (event: ValueChangedEvent): void { const target = event.target as DatetimeField; - const index = target === this.inputToRef.value ? 1 : 0; /* 0 - from, single; 1 - to */ + // 0 - from, single; 1 - to + const index = target === this.inputToRef.value ? 1 : 0; const newValues = [...this.values]; newValues[index] = target.value; From 9087025ecb4bb692d426e7ff74ad7e25f7ae30f9 Mon Sep 17 00:00:00 2001 From: AG <81616437+goremikins@users.noreply.github.com> Date: Thu, 4 Aug 2022 10:27:57 +0100 Subject: [PATCH 09/21] fix(datetime-picker): view property is not reset to empty string when set an invalid view fix(datetime-picker): a warning is shown when a value with timepicker is set --- .../elements/src/datetime-picker/index.ts | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/elements/src/datetime-picker/index.ts b/packages/elements/src/datetime-picker/index.ts index 8f0c079a62..3d4564d27c 100644 --- a/packages/elements/src/datetime-picker/index.ts +++ b/packages/elements/src/datetime-picker/index.ts @@ -351,7 +351,8 @@ export class DatetimePicker extends ControlElement implements MultiValue { @property({ attribute: false }) public set views (views: string[]) { const oldViews = this._views; - if (oldViews.toString() !== views.toString()) { + views = this.filterInvalidViews(views); + if (String(oldViews) !== String(views)) { this._views = views; this.requestUpdate('views', oldViews); } @@ -546,6 +547,31 @@ export class DatetimePicker extends ControlElement implements MultiValue { }); } + /** + * A helper method to make sure that only valid views are passed + * @param views Views to check + * @returns Filtered collection of views + */ + private filterInvalidViews (views: string[]): string[] { + const filtered = []; + + // views must match in duplex mode + if (views.length !== (this.isDuplex() ? 2 : 1)) { + return []; + } + + for (let i = 0; i < views.length && filtered.length <= 2; i += 1) { + const view = views[0]; + // cannot have empty or invalid views + if (typeof view !== 'string' || !view || getFormat(view) !== DateFormat.yyyyMM) { + return []; + } + filtered.push(view); + } + + return filtered; + } + /** * Show invalid value message * @param value Invalid value @@ -895,9 +921,9 @@ export class DatetimePicker extends ControlElement implements MultiValue { max=${ifDefined(this.max || undefined)} ?disabled=${this.disabled} ?readonly=${this.readonly || this.inputDisabled} + .locale=${resolvedLocale(this)} .value=${live(isTo ? (this.values[1] || '') : (this.values[0] || ''))} .placeholder=${this.placeholder} - .locale=${resolvedLocale(this)} @value-changed=${this.onInputValueChanged} @error-changed=${this.onInputErrorChanged}>`; } From a8a913de410b1c699324c82ad5c065548fb3b19d Mon Sep 17 00:00:00 2001 From: AG <81616437+goremikins@users.noreply.github.com> Date: Thu, 4 Aug 2022 13:53:59 +0100 Subject: [PATCH 10/21] fix(datetime-picker): no closing template fix(datetime-picker): seconds are not displayed --- packages/elements/src/datetime-picker/index.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/elements/src/datetime-picker/index.ts b/packages/elements/src/datetime-picker/index.ts index 3d4564d27c..67529a11af 100644 --- a/packages/elements/src/datetime-picker/index.ts +++ b/packages/elements/src/datetime-picker/index.ts @@ -851,9 +851,8 @@ export class DatetimePicker extends ControlElement implements MultiValue { return html``; } @@ -979,12 +978,12 @@ export class DatetimePicker extends ControlElement implements MultiValue { }`)}" part="list" with-shadow + lock-position-target .delegatesFocus=${true} .positionTarget=${this} - lock-position-target .position=${POPUP_POSITION} ?opened=${this.opened} - @opened-changed=${this.onPopupOpenedChanged} + @opened-changed=${this.onPopupOpenedChanged}>
From 3f7074561b100815a6ace0e8ca9c91e38d28bf58 Mon Sep 17 00:00:00 2001 From: AG <81616437+goremikins@users.noreply.github.com> Date: Thu, 4 Aug 2022 13:58:33 +0100 Subject: [PATCH 11/21] fix(datetime-picker): incorrect view filtering --- packages/elements/src/datetime-picker/index.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/elements/src/datetime-picker/index.ts b/packages/elements/src/datetime-picker/index.ts index 67529a11af..5f2284bf7b 100644 --- a/packages/elements/src/datetime-picker/index.ts +++ b/packages/elements/src/datetime-picker/index.ts @@ -553,23 +553,17 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @returns Filtered collection of views */ private filterInvalidViews (views: string[]): string[] { - const filtered = []; - // views must match in duplex mode if (views.length !== (this.isDuplex() ? 2 : 1)) { return []; } - for (let i = 0; i < views.length && filtered.length <= 2; i += 1) { - const view = views[0]; - // cannot have empty or invalid views - if (typeof view !== 'string' || !view || getFormat(view) !== DateFormat.yyyyMM) { - return []; - } - filtered.push(view); + // cannot have empty or invalid views + if (views.findIndex(view => typeof view !== 'string' || view === '' || getFormat(view) !== DateFormat.yyyyMM) !== -1) { + return []; } - return filtered; + return views; } /** From a1b5cdd56a1032572776bd5cd5004b45da28e372 Mon Sep 17 00:00:00 2001 From: Sarin-Udompanish <86759822+Sarin-Udompanish@users.noreply.github.com> Date: Wed, 10 Aug 2022 16:53:48 +0700 Subject: [PATCH 12/21] Feature/integrate datetime field update unit test (#424) --- package-lock.json | 162 +++---- .../{DOMStructure.md => DatetimePicker.md} | 432 ++++++++++++++---- .../__test__/datetime-picker.default.test.js | 129 +++--- .../datetime-picker.navigation.test.js | 63 +-- .../__test__/datetime-picker.snapshot.test.js | 52 --- .../__test__/datetime-picker.value.test.js | 100 ++-- .../__test__/datetime-picker.view.test.js | 36 +- .../src/datetime-picker/__test__/utils.js | 39 +- .../elements/src/datetime-picker/index.ts | 4 +- 9 files changed, 589 insertions(+), 428 deletions(-) rename packages/elements/src/datetime-picker/__snapshots__/{DOMStructure.md => DatetimePicker.md} (50%) delete mode 100644 packages/elements/src/datetime-picker/__test__/datetime-picker.snapshot.test.js diff --git a/package-lock.json b/package-lock.json index 8eaa4b0dd8..2b984e0661 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,10 +57,10 @@ }, "documents": { "name": "@refinitiv-ui/docs", - "version": "6.0.1", + "version": "6.0.3", "license": "Apache-2.0", "dependencies": { - "@refinitiv-ui/elements": "^6.0.1", + "@refinitiv-ui/elements": "^6.0.3", "fast-glob": "^3.2.7", "fs-extra": "^10.0.0" }, @@ -21428,7 +21428,7 @@ }, "packages/configurations": { "name": "@refinitiv-ui/configurations", - "version": "6.0.1", + "version": "6.0.2", "license": "Apache-2.0", "peerDependencies": { "@typescript-eslint/eslint-plugin": "^5.29.0", @@ -21441,7 +21441,7 @@ }, "packages/core": { "name": "@refinitiv-ui/core", - "version": "6.0.1", + "version": "6.0.2", "license": "Apache-2.0", "dependencies": { "@juggle/resize-observer": "^3.3.1", @@ -21449,47 +21449,47 @@ "tslib": "^2.3.1" }, "devDependencies": { - "@refinitiv-ui/test-helpers": "^6.0.1", - "@refinitiv-ui/utils": "^6.0.1" + "@refinitiv-ui/test-helpers": "^6.0.2", + "@refinitiv-ui/utils": "^6.0.2" }, "peerDependencies": { - "@refinitiv-ui/utils": "^6.0.1" + "@refinitiv-ui/utils": "^6.0.2" } }, "packages/demo-block": { "name": "@refinitiv-ui/demo-block", - "version": "6.0.1", + "version": "6.0.3", "license": "Apache-2.0", "dependencies": { - "@refinitiv-ui/elemental-theme": "^6.0.1", - "@refinitiv-ui/halo-theme": "^6.1.0", - "@refinitiv-ui/solar-theme": "^6.0.1", + "@refinitiv-ui/elemental-theme": "^6.0.3", + "@refinitiv-ui/halo-theme": "^6.1.2", + "@refinitiv-ui/solar-theme": "^6.0.3", "tslib": "^2.3.1" }, "devDependencies": { - "@refinitiv-ui/core": "^6.0.1", - "@refinitiv-ui/test-helpers": "^6.0.1" + "@refinitiv-ui/core": "^6.0.2", + "@refinitiv-ui/test-helpers": "^6.0.2" }, "peerDependencies": { - "@refinitiv-ui/core": "^6.0.1" + "@refinitiv-ui/core": "^6.0.2" } }, "packages/elemental-theme": { "name": "@refinitiv-ui/elemental-theme", - "version": "6.0.1", + "version": "6.0.3", "license": "Apache-2.0", "devDependencies": { - "@refinitiv-ui/theme-compiler": "^6.0.1" + "@refinitiv-ui/theme-compiler": "^6.0.2" } }, "packages/elements": { "name": "@refinitiv-ui/elements", - "version": "6.0.1", + "version": "6.0.3", "license": "Apache-2.0", "dependencies": { "@refinitiv-ui/browser-sparkline": "1.1.8", - "@refinitiv-ui/halo-theme": "^6.1.0", - "@refinitiv-ui/solar-theme": "^6.0.1", + "@refinitiv-ui/halo-theme": "^6.1.2", + "@refinitiv-ui/solar-theme": "^6.0.3", "@types/chart.js": "^2.9.31", "chart.js": "~2.9.4", "d3-interpolate": "^3.0.1", @@ -21498,37 +21498,37 @@ "tslib": "^2.3.1" }, "devDependencies": { - "@refinitiv-ui/core": "^6.0.1", - "@refinitiv-ui/demo-block": "^6.0.1", - "@refinitiv-ui/i18n": "^6.0.1", - "@refinitiv-ui/phrasebook": "^6.1.0", - "@refinitiv-ui/test-helpers": "^6.0.1", - "@refinitiv-ui/translate": "^6.0.1", - "@refinitiv-ui/utils": "^6.0.1", + "@refinitiv-ui/core": "^6.0.2", + "@refinitiv-ui/demo-block": "^6.0.3", + "@refinitiv-ui/i18n": "^6.0.2", + "@refinitiv-ui/phrasebook": "^6.1.1", + "@refinitiv-ui/test-helpers": "^6.0.2", + "@refinitiv-ui/translate": "^6.0.2", + "@refinitiv-ui/utils": "^6.0.2", "@types/d3-interpolate": "^3.0.1" }, "peerDependencies": { - "@refinitiv-ui/core": "^6.0.1", - "@refinitiv-ui/i18n": "^6.0.1", - "@refinitiv-ui/phrasebook": "^6.1.0", - "@refinitiv-ui/translate": "^6.0.1", - "@refinitiv-ui/utils": "^6.0.1" + "@refinitiv-ui/core": "^6.0.2", + "@refinitiv-ui/i18n": "^6.0.2", + "@refinitiv-ui/phrasebook": "^6.1.1", + "@refinitiv-ui/translate": "^6.0.2", + "@refinitiv-ui/utils": "^6.0.2" } }, "packages/halo-theme": { "name": "@refinitiv-ui/halo-theme", - "version": "6.1.0", + "version": "6.1.2", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@refinitiv-ui/elemental-theme": "^6.0.1" + "@refinitiv-ui/elemental-theme": "^6.0.3" }, "devDependencies": { - "@refinitiv-ui/theme-compiler": "^6.0.1" + "@refinitiv-ui/theme-compiler": "^6.0.2" } }, "packages/i18n": { "name": "@refinitiv-ui/i18n", - "version": "6.0.1", + "version": "6.0.2", "license": "Apache-2.0", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.0.15", @@ -21538,16 +21538,16 @@ "tslib": "^2.3.1" }, "devDependencies": { - "@refinitiv-ui/phrasebook": "^6.1.0", - "@refinitiv-ui/test-helpers": "^6.0.1" + "@refinitiv-ui/phrasebook": "^6.1.1", + "@refinitiv-ui/test-helpers": "^6.0.2" }, "peerDependencies": { - "@refinitiv-ui/phrasebook": "^6.1.0" + "@refinitiv-ui/phrasebook": "^6.1.1" } }, "packages/phrasebook": { "name": "@refinitiv-ui/phrasebook", - "version": "6.1.0", + "version": "6.1.1", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.1" @@ -21560,7 +21560,7 @@ }, "packages/polyfills": { "name": "@refinitiv-ui/polyfills", - "version": "6.0.1", + "version": "6.0.2", "license": "Apache-2.0", "dependencies": { "@webcomponents/custom-elements": "^1.4.1", @@ -21574,18 +21574,18 @@ }, "packages/solar-theme": { "name": "@refinitiv-ui/solar-theme", - "version": "6.0.1", + "version": "6.0.3", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@refinitiv-ui/elemental-theme": "^6.0.1" + "@refinitiv-ui/elemental-theme": "^6.0.3" }, "devDependencies": { - "@refinitiv-ui/theme-compiler": "^6.0.1" + "@refinitiv-ui/theme-compiler": "^6.0.2" } }, "packages/test-helpers": { "name": "@refinitiv-ui/test-helpers", - "version": "6.0.1", + "version": "6.0.2", "license": "Apache-2.0", "dependencies": { "@open-wc/testing": "^3.0.0-next.5", @@ -22053,7 +22053,7 @@ }, "packages/theme-compiler": { "name": "@refinitiv-ui/theme-compiler", - "version": "6.0.1", + "version": "6.0.2", "license": "Apache-2.0", "dependencies": { "autoprefixer": "^10.4.2", @@ -22135,26 +22135,26 @@ }, "packages/translate": { "name": "@refinitiv-ui/translate", - "version": "6.0.1", + "version": "6.0.2", "license": "Apache-2.0", "dependencies": { "lit": "2.2.2", "tslib": "^2.3.1" }, "devDependencies": { - "@refinitiv-ui/core": "^6.0.1", - "@refinitiv-ui/i18n": "^6.0.1", - "@refinitiv-ui/phrasebook": "^6.1.0", - "@refinitiv-ui/test-helpers": "^6.0.1" + "@refinitiv-ui/core": "^6.0.2", + "@refinitiv-ui/i18n": "^6.0.2", + "@refinitiv-ui/phrasebook": "^6.1.1", + "@refinitiv-ui/test-helpers": "^6.0.2" }, "peerDependencies": { - "@refinitiv-ui/i18n": "^6.0.1", - "@refinitiv-ui/phrasebook": "^6.1.0" + "@refinitiv-ui/i18n": "^6.0.2", + "@refinitiv-ui/phrasebook": "^6.1.1" } }, "packages/utils": { "name": "@refinitiv-ui/utils", - "version": "6.0.1", + "version": "6.0.2", "license": "Apache-2.0", "dependencies": { "@types/d3-color": "^3.0.2", @@ -26194,8 +26194,8 @@ "version": "file:packages/core", "requires": { "@juggle/resize-observer": "^3.3.1", - "@refinitiv-ui/test-helpers": "^6.0.1", - "@refinitiv-ui/utils": "^6.0.1", + "@refinitiv-ui/test-helpers": "^6.0.2", + "@refinitiv-ui/utils": "^6.0.2", "lit": "2.2.2", "tslib": "^2.3.1" } @@ -26203,18 +26203,18 @@ "@refinitiv-ui/demo-block": { "version": "file:packages/demo-block", "requires": { - "@refinitiv-ui/core": "^6.0.1", - "@refinitiv-ui/elemental-theme": "^6.0.1", - "@refinitiv-ui/halo-theme": "^6.1.0", - "@refinitiv-ui/solar-theme": "^6.0.1", - "@refinitiv-ui/test-helpers": "^6.0.1", + "@refinitiv-ui/core": "^6.0.2", + "@refinitiv-ui/elemental-theme": "^6.0.3", + "@refinitiv-ui/halo-theme": "^6.1.2", + "@refinitiv-ui/solar-theme": "^6.0.3", + "@refinitiv-ui/test-helpers": "^6.0.2", "tslib": "^2.3.1" } }, "@refinitiv-ui/docs": { "version": "file:documents", "requires": { - "@refinitiv-ui/elements": "^6.0.1", + "@refinitiv-ui/elements": "^6.0.3", "chalk": "^4.1.2", "concurrently": "^6.4.0", "fast-glob": "^3.2.7", @@ -26226,22 +26226,22 @@ "@refinitiv-ui/elemental-theme": { "version": "file:packages/elemental-theme", "requires": { - "@refinitiv-ui/theme-compiler": "^6.0.1" + "@refinitiv-ui/theme-compiler": "^6.0.2" } }, "@refinitiv-ui/elements": { "version": "file:packages/elements", "requires": { "@refinitiv-ui/browser-sparkline": "1.1.8", - "@refinitiv-ui/core": "^6.0.1", - "@refinitiv-ui/demo-block": "^6.0.1", - "@refinitiv-ui/halo-theme": "^6.1.0", - "@refinitiv-ui/i18n": "^6.0.1", - "@refinitiv-ui/phrasebook": "^6.1.0", - "@refinitiv-ui/solar-theme": "^6.0.1", - "@refinitiv-ui/test-helpers": "^6.0.1", - "@refinitiv-ui/translate": "^6.0.1", - "@refinitiv-ui/utils": "^6.0.1", + "@refinitiv-ui/core": "^6.0.2", + "@refinitiv-ui/demo-block": "^6.0.3", + "@refinitiv-ui/halo-theme": "^6.1.2", + "@refinitiv-ui/i18n": "^6.0.2", + "@refinitiv-ui/phrasebook": "^6.1.1", + "@refinitiv-ui/solar-theme": "^6.0.3", + "@refinitiv-ui/test-helpers": "^6.0.2", + "@refinitiv-ui/translate": "^6.0.2", + "@refinitiv-ui/utils": "^6.0.2", "@types/chart.js": "^2.9.31", "@types/d3-interpolate": "^3.0.1", "chart.js": "~2.9.4", @@ -26254,8 +26254,8 @@ "@refinitiv-ui/halo-theme": { "version": "file:packages/halo-theme", "requires": { - "@refinitiv-ui/elemental-theme": "^6.0.1", - "@refinitiv-ui/theme-compiler": "^6.0.1" + "@refinitiv-ui/elemental-theme": "^6.0.3", + "@refinitiv-ui/theme-compiler": "^6.0.2" } }, "@refinitiv-ui/i18n": { @@ -26263,8 +26263,8 @@ "requires": { "@formatjs/icu-messageformat-parser": "^2.0.15", "@formatjs/intl-utils": "^3.8.4", - "@refinitiv-ui/phrasebook": "^6.1.0", - "@refinitiv-ui/test-helpers": "^6.0.1", + "@refinitiv-ui/phrasebook": "^6.1.1", + "@refinitiv-ui/test-helpers": "^6.0.2", "intl-format-cache": "^4.3.1", "intl-messageformat": "^9.10.0", "tslib": "^2.3.1" @@ -26294,8 +26294,8 @@ "@refinitiv-ui/solar-theme": { "version": "file:packages/solar-theme", "requires": { - "@refinitiv-ui/elemental-theme": "^6.0.1", - "@refinitiv-ui/theme-compiler": "^6.0.1" + "@refinitiv-ui/elemental-theme": "^6.0.3", + "@refinitiv-ui/theme-compiler": "^6.0.2" } }, "@refinitiv-ui/test-helpers": { @@ -26658,10 +26658,10 @@ "@refinitiv-ui/translate": { "version": "file:packages/translate", "requires": { - "@refinitiv-ui/core": "^6.0.1", - "@refinitiv-ui/i18n": "^6.0.1", - "@refinitiv-ui/phrasebook": "^6.1.0", - "@refinitiv-ui/test-helpers": "^6.0.1", + "@refinitiv-ui/core": "^6.0.2", + "@refinitiv-ui/i18n": "^6.0.2", + "@refinitiv-ui/phrasebook": "^6.1.1", + "@refinitiv-ui/test-helpers": "^6.0.2", "lit": "2.2.2", "tslib": "^2.3.1" } diff --git a/packages/elements/src/datetime-picker/__snapshots__/DOMStructure.md b/packages/elements/src/datetime-picker/__snapshots__/DatetimePicker.md similarity index 50% rename from packages/elements/src/datetime-picker/__snapshots__/DOMStructure.md rename to packages/elements/src/datetime-picker/__snapshots__/DatetimePicker.md index b324fb2356..04c5ec342a 100644 --- a/packages/elements/src/datetime-picker/__snapshots__/DOMStructure.md +++ b/packages/elements/src/datetime-picker/__snapshots__/DatetimePicker.md @@ -1,4 +1,4 @@ -# `datetime-picker/DOMStructure` +# `datetime-picker/DatetimePicker` ## `DOM Structure` @@ -6,19 +6,24 @@ ```html
- - +
- - + + + ``` @@ -26,28 +31,35 @@ ```html
- - +
- - + + +
@@ -62,7 +74,6 @@
- - +
- - +
- - + + +
@@ -133,7 +152,6 @@
- - +
- - + + +
@@ -196,7 +221,6 @@
- - +
- - + + +
@@ -266,7 +296,6 @@
- - +
- - + + +
@@ -337,7 +372,6 @@
- - +
- - +
- - + + +
@@ -417,7 +458,6 @@
+ + +
+ + + + +
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +``` + +#### `DOM structure is correct when time-only formatOptions` + +```html +
+ + +
+ + + + +
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +``` + +#### `DOM structure is correct when date-time formatOptions` + +```html +
+ + +
+ + + + +
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +``` + diff --git a/packages/elements/src/datetime-picker/__test__/datetime-picker.default.test.js b/packages/elements/src/datetime-picker/__test__/datetime-picker.default.test.js index 4cf3d044d7..530ca6771a 100644 --- a/packages/elements/src/datetime-picker/__test__/datetime-picker.default.test.js +++ b/packages/elements/src/datetime-picker/__test__/datetime-picker.default.test.js @@ -3,25 +3,74 @@ import { fixture, expect, elementUpdated } from '@refinitiv-ui/test-helpers'; // import element and theme import '@refinitiv-ui/elements/datetime-picker'; import '@refinitiv-ui/elemental-theme/light/ef-datetime-picker'; - -const INPUT_FORMAT = { - DATE: 'dd-MMM-yyyy', - DATETIME: 'dd-MMM-yyyy HH:mm', - DATETIME_AM_PM: 'dd-MMM-yyyy hh:mm aaa', - DATETIME_SECONDS: 'dd-MMM-yyyy HH:mm:ss', - DATETIME_SECONDS_AM_PM: 'dd-MMM-yyyy hh:mm:ss aaa' -}; +import { inputElement, inputToElement, snapshotIgnore } from './utils'; describe('datetime-picker/DatetimePicker', () => { + describe('DOM Structure', () => { + it('DOM structure is correct', async () => { + const el = await fixture(''); + expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); + }); + it('DOM structure is correct when opened', async () => { + const el = await fixture(''); + expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); + }); + it('DOM structure is correct when range', async () => { + const el = await fixture(''); + expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); + }); + it('DOM structure is correct when duplex', async () => { + const el = await fixture(''); + expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); + }); + it('DOM structure is correct when timepicker', async () => { + const el = await fixture(''); + expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); + }); + it('DOM structure is correct when timepicker and with-seconds', async () => { + const el = await fixture(''); + expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); + }); + it('DOM structure is correct when range timepicker', async () => { + const el = await fixture(''); + expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); + }); + it('DOM structure is correct when date-only formatOptions', async () => { + const el = await fixture(''); + el.formatOptions = { + day: 'numeric' + }; + expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); + }); + it('DOM structure is correct when time-only formatOptions', async () => { + const el = await fixture(''); + el.formatOptions = { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }; + expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); + }); + it('DOM structure is correct when date-time formatOptions', async () => { + const el = await fixture(''); + el.formatOptions = { + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }; + expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); + }); + }); describe('Defaults', () => { it('Check default properties', async () => { const el = await fixture(''); - expect(el.min).to.be.equal(''); - expect(el.max).to.be.equal(''); + expect(el.min).to.be.equal(null); + expect(el.max).to.be.equal(null); expect(el.weekdaysOnly).to.be.equal(false); expect(el.weekendsOnly).to.be.equal(false); expect(el.lang).to.be.equal(''); - expect(el.firstDayOfWeek).to.be.equal(undefined); + expect(el.firstDayOfWeek).to.be.equal(null); expect(el.range).to.be.equal(false); expect(el.value).to.be.equal(''); expect(el.values.join('')).to.be.equal(''); @@ -31,73 +80,25 @@ describe('datetime-picker/DatetimePicker', () => { expect(el.opened).to.be.equal(false); expect(el.error).to.be.equal(false); expect(el.warning).to.be.equal(false); - expect(el.inputTriggerDisabled).to.be.equal(false); expect(el.inputDisabled).to.be.equal(false); expect(el.popupDisabled).to.be.equal(false); expect(el.timepicker).to.be.equal(false); expect(el.duplex).to.be.equal(null); expect(el.readonly).to.be.equal(false); expect(el.disabled).to.be.equal(false); - }); - - it('date format is correct', async () => { - const el = await fixture(''); - expect(el.format).to.be.equal(INPUT_FORMAT.DATE, 'Date format is wrong'); - expect(el.inputEl.value).to.be.equal('21-Apr-2020', 'Date format is not applied'); - }); - - it('date-time format is correct', async () => { - const el = await fixture(''); - expect(el.format).to.be.equal(INPUT_FORMAT.DATETIME, 'Datetime format is wrong'); - expect(el.inputEl.value).to.be.equal('21-Apr-2020 14:58', 'Datetime format is not applied'); - }); - - it('date-time-am-pm format is correct', async () => { - const el = await fixture(''); - expect(el.format).to.be.equal(INPUT_FORMAT.DATETIME_AM_PM, 'Datetime AM-PM format is wrong'); - expect(el.inputEl.value).to.be.equal('21-Apr-2020 02:58 pm', 'Datetime AM-PM format is not applied'); - }); - - it('date-time-seconds format is correct', async () => { - const el = await fixture(''); - expect(el.format).to.be.equal(INPUT_FORMAT.DATETIME_SECONDS, 'Datetime with seconds format is wrong'); - expect(el.inputEl.value).to.be.equal('21-Apr-2020 14:58:59', 'Datetime with seconds format is not applied'); - }); - - it('date-time-am-pm-seconds format is correct', async () => { - const el = await fixture(''); - expect(el.format).to.be.equal(INPUT_FORMAT.DATETIME_SECONDS_AM_PM, 'Datetime AM-PM with seconds format is wrong'); - expect(el.inputEl.value).to.be.equal('21-Apr-2020 02:58:59 pm', 'Datetime AM-PM with seconds format is not applied'); - }); - - it('date-time-seconds local format is correct', async () => { - const el = await fixture(''); - expect(el.format).to.be.equal(INPUT_FORMAT.DATETIME_SECONDS, 'Datetime custom locale with seconds format is wrong'); - expect(el.inputEl.value).to.be.equal('21-апр.-2020 14:58:59', 'Datetime custom locale with seconds format is not applied'); - }); - - it('Can change format', async () => { - const customFormat = 'dd-MM-yy HH:mm:ss'; - const el = await fixture(``); - expect(el.format).to.be.equal(customFormat, 'Custom format is not passed'); - expect(el.inputEl.value).to.be.equal('21-04-20 14:58:59', 'Custom format is not applied'); + expect(el.placeholder).to.be.equal(''); + expect(el.locale).to.be.equal(null); + expect(el.formatOptions).to.be.equal(null); }); }); describe('Placeholder Test', () => { - it('Default Placeholder', async () => { - const el = await fixture(''); - expect(el.placeholder).to.be.equal(INPUT_FORMAT.DATE); - const input = el.inputEl; - expect(input.placeholder).to.be.equal(INPUT_FORMAT.DATE, 'Default placeholder is not passed to to input'); - }); - it('Can set custom placeholder', async () => { const placeholder = 'Test'; const el = await fixture(''); el.placeholder = placeholder; await elementUpdated(el); - const inputFrom = el.inputEl; - const inputTo = el.inputToEl; + const inputFrom = inputElement(el); + const inputTo = inputToElement(el); expect(el.placeholder).to.be.equal(placeholder, 'Placeholder getter is wrong'); expect(inputFrom.placeholder).to.be.equal(placeholder, 'Placeholder is not passed to to input'); expect(inputTo.placeholder).to.be.equal(placeholder, 'Placeholder is not passed to from input'); diff --git a/packages/elements/src/datetime-picker/__test__/datetime-picker.navigation.test.js b/packages/elements/src/datetime-picker/__test__/datetime-picker.navigation.test.js index 7da46d9bec..7d4c7eca03 100644 --- a/packages/elements/src/datetime-picker/__test__/datetime-picker.navigation.test.js +++ b/packages/elements/src/datetime-picker/__test__/datetime-picker.navigation.test.js @@ -4,7 +4,7 @@ import { elementUpdated, oneEvent } from '@refinitiv-ui/test-helpers'; -import { fireKeydownEvent } from './utils'; +import { fireKeydownEvent, buttonElement } from './utils'; // import element and theme import '@refinitiv-ui/elements/datetime-picker'; @@ -12,76 +12,33 @@ import '@refinitiv-ui/elemental-theme/light/ef-datetime-picker'; describe('datetime-picker/Navigation', () => { describe('Navigation', () => { - it('Clicking on datetime picker icon should open/close calendar and fire opened-changed event', async () => { + it('Clicking on datetime picker button should open calendar and fire opened-changed event', async () => { const el = await fixture(''); - const iconEl = el.iconEl; - - setTimeout(() => iconEl.click()); + const buttonEl = buttonElement(el); + setTimeout(() => buttonEl.click()); await elementUpdated(el); - let event = await oneEvent(el, 'opened-changed'); + const event = await oneEvent(el, 'opened-changed'); expect(el.opened).to.be.equal(true, 'Clicking on icon should open calendar'); expect(event.detail.value).to.be.equal(true, 'opened-changed event is wrong'); - - setTimeout(() => iconEl.click()); - await elementUpdated(el); - event = await oneEvent(el, 'opened-changed'); - expect(el.opened).to.be.equal(false, 'Clicking on icon again should close calendar'); - expect(event.detail.value).to.be.equal(false, 'opened-changed event is wrong'); - }); - it('Clicking on datetime picker should open calendar', async () => { - const el = await fixture(''); - el.click(); - await elementUpdated(el); - expect(el.opened).to.be.equal(true, 'Clicking on calendar area should open calendar'); - el.click(); - await elementUpdated(el); - expect(el.opened).to.be.equal(true, 'Clicking on calendar area again should not close calendar'); }); - it('Arrow Down/Up should open/close calendar', async () => { + it('Tab on button should open calendar', async () => { const el = await fixture(''); - fireKeydownEvent(el, 'ArrowDown'); - await elementUpdated(el); - expect(el.opened).to.be.equal(true, 'Arrow down should open calendar'); - fireKeydownEvent(el, 'ArrowUp'); - await elementUpdated(el); - expect(el.opened).to.be.equal(false, 'Arrow up should close calendar'); - fireKeydownEvent(el, 'Down'); - await elementUpdated(el); - expect(el.opened).to.be.equal(true, 'Down should open calendar'); - fireKeydownEvent(el, 'Up'); + buttonElement(el).dispatchEvent(new CustomEvent('tap')); await elementUpdated(el); - expect(el.opened).to.be.equal(false, 'Up should close calendar'); + expect(el.opened).to.be.equal(true, 'Tab should open calendar'); }); it('Esc should close calendar', async () => { const el = await fixture(''); - fireKeydownEvent(el.calendarEl, 'Esc'); + fireKeydownEvent(el, 'Esc'); await elementUpdated(el); expect(el.opened).to.be.equal(false, 'Esc should close calendar'); }); it('Escape should close calendar', async () => { const el = await fixture(''); - fireKeydownEvent(el.calendarEl, 'Escape'); + fireKeydownEvent(el, 'Escape'); await elementUpdated(el); expect(el.opened).to.be.equal(false, 'Escape should close calendar'); }); - it('Esc on input should close calendar', async () => { - const el = await fixture(''); - fireKeydownEvent(el.inputEl, 'Esc'); - await elementUpdated(el); - expect(el.opened).to.be.equal(false, 'Esc should close calendar'); - }); - it('Escape on input should close calendar', async () => { - const el = await fixture(''); - fireKeydownEvent(el.inputEl, 'Escape'); - await elementUpdated(el); - expect(el.opened).to.be.equal(false, 'Escape should close calendar'); - }); - it('Enter key on input should open calendar', async () => { - const el = await fixture(''); - fireKeydownEvent(el.inputEl, 'Enter'); - await elementUpdated(el); - expect(el.opened).to.be.equal(true, 'Enter should open calendar'); - }); it('Clicking on outside should close calendar', async () => { const el = await fixture(''); document.dispatchEvent(new CustomEvent('tapstart')); diff --git a/packages/elements/src/datetime-picker/__test__/datetime-picker.snapshot.test.js b/packages/elements/src/datetime-picker/__test__/datetime-picker.snapshot.test.js deleted file mode 100644 index 2fd51e556a..0000000000 --- a/packages/elements/src/datetime-picker/__test__/datetime-picker.snapshot.test.js +++ /dev/null @@ -1,52 +0,0 @@ -import { fixture, expect, nextFrame } from '@refinitiv-ui/test-helpers'; -import { snapshotIgnore } from './utils'; - -// import element and theme -import '@refinitiv-ui/elements/datetime-picker'; -import '@refinitiv-ui/elemental-theme/light/ef-datetime-picker'; - -describe('datetime-picker/DOMStructure', () => { - describe('DOM Structure', () => { - it('DOM structure is correct', async () => { - const el = await fixture(''); - await nextFrame(); - expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); - }); - it('DOM structure is correct when opened', async () => { - const el = await fixture(''); - await nextFrame(); - await nextFrame(); /* second frame required for IE11 as popup opened might not fit into one frame */ - expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); - }); - it('DOM structure is correct when range', async () => { - const el = await fixture(''); - await nextFrame(); - await nextFrame(); - expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); - }); - it('DOM structure is correct when duplex', async () => { - const el = await fixture(''); - await nextFrame(); - await nextFrame(); - expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); - }); - it('DOM structure is correct when timepicker', async () => { - const el = await fixture(''); - await nextFrame(); - await nextFrame(); - expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); - }); - it('DOM structure is correct when timepicker and with-seconds', async () => { - const el = await fixture(''); - await nextFrame(); - await nextFrame(); - expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); - }); - it('DOM structure is correct when range timepicker', async () => { - const el = await fixture(''); - await nextFrame(); - await nextFrame(); - expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); - }); - }); -}); diff --git a/packages/elements/src/datetime-picker/__test__/datetime-picker.value.test.js b/packages/elements/src/datetime-picker/__test__/datetime-picker.value.test.js index a3e78bdcdf..aa69f0f198 100644 --- a/packages/elements/src/datetime-picker/__test__/datetime-picker.value.test.js +++ b/packages/elements/src/datetime-picker/__test__/datetime-picker.value.test.js @@ -1,5 +1,6 @@ -import { fixture, expect, elementUpdated, oneEvent, triggerFocusFor, nextFrame, isIE } from '@refinitiv-ui/test-helpers'; -import { typeText } from './utils'; +import { fixture, expect, elementUpdated, oneEvent, nextFrame } from '@refinitiv-ui/test-helpers'; +import { calendarElement, calendarToElement, inputElement, inputToElement, timePickerElement, typeText } from './utils'; +import { Locale } from '@refinitiv-ui/utils/date.js'; // import element and theme import '@refinitiv-ui/elements/datetime-picker'; @@ -9,109 +10,96 @@ describe('datetime-picker/Value', () => { describe('Value Test', () => { it('Changing the value should fire value-changed event', async () => { const el = await fixture(''); - setTimeout(() => typeText(el.inputEl, '21-Apr-2020')); + setTimeout(() => typeText(inputElement(el), '2020-04-21')); const { detail: { value } } = await oneEvent(el, 'value-changed'); - await elementUpdated(); + await elementUpdated(el); expect(el.value).to.be.equal('2020-04-21'); - expect(el.calendarEl.value).to.be.equal('2020-04-21'); + expect(calendarElement(el).value).to.be.equal('2020-04-21'); expect(value).to.be.equal('2020-04-21', 'value-changed event should be fired when changing input'); }); it('It should be possible to set min/max', async () => { const el = await fixture(''); + const calendarEl = calendarElement(el); expect(el.min).to.be.equal('2020-04-01', 'min getter is wrong'); expect(el.max).to.be.equal('2020-04-30', 'max getter is wrong'); - expect(el.calendarEl.min).to.be.equal('2020-04-01', 'calendar min getter is wrong'); - expect(el.calendarEl.max).to.be.equal('2020-04-30', 'calendar min getter is wrong'); - }); - it('It should not be possible to set invalid min/max', async () => { - const el = await fixture(''); - expect(el.min).to.be.equal('', 'Invalid min should reset min'); - expect(el.max).to.be.equal('', 'Invalid max should reset max'); - }); - it('Typing invalid value in input should mark datetime picker as invalid and error-changed event is fired', async () => { - const el = await fixture(''); - setTimeout(() => typeText(el.inputEl, 'Invalid Value')); - const { detail: { value } } = await oneEvent(el, 'error-changed'); - await elementUpdated(); - expect(el.error).to.be.equal(true); - expect(el.value).to.be.equal(''); - expect(el.calendarEl.value).to.be.equal(''); - expect(value).to.be.equal(true, 'error-changed event should be fired when user puts invalid value'); + expect(calendarEl.min).to.be.equal('2020-04-01', 'calendar min getter is wrong'); + expect(calendarEl.max).to.be.equal('2020-04-30', 'calendar max getter is wrong'); }); it('It should not be possible to set from value after to', async () => { const el = await fixture(''); - expect(el.error).to.be.equal(true); + expect(el.checkValidity()).to.be.equal(false, 'from value is after to'); }); it('It should not be possible to set value before min', async () => { const el = await fixture(''); - expect(el.error).to.be.equal(true); + expect(el.checkValidity()).to.be.equal(false, 'value is less than min'); }); it('It should not be possible to set value after max', async () => { const el = await fixture(''); - expect(el.error).to.be.equal(true); - }); - it('While typing the value calendar input should not randomly update value', async function () { - if (isIE()) { - this.skip(); - } - // this test becomes invalid if date-fns ever supports strict formatting - const el = await fixture(''); - const input = el.inputEl; - await triggerFocusFor(input); - typeText(el.inputEl, '21-A-2020'); - await elementUpdated(el); - expect(el.inputEl.value).to.be.equal('21-A-2020', 'While in focus input value is not changed'); - await triggerFocusFor(el); - await elementUpdated(el); - expect(el.inputEl.value).to.be.equal('21-Apr-2020', 'On blur input values becomes formatted value'); + expect(el.checkValidity()).to.be.equal(false, 'value is more than max'); }); it('It should be possible to select value by clicking on calendar', async () => { const el = await fixture(''); - const calendarEl = el.calendarEl; + const calendarEl = calendarElement(el); await elementUpdated(el); const cell = calendarEl.shadowRoot.querySelectorAll('div[tabindex]')[2]; // 2020-04-01 cell.click(); await elementUpdated(el); expect(el.value).to.be.equal('2020-04-01', 'Value has not update'); - expect(el.inputEl.value).to.be.equal('01-Apr-2020', 'Input value has not updated'); + expect(inputElement(el).value).to.be.equal('2020-04-01', 'Input value has not updated'); }); it('It should be possible to select value in range duplex mode', async () => { const el = await fixture(''); el.views = ['2020-04', '2020-05']; - await elementUpdated(el); - await nextFrame(); - await nextFrame(); + await nextFrame(el); - const calendarEl = el.calendarEl; + const calendarEl = calendarElement(el); const fromCell = calendarEl.shadowRoot.querySelectorAll('div[tabindex]')[0]; // 2020-04-01 fromCell.click(); await elementUpdated(el); - await nextFrame(); - const calendarToEl = el.calendarToEl; + const calendarToEl = calendarToElement(el); const toCell = calendarToEl.shadowRoot.querySelectorAll('div[tabindex]')[0]; // 2020-05-01 toCell.click(); await elementUpdated(el); - await nextFrame(); expect(el.values[0]).to.be.equal('2020-04-01', 'Value from has not been updated'); expect(el.values[1]).to.be.equal('2020-05-01', 'Value to has not been update'); - expect(el.inputEl.value).to.be.equal('01-Apr-2020', 'Input from value has not updated'); - expect(el.inputToEl.value).to.be.equal('01-May-2020', 'Input to value has not updated'); + expect(inputElement(el).value).to.be.equal('2020-04-01', 'Input from value has not updated'); + expect(inputToElement(el).value).to.be.equal('2020-05-01', 'Input to value has not updated'); }); it('Timepicker value is populated', async () => { - const el = await fixture(''); - const timePicker = el.timepickerEl; + const el = await fixture(''); + const timePicker = timePickerElement(el); expect(timePicker.hours).to.equal(13); expect(timePicker.minutes).to.equal(14); expect(timePicker.seconds).to.equal(15); }); it('It should be possible to change timepicker value', async () => { - const el = await fixture(''); - const timePicker = el.timepickerEl; - typeText(timePicker, '16:17:18'); + const el = await fixture(''); + typeText(timePickerElement(el), '16:17:18'); expect(el.value).to.equal('2020-04-21T16:17:18'); }); + it('It should be possible to change formatOptions value', async () => { + const el = await fixture(''); + expect(timePickerElement(el)).to.be.exist; + el.formatOptions = { + month: 'long', + day: 'numeric' + } + await elementUpdated(el); + expect(timePickerElement(el)).to.not.exist; + }); + it('It should be possible to change locale value', async () => { + const el = await fixture(''); + expect(timePickerElement(el)).to.be.exist; + el.locale = Locale.fromOptions({ + month: 'long', + day: 'numeric' + }, 'en-us'); + await elementUpdated(el); + expect(inputElement(el).inputValue).to.equal('April 21', 'locale is not override lang value'); + expect(timePickerElement(el)).to.not.exist; + }); }); }); diff --git a/packages/elements/src/datetime-picker/__test__/datetime-picker.view.test.js b/packages/elements/src/datetime-picker/__test__/datetime-picker.view.test.js index ea55fac79e..507df73369 100644 --- a/packages/elements/src/datetime-picker/__test__/datetime-picker.view.test.js +++ b/packages/elements/src/datetime-picker/__test__/datetime-picker.view.test.js @@ -1,5 +1,5 @@ import { fixture, expect, elementUpdated, oneEvent } from '@refinitiv-ui/test-helpers'; -import { typeText, calendarClickNext, formatToView, addMonths } from './utils'; +import { typeText, calendarClickNext, formatToView, addMonths, inputElement, inputToElement, calendarElement, calendarToElement } from './utils'; // import element and theme import '@refinitiv-ui/elements/datetime-picker'; @@ -39,40 +39,34 @@ describe('datetime-picker/View', () => { }); it('View changes when typing the value', async () => { const el = await fixture(''); - const input = el.inputEl; - typeText(input, '21-Apr-2020'); + typeText(inputElement(el), '2020-04-21'); await elementUpdated(el); expect(el.view).to.be.equal('2020-04', 'View did not change when typing text'); }); it('View reset to today when clearing the value', async () => { const el = await fixture(''); - const input = el.inputEl; - typeText(input, ''); + typeText(inputElement(el), ''); await elementUpdated(el); expect(el.view).to.be.equal(formatToView(now), 'View should reset to now when value clears'); }); it('Duplex view changes when typing the value', async () => { const el = await fixture(''); - const input = el.inputEl; - typeText(input, '21-Apr-2020'); + typeText(inputElement(el), '2020-04-21'); await elementUpdated(el); expect(el.views[0]).to.be.equal('2020-04', 'Duplex: view from did not change when typing text'); expect(el.views[1]).to.be.equal('2020-05', 'Duplex: view to did not change when typing text'); }); it('Duplex split view changes when typing the value', async () => { const el = await fixture(''); - const input = el.inputEl; - typeText(input, '21-Apr-2020'); + typeText(inputElement(el), '2020-04-21'); await elementUpdated(el); expect(el.views[0]).to.be.equal('2020-04', 'Duplex split: view from did not change when typing text'); expect(el.views[1]).to.be.equal('2020-05', 'Duplex split: view to did not change when typing text'); }); it('Duplex split range view changes when typing the value', async () => { const el = await fixture(''); - const inputFrom = el.inputEl; - const inputTo = el.inputToEl; - typeText(inputFrom, '21-Jan-2020'); - typeText(inputTo, '21-Apr-2020'); + typeText(inputElement(el), '2020-01-21'); + typeText(inputToElement(el), '2020-04-21'); await elementUpdated(el); expect(el.views[0]).to.be.equal('2020-01', 'Duplex split range: view from did not change when typing text'); expect(el.views[1]).to.be.equal('2020-04', 'Duplex split range: view to did not change when typing text'); @@ -87,10 +81,8 @@ describe('datetime-picker/View', () => { const el = await fixture(''); el.views = ['2020-01', '2020-04']; await elementUpdated(el); - const calendarFrom = el.calendarEl; - const calendarTo = el.calendarToEl; - expect(calendarFrom.view).to.be.equal('2020-01', 'From view is not propagated to calendar'); - expect(calendarTo.view).to.be.equal('2020-04', 'To view is not propagated to calendar'); + expect(calendarElement(el).view).to.be.equal('2020-01', 'From view is not propagated to calendar'); + expect(calendarToElement(el).view).to.be.equal('2020-04', 'To view is not propagated to calendar'); }); it('Passing empty string should reset views to default', async () => { const el = await fixture(''); @@ -101,7 +93,7 @@ describe('datetime-picker/View', () => { }); it('Changing view in calendar should be reflected in datetime-picker and should fire view-changed event', async () => { const el = await fixture(''); - setTimeout(() => calendarClickNext(el.calendarEl)); + setTimeout(() => calendarClickNext(calendarElement(el))); const { detail: { value } } = await oneEvent(el, 'view-changed'); await elementUpdated(); expect(value).to.be.equal('2020-05', 'view-changed event does not contain valid value'); @@ -109,8 +101,8 @@ describe('datetime-picker/View', () => { }); it('In duplex mode calendar view should be in sync', async () => { const el = await fixture(''); - const calendarFrom = el.calendarEl; - const calendarTo = el.calendarToEl; + const calendarFrom = calendarElement(el); + const calendarTo = calendarToElement(el); await elementUpdated(calendarFrom); await elementUpdated(calendarTo); calendarClickNext(calendarFrom); @@ -128,8 +120,8 @@ describe('datetime-picker/View', () => { const el = await fixture(''); el.views = ['2020-04', '2020-05']; await elementUpdated(el); - const calendarFrom = el.calendarEl; - const calendarTo = el.calendarToEl; + const calendarFrom = calendarElement(el); + const calendarTo = calendarToElement(el); calendarClickNext(calendarFrom); await elementUpdated(el); expect(calendarFrom.view).to.equal('2020-05', 'Calendar from is not in sync'); diff --git a/packages/elements/src/datetime-picker/__test__/utils.js b/packages/elements/src/datetime-picker/__test__/utils.js index 544c291d02..fea501b5af 100644 --- a/packages/elements/src/datetime-picker/__test__/utils.js +++ b/packages/elements/src/datetime-picker/__test__/utils.js @@ -1,12 +1,24 @@ import { elementUpdated, keyboardEvent } from '@refinitiv-ui/test-helpers'; import { format, parse, DateFormat, DateTimeFormat, addMonths as utilsAddMonths } from '@refinitiv-ui/utils'; -export const fireKeydownEvent = (element, key, shiftKey = false) => { +const snapshotIgnore = { + ignoreAttributes: ['style'] +}; + +const buttonElement = (el) => el.shadowRoot.querySelector('[part="button"]'); +const inputElement = (el) => el.inputRef.value; // Access private property +const inputToElement = (el) => el.inputToRef.value // Access private property +const calendarElement = (el) => el.calendarRef.value // Access private property +const calendarToElement = (el) => el.calendarToRef.value // Access private property +const timePickerElement = (el) => el.timepickerRef.value // Access private property + + +const fireKeydownEvent = (element, key, shiftKey = false) => { const event = keyboardEvent('keydown', { key, shiftKey }); element.dispatchEvent(event); }; -export const typeText = (element, text) => { +const typeText = (element, text) => { element.value = text; element.dispatchEvent(new CustomEvent('value-changed', { detail: { @@ -15,19 +27,30 @@ export const typeText = (element, text) => { })); }; -export const addMonths = (date, amount) => { +const addMonths = (date, amount) => { return parse(utilsAddMonths(format(date, DateTimeFormat.yyyMMddTHHmmss), amount)); }; -export const formatToView = (date) => { +const formatToView = (date) => { return format(date, DateFormat.yyyyMM); }; -export const calendarClickNext = async (calendarEl) => { +const calendarClickNext = async (calendarEl) => { calendarEl.shadowRoot.querySelector('[part=btn-next]').click(); await elementUpdated(calendarEl); }; -export const snapshotIgnore = { - ignoreAttributes: ['style', 'class'] -}; +export { + snapshotIgnore, + buttonElement, + inputElement, + inputToElement, + calendarElement, + calendarToElement, + timePickerElement, + fireKeydownEvent, + typeText, + addMonths, + formatToView, + calendarClickNext +} diff --git a/packages/elements/src/datetime-picker/index.ts b/packages/elements/src/datetime-picker/index.ts index 5f2284bf7b..79126625c7 100644 --- a/packages/elements/src/datetime-picker/index.ts +++ b/packages/elements/src/datetime-picker/index.ts @@ -184,9 +184,10 @@ export class DatetimePicker extends ControlElement implements MultiValue { /** * Set the datetime format options based on - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat + * [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat](Intl.DatetimeFormat) * `formatOptions` overrides `timepicker` and `showSeconds` properties. * Note: time-zone is not supported + * @type {Intl.DateTimeFormatOptions | null} */ @property({ attribute: false }) public formatOptions: Intl.DateTimeFormatOptions | null = null; @@ -194,6 +195,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { /** * Set the Locale object. * `Locale` overrides `formatOptions`, `timepicker` and `showSeconds` properties. + * @type {Locale | null} */ @property({ attribute: false }) public locale: Locale | null = null; From ca41c8935979afaf0151ff3188535f98c6bdd477 Mon Sep 17 00:00:00 2001 From: AG <81616437+goremikins@users.noreply.github.com> Date: Wed, 10 Aug 2022 14:11:29 +0100 Subject: [PATCH 13/21] feat(utils): add format checks to Locale object refactor(datetime-picker): improve code quality refactor(datetime-field): improve code quality --- packages/elements/src/datetime-field/index.ts | 17 ++- .../src/datetime-field/resolvedLocale.ts | 122 ++++++++++++------ .../elements/src/datetime-picker/index.ts | 39 +++--- .../elements/src/datetime-picker/utils.ts | 34 +---- packages/utils/src/date/Locale.ts | 32 +++++ 5 files changed, 151 insertions(+), 93 deletions(-) diff --git a/packages/elements/src/datetime-field/index.ts b/packages/elements/src/datetime-field/index.ts index 41b407eba6..22b04f2099 100644 --- a/packages/elements/src/datetime-field/index.ts +++ b/packages/elements/src/datetime-field/index.ts @@ -208,13 +208,20 @@ export class DatetimeField extends TextField { @state() protected partLabel = ''; + /** + * Get resolved locale for current element + */ + protected get resolvedLocale (): Locale { + return resolvedLocale(this); + } + /** * Transform Date object to date string * @param value Date * @returns dateSting */ protected dateToString (value: Date): string { - return isNaN(value.getTime()) ? '' : utcFormat(value, resolvedLocale(this).isoFormat); + return isNaN(value.getTime()) ? '' : utcFormat(value, this.resolvedLocale.isoFormat); } /** @@ -308,7 +315,7 @@ export class DatetimeField extends TextField { return true; } // value format depends on locale. - return getFormat(value) === resolvedLocale(this).isoFormat; + return getFormat(value) === this.resolvedLocale.isoFormat; } /** @@ -317,14 +324,14 @@ export class DatetimeField extends TextField { * @returns {void} */ protected override warnInvalidValue (value: string): void { - new WarningNotice(`${this.localName}: the specified value "${value}" does not conform to the required format. The format is '${resolvedLocale(this).isoFormat}'.`).show(); + new WarningNotice(`${this.localName}: the specified value "${value}" does not conform to the required format. The format is '${this.resolvedLocale.isoFormat}'.`).show(); } /** * Get Intl.DateTimeFormat object from locale */ protected get formatter (): Intl.DateTimeFormat { - return resolvedLocale(this).formatter; + return this.resolvedLocale.formatter; } /** @@ -355,7 +362,7 @@ export class DatetimeField extends TextField { protected toValue (inputValue: string): string { let value = ''; try { - value = inputValue ? resolvedLocale(this).parse(inputValue, this.value || this.startDate) : ''; + value = inputValue ? this.resolvedLocale.parse(inputValue, this.value || this.startDate) : ''; } catch (error) { // do nothing diff --git a/packages/elements/src/datetime-field/resolvedLocale.ts b/packages/elements/src/datetime-field/resolvedLocale.ts index 71c761a3b9..e063bc8aa7 100644 --- a/packages/elements/src/datetime-field/resolvedLocale.ts +++ b/packages/elements/src/datetime-field/resolvedLocale.ts @@ -1,25 +1,27 @@ import { Locale } from '@refinitiv-ui/utils/date.js'; import { getLocale as getLang } from '@refinitiv-ui/translate'; -const LocaleMap = new WeakMap(); + locale?: Locale, + lang?: string; + formatOptions?: Intl.DateTimeFormatOptions; + amPm?: boolean; + showSeconds?: boolean; + timepicker?: boolean; +}; + +const LocaleMap = new WeakMap(); /** * Used for date elements to construct Locale object */ type LocaleDateElement = HTMLElement & { - formatOptions: Intl.DateTimeFormatOptions | null; - amPm: boolean; - showSeconds: boolean; - timepicker: boolean; - locale: Locale | null; + formatOptions?: Intl.DateTimeFormatOptions | null; + amPm?: boolean; + showSeconds?: boolean; + timepicker?: boolean; + locale?: Locale | null; }; /** @@ -51,27 +53,33 @@ const hasTimepicker = (element: LocaleDateElement): boolean => { }; /** - * Resolve locale based on element parameters + * Resolve locale based on locale properties * @param lang Resolved language (locale) - * @param formatOptions Format options * @param timepicker Has time info * @param amPm Has amPm info * @param showSeconds Has seconds info + * @param options Override options if resolved from element * @returns locale Resolved locale */ -const resolveLocaleFromElement = (lang: string, formatOptions: Intl.DateTimeFormatOptions | null, timepicker: boolean, amPm: boolean, showSeconds: boolean): Locale => { +const localeFromProperties = (lang: string, timepicker: boolean, amPm: boolean, showSeconds: boolean, options: Intl.DateTimeFormatOptions): Locale => { // TODO: Do not use dateStyle and timeStyle as these are supported only in modern browsers - return Locale.fromOptions(formatOptions || { - year: 'numeric', - month: 'short', - day: 'numeric', + return Locale.fromOptions({ hour: timepicker ? 'numeric' : undefined, minute: timepicker ? 'numeric' : undefined, second: showSeconds ? 'numeric' : undefined, - hour12: amPm ? true : undefined // force am-pm if provided, otherwise rely on locale + hour12: amPm ? true : undefined, // force am-pm if provided, otherwise rely on locale + ...options }, lang); }; +/** + * Resolve locale based on format options + * @param lang Resolved language (locale) + * @param formatOptions Format options + * @returns locale Resolved locale + */ +const localeFromOptions = (lang: string, formatOptions: Intl.DateTimeFormatOptions): Locale => Locale.fromOptions(formatOptions, lang); + /** * Get Locale object from LocaleMap cache * @param element Locale Date element @@ -82,50 +90,88 @@ const getLocale = (element: LocaleDateElement): Locale | null => { if (localeMap) { const { resolvedLocale, locale, formatOptions, amPm, showSeconds, timepicker, lang } = localeMap; // calculate Diff with cache to check if the object has changed - if ((locale && locale === element.locale) || (!locale && !element.locale && lang === getLang(element) && ( - (formatOptions && formatOptions === element.formatOptions) - || (!formatOptions && !element.formatOptions && timepicker === hasTimepicker(element) && amPm === hasAmPm(element) && showSeconds === hasSeconds(element)) - ))) { - return resolvedLocale; + // Locale includes all required information for localisation + // and takes priority of other properties + if (locale || element.locale) { + return locale === element.locale ? resolvedLocale : null; } + + // Lang has changed + if (lang !== getLang(element)) { + return null; + } + + if (formatOptions || element.formatOptions) { + // formatOptions take priority over properties + return formatOptions === element.formatOptions ? resolvedLocale : null; + } + + return timepicker === hasTimepicker(element) && amPm === hasAmPm(element) && showSeconds === hasSeconds(element) ? resolvedLocale : null; } return null; }; +/** + * Populate LocaleMap cache + * @param element Locale Date element + * @param options Locale Map options + * @returns locale Locale object + */ +const setLocaleMap = (element: LocaleDateElement, options: LocaleMapOptions): Locale => { + LocaleMap.set(element, options); + return options.resolvedLocale; +}; + /** * Set Locale object in LocaleMap cache * @param element Locale Date element + * @param options Override options if resolved from element * @returns locale Resolved Locale object */ -const setLocale = (element: LocaleDateElement): Locale => { +const setLocale = (element: LocaleDateElement, options: Intl.DateTimeFormatOptions): Locale => { + if (element.locale) { + const resolvedLocale = element.locale; + return setLocaleMap(element, { + resolvedLocale, + locale: resolvedLocale + }); + } + const lang = getLang(element); const formatOptions = element.formatOptions; + if (formatOptions) { + return setLocaleMap(element, { + resolvedLocale: localeFromOptions(lang, formatOptions), + lang, + formatOptions + }); + } + const timepicker = hasTimepicker(element); const showSeconds = hasSeconds(element); const amPm = hasAmPm(element); - const locale = element.locale; - const resolvedLocale = locale || resolveLocaleFromElement(lang, formatOptions, timepicker, amPm, showSeconds); - LocaleMap.set(element, { - resolvedLocale, - locale, - formatOptions, + return setLocaleMap(element, { + resolvedLocale: localeFromProperties(lang, timepicker, amPm, showSeconds, options), + lang, amPm, timepicker, - showSeconds, - lang + showSeconds }); - - return resolvedLocale; }; /** * Resolve locale based on element parameters * @param element Locale Date element + * @param [options] Override options if resolved from element * @returns locale Resolved Locale object */ -const resolvedLocale = (element: LocaleDateElement): Locale => getLocale(element) || setLocale(element); +const resolvedLocale = (element: LocaleDateElement, options: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'short', + day: 'numeric' +}): Locale => getLocale(element) || setLocale(element, options); export { LocaleDateElement, diff --git a/packages/elements/src/datetime-picker/index.ts b/packages/elements/src/datetime-picker/index.ts index 79126625c7..a8b5103cb8 100644 --- a/packages/elements/src/datetime-picker/index.ts +++ b/packages/elements/src/datetime-picker/index.ts @@ -46,11 +46,7 @@ import { getCurrentSegment, formatToView, formatToDate, - formatToTime, - hasTimePicker, - hasSeconds, - hasDatePicker, - hasAmPm + formatToTime } from './utils.js'; import { preload } from '../icon/index.js'; @@ -146,7 +142,8 @@ export class DatetimePicker extends ControlElement implements MultiValue { `; } - private lazyRendered = false; /* speed up rendering by not populating popup window on first load */ + // speed up rendering by not populating popup window on first load + private lazyRendered = false; /** * Set minimum date. @@ -222,7 +219,6 @@ export class DatetimePicker extends ControlElement implements MultiValue { /** * Set the first day of the week. * 0 - for Sunday, 6 - for Saturday - * @param firstDayOfWeek The first day of the week */ @property({ type: Number, attribute: 'first-day-of-week' }) public firstDayOfWeek: number | null = null; @@ -252,6 +248,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { /** * Current date time value * @param value Calendar value + * @type {string} * @default - */ @property({ type: String }) @@ -414,38 +411,45 @@ export class DatetimePicker extends ControlElement implements MultiValue { private inputRef: Ref = createRef(); private inputToRef: Ref = createRef(); + /** + * Get resolved locale for current element + */ + protected get resolvedLocale (): Locale { + return resolvedLocale(this); + } + /** * Returns true if Locale has time picker */ protected get hasTimePicker (): boolean { - return hasTimePicker(resolvedLocale(this).options); + return this.resolvedLocale.hasTimePicker; } /** * Returns true if Locale has seconds */ protected get hasSeconds (): boolean { - return hasSeconds(resolvedLocale(this).options); + return this.resolvedLocale.hasSeconds; } /** * Returns true if Locale has date picker */ protected get hasDatePicker (): boolean { - return hasDatePicker(resolvedLocale(this).options); + return this.resolvedLocale.hasDatePicker; } /** * Returns true if Locale has 12h time format */ protected get hasAmPm (): boolean { - return hasAmPm(resolvedLocale(this).options); + return this.resolvedLocale.hasAmPm; } /** * Called after render life-cycle finished * @param changedProperties Properties which have changed - * @return {void} + * @returns {void} */ protected updated (changedProperties: PropertyValues): void { super.updated(changedProperties); @@ -477,9 +481,10 @@ export class DatetimePicker extends ControlElement implements MultiValue { if (changedProperties.has('opened') && this.opened) { this.lazyRendered = true; } + // make sure to close popup for disabled if (this.opened && !this.canOpenPopup) { - this.opened = false; /* this cannot be nor stopped nor listened */ + this.opened = false; } if (this.shouldValidateInput(changedProperties)) { @@ -574,7 +579,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @returns {void} */ protected override warnInvalidValue (value: string): void { - new WarningNotice(`The specified value "${value}" does not conform to the required format. The format is ${resolvedLocale(this).isoFormat}.`).once(); + new WarningNotice(`The specified value "${value}" does not conform to the required format. The format is ${this.resolvedLocale.isoFormat}.`).once(); } /** @@ -583,7 +588,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @returns valid Validity */ protected isValidValue (value: string): boolean { - return value === '' ? true : typeof value === 'string' && getFormat(value) === resolvedLocale(this).isoFormat; + return value === '' ? true : typeof value === 'string' && getFormat(value) === this.resolvedLocale.isoFormat; } /** @@ -769,7 +774,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { private async synchroniseCalendarValues (values: string[]): Promise { const segments = values.map(value => value ? toSegment(value) : null); const oldSegments = this.values.map(value => value ? toSegment(value) : null); - const newValues = segments.map((segment, idx) => segment ? format(Object.assign(getCurrentSegment(), oldSegments[idx] || {}, segment), resolvedLocale(this).isoFormat) : ''); + const newValues = segments.map((segment, idx) => segment ? format(Object.assign(getCurrentSegment(), oldSegments[idx] || {}, segment), this.resolvedLocale.isoFormat) : ''); this.notifyValuesChange(newValues); @@ -916,7 +921,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { max=${ifDefined(this.max || undefined)} ?disabled=${this.disabled} ?readonly=${this.readonly || this.inputDisabled} - .locale=${resolvedLocale(this)} + .locale=${this.resolvedLocale} .value=${live(isTo ? (this.values[1] || '') : (this.values[0] || ''))} .placeholder=${this.placeholder} @value-changed=${this.onInputValueChanged} diff --git a/packages/elements/src/datetime-picker/utils.ts b/packages/elements/src/datetime-picker/utils.ts index 00825a4623..f1db62db2a 100644 --- a/packages/elements/src/datetime-picker/utils.ts +++ b/packages/elements/src/datetime-picker/utils.ts @@ -45,41 +45,9 @@ const formatToTime = (value?: string | null, includeSeconds = false): string => */ const formatToView = (value?: string | null): string => value ? format(toSegment(value), DateFormat.yyyyMM) : ''; -/** - * Check if options have second information - * @param options Intl DateTime format options - * @returns hasSeconds true if options have second or millisecond - */ -const hasSeconds = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.second || !!options.fractionalSecondDigits; - -/** - * Check if options have timepicker information - * @param options Intl DateTime format options - * @returns hasTimePicker true if options have hour, minute, second or millisecond - */ -const hasTimePicker = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.hour || !!options.minute || hasSeconds(options); - -/** - * Check if options use 12h format - * @param options Intl DateTime format options - * @returns hasAmPm true if options use 12h format - */ -const hasAmPm = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.hour12; - -/** - * Check if options have date information - * @param options Intl DateTime format options - * @returns hasDatePicker true if options have year, month, day or weekday - */ -const hasDatePicker = (options: Intl.ResolvedDateTimeFormatOptions): boolean => !!options.year || !!options.month || !!options.day || !!options.weekday; - export { getCurrentSegment, formatToDate, formatToTime, - formatToView, - hasTimePicker, - hasSeconds, - hasDatePicker, - hasAmPm + formatToView }; diff --git a/packages/utils/src/date/Locale.ts b/packages/utils/src/date/Locale.ts index 28f310767b..b2b0915d7a 100644 --- a/packages/utils/src/date/Locale.ts +++ b/packages/utils/src/date/Locale.ts @@ -467,6 +467,38 @@ class Locale { return this._resolvedFormat; } + /** + * Check if options have date information + * @returns hasDatePicker true if options have year, month, day or weekday + */ + public get hasDatePicker (): boolean { + return !!this.options.year || !!this.options.month || !!this.options.day || !!this.options.weekday; + } + + /** + * Check if options have timepicker information + * @returns hasTimePicker true if options have hour, minute, second or millisecond + */ + public get hasTimePicker (): boolean { + return !!this.options.hour || !!this.options.minute || this.hasSeconds; + } + + /** + * Check if options have second information + * @returns hasSeconds true if options have second or millisecond + */ + public get hasSeconds (): boolean { + return !!this.options.second || !!this.options.fractionalSecondDigits; + } + + /** + * Check if options use 12h format + * @returns hasAmPm true if options use 12h format + */ + public get hasAmPm (): boolean { + return !!this.options.hour12; + } + /** * Try to parse localised date string into ISO date/time/datetime string * Throw an error if value is invalid From 56c7fe915f45815ad8907c780cacb02a2d5e462f Mon Sep 17 00:00:00 2001 From: AG <81616437+goremikins@users.noreply.github.com> Date: Thu, 11 Aug 2022 13:13:01 +0100 Subject: [PATCH 14/21] feat(datetime-picker): remove duplex consecutive mode --- .../src/datetime-picker/__demo__/index.html | 18 ++---- .../elements/src/datetime-picker/index.ts | 60 ++++--------------- .../elements/src/datetime-picker/types.ts | 3 - 3 files changed, 16 insertions(+), 65 deletions(-) diff --git a/packages/elements/src/datetime-picker/__demo__/index.html b/packages/elements/src/datetime-picker/__demo__/index.html index 875580cb57..af0d38cc1b 100644 --- a/packages/elements/src/datetime-picker/__demo__/index.html +++ b/packages/elements/src/datetime-picker/__demo__/index.html @@ -50,15 +50,11 @@

Range + Duplex

Reset View

-

- Single - Duplex - Duplex Split -

Timepicker AM-PM mode @@ -181,13 +177,9 @@ document.getElementById('range').addEventListener('checked-changed', ({ detail: { value } }) => { setRange(value); }); - document.querySelectorAll('ef-radio-button[name=duplex]').forEach((ch, i) => { - ch.addEventListener('checked-changed', ({ detail: { value } }) => { - if (value) { - dateTimePicker.view = undefined; - dateTimePicker.duplex = i === 0 ? undefined : i === 1 ? '' : 'split'; - } - }); + + document.getElementById('duplex').addEventListener('checked-changed', ({ detail: { value } }) => { + dateTimePicker.duplex = value; }); const setTimePicker = (value) => { @@ -437,7 +429,7 @@ } - +

Today 1 Week diff --git a/packages/elements/src/datetime-picker/index.ts b/packages/elements/src/datetime-picker/index.ts index a8b5103cb8..25fe278791 100644 --- a/packages/elements/src/datetime-picker/index.ts +++ b/packages/elements/src/datetime-picker/index.ts @@ -17,7 +17,6 @@ import { live } from '@refinitiv-ui/core/directives/live.js'; import { VERSION } from '../version.js'; import type { OpenedChangedEvent, ViewChangedEvent, ValueChangedEvent, ErrorChangedEvent } from '../events'; import type { - DatetimePickerDuplex, DatetimePickerFilter } from './types'; import '../calendar/index.js'; @@ -58,8 +57,7 @@ import '@refinitiv-ui/phrasebook/locale/en/datetime-picker.js'; preload('calendar', 'down', 'left', 'right'); /* preload calendar icons for faster loading */ export type { - DatetimePickerFilter, - DatetimePickerDuplex + DatetimePickerFilter }; const POPUP_POSITION = ['bottom-start', 'top-start', 'bottom-end', 'top-end', 'bottom-middle', 'top-middle']; @@ -320,10 +318,9 @@ export class DatetimePicker extends ControlElement implements MultiValue { /** * Display two calendar pickers. - * @type {"" | "consecutive" | "split"} */ - @property({ type: String, reflect: true }) - public duplex: DatetimePickerDuplex | null = null; + @property({ type: Boolean, reflect: true }) + public duplex = false; /** * Set the current calendar view. @@ -364,18 +361,17 @@ export class DatetimePicker extends ControlElement implements MultiValue { const now = format(new Date(), DateFormat.yyyyMM); const from = formatToView(this.values[0]); - if (!this.isDuplex()) { + if (!this.duplex) { return [from || now]; } const to = formatToView(this.values[1]); // default duplex mode - if (this.isDuplexConsecutive() || !from || !to || from === to || isBefore(to, from)) { + if (!from || !to || from === to || isBefore(to, from)) { return this.composeViews(from || to || now, !from && to ? 1 : 0, []); } - // duplex split if as from and to return [from, to]; } @@ -561,7 +557,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { */ private filterInvalidViews (views: string[]): string[] { // views must match in duplex mode - if (views.length !== (this.isDuplex() ? 2 : 1)) { + if (views.length !== (this.duplex ? 2 : 1)) { return []; } @@ -591,30 +587,6 @@ export class DatetimePicker extends ControlElement implements MultiValue { return value === '' ? true : typeof value === 'string' && getFormat(value) === this.resolvedLocale.isoFormat; } - /** - * Return true if calendar is in duplex mode - * @returns duplex - */ - private isDuplex (): boolean { - return this.isDuplexSplit() || this.isDuplexConsecutive(); - } - - /** - * Return true if calendar is in duplex split mode - * @returns duplex split - */ - private isDuplexSplit (): boolean { - return this.duplex === 'split'; - } - - /** - * Return true if calendar is in duplex consecutive mode - * @returns duplex consecutive - */ - private isDuplexConsecutive (): boolean { - return this.duplex === '' || this.duplex === 'consecutive'; - } - /** * Construct view collection * @param view The view that has changed @@ -625,20 +597,10 @@ export class DatetimePicker extends ControlElement implements MultiValue { private composeViews (view: string, index: number, views = this.views): string[] { view = formatToView(view); - if (!this.isDuplex()) { + if (!this.duplex) { return [view]; } - if (this.isDuplexConsecutive()) { - if (index === 0) { /* from */ - return [view, addMonths(view, 1)]; - } - else { /* to */ - return [subMonths(view, 1), view]; - } - } - - // duplex split if (index === 0) { /* from. to must be after or the same */ let after = views[1] || addMonths(view, 1); if (isBefore(after, view)) { @@ -740,13 +702,13 @@ export class DatetimePicker extends ControlElement implements MultiValue { // in duplex mode, avoid jumping on views // Therefore if any of values have changed, save the current view - if (this.isDuplex() && this.calendarRef.value && this.calendarToRef.value) { + if (this.duplex && this.calendarRef.value && this.calendarToRef.value) { this.notifyViewsChange([this.calendarRef.value.view, this.calendarToRef.value.view]); } // Close popup if there is no time picker const newValues = this.values; - if (!this.timepicker && newValues[0] && (this.range ? newValues[1] : true)) { + if (!this.timepicker && newValues[0] && (!this.range || newValues[1])) { this.setOpened(false); } } @@ -867,7 +829,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { ${ref(isTo ? this.calendarToRef : this.calendarRef)} part="calendar" lang=${ifDefined(this.lang || undefined)} - .fillCells=${!this.isDuplex()} + .fillCells=${!this.duplex} .range=${this.range} .multiple=${this.multiple} .min=${ifDefined(formatToDate(this.min) || undefined)} @@ -888,7 +850,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { private get calendarsTemplate (): TemplateResult { return html` ${this.getCalendarTemplate()} - ${this.isDuplex() ? this.getCalendarTemplate(true) : undefined} + ${this.duplex ? this.getCalendarTemplate(true) : undefined} `; } diff --git a/packages/elements/src/datetime-picker/types.ts b/packages/elements/src/datetime-picker/types.ts index 4e2a775743..bef5b197f1 100644 --- a/packages/elements/src/datetime-picker/types.ts +++ b/packages/elements/src/datetime-picker/types.ts @@ -2,9 +2,6 @@ import type { CalendarFilter as DatetimePickerFilter } from '../calendar'; -type DatetimePickerDuplex = '' | 'consecutive' | 'split'; - export { - DatetimePickerDuplex, DatetimePickerFilter }; From 9eb3feb9f48086ff3b9f9ba7f0cfdb891ac94a49 Mon Sep 17 00:00:00 2001 From: AG <81616437+goremikins@users.noreply.github.com> Date: Thu, 11 Aug 2022 13:22:54 +0100 Subject: [PATCH 15/21] test(datetime-picker): remove duplex consecutive mode --- .../__snapshots__/DatetimePicker.md | 9 ---- .../__test__/datetime-picker.default.test.js | 2 +- .../__test__/datetime-picker.view.test.js | 48 +++---------------- 3 files changed, 8 insertions(+), 51 deletions(-) diff --git a/packages/elements/src/datetime-picker/__snapshots__/DatetimePicker.md b/packages/elements/src/datetime-picker/__snapshots__/DatetimePicker.md index 04c5ec342a..d85d1092b9 100644 --- a/packages/elements/src/datetime-picker/__snapshots__/DatetimePicker.md +++ b/packages/elements/src/datetime-picker/__snapshots__/DatetimePicker.md @@ -58,7 +58,6 @@ opened="" part="list" role="dialog" - style="z-index: 103; pointer-events: auto;" tabindex="-1" with-shadow="" > @@ -136,7 +135,6 @@ opened="" part="list" role="dialog" - style="z-index: 103; pointer-events: auto;" tabindex="-1" with-shadow="" > @@ -205,7 +203,6 @@ opened="" part="list" role="dialog" - style="z-index: 103; pointer-events: auto;" tabindex="-1" with-shadow="" > @@ -280,7 +277,6 @@ opened="" part="list" role="dialog" - style="z-index: 103; pointer-events: auto;" tabindex="-1" with-shadow="" > @@ -356,7 +352,6 @@ opened="" part="list" role="dialog" - style="z-index: 103; pointer-events: auto;" tabindex="-1" with-shadow="" > @@ -442,7 +437,6 @@ opened="" part="list" role="dialog" - style="z-index: 103; pointer-events: auto;" tabindex="-1" with-shadow="" > @@ -527,7 +521,6 @@ opened="" part="list" role="dialog" - style="z-index: 103; pointer-events: auto;" tabindex="-1" with-shadow="" > @@ -603,7 +596,6 @@ opened="" part="list" role="dialog" - style="z-index: 103; pointer-events: auto;" tabindex="-1" with-shadow="" > @@ -671,7 +663,6 @@ opened="" part="list" role="dialog" - style="z-index: 103; pointer-events: auto;" tabindex="-1" with-shadow="" > diff --git a/packages/elements/src/datetime-picker/__test__/datetime-picker.default.test.js b/packages/elements/src/datetime-picker/__test__/datetime-picker.default.test.js index 530ca6771a..1567f05183 100644 --- a/packages/elements/src/datetime-picker/__test__/datetime-picker.default.test.js +++ b/packages/elements/src/datetime-picker/__test__/datetime-picker.default.test.js @@ -83,7 +83,7 @@ describe('datetime-picker/DatetimePicker', () => { expect(el.inputDisabled).to.be.equal(false); expect(el.popupDisabled).to.be.equal(false); expect(el.timepicker).to.be.equal(false); - expect(el.duplex).to.be.equal(null); + expect(el.duplex).to.be.equal(false); expect(el.readonly).to.be.equal(false); expect(el.disabled).to.be.equal(false); expect(el.placeholder).to.be.equal(''); diff --git a/packages/elements/src/datetime-picker/__test__/datetime-picker.view.test.js b/packages/elements/src/datetime-picker/__test__/datetime-picker.view.test.js index 507df73369..8299d3592e 100644 --- a/packages/elements/src/datetime-picker/__test__/datetime-picker.view.test.js +++ b/packages/elements/src/datetime-picker/__test__/datetime-picker.view.test.js @@ -18,11 +18,6 @@ describe('datetime-picker/View', () => { expect(el.views[0]).to.be.equal(formatToView(now), 'Default view duplex from should be set to this month'); expect(el.views[1]).to.be.equal(formatToView(addMonths(now, 1)), 'Default view duplex to should be set to next month'); }); - it('Check default view duplex=split', async () => { - const el = await fixture(''); - expect(el.views[0]).to.be.equal(formatToView(now), 'Default view duplex split from should be set to this month'); - expect(el.views[1]).to.be.equal(formatToView(addMonths(now, 1)), 'Default view duplex split to should be set to next month'); - }); it('Check view when value set', async () => { const el = await fixture(''); expect(el.view).to.be.equal('2020-04', 'View should be adjusted to value'); @@ -30,11 +25,6 @@ describe('datetime-picker/View', () => { it('Check duplex view when values set', async () => { const el = await fixture(''); expect(el.views[0]).to.be.equal('2020-04', 'View from should be adjusted to from value'); - expect(el.views[1]).to.be.equal('2020-05', 'View to should be followed by from value'); - }); - it('Check duplex="split" view when values set', async () => { - const el = await fixture(''); - expect(el.views[0]).to.be.equal('2020-04', 'View from should be adjusted to from value'); expect(el.views[1]).to.be.equal('2020-06', 'View to should be adjusted to to value'); }); it('View changes when typing the value', async () => { @@ -56,20 +46,13 @@ describe('datetime-picker/View', () => { expect(el.views[0]).to.be.equal('2020-04', 'Duplex: view from did not change when typing text'); expect(el.views[1]).to.be.equal('2020-05', 'Duplex: view to did not change when typing text'); }); - it('Duplex split view changes when typing the value', async () => { - const el = await fixture(''); - typeText(inputElement(el), '2020-04-21'); - await elementUpdated(el); - expect(el.views[0]).to.be.equal('2020-04', 'Duplex split: view from did not change when typing text'); - expect(el.views[1]).to.be.equal('2020-05', 'Duplex split: view to did not change when typing text'); - }); - it('Duplex split range view changes when typing the value', async () => { - const el = await fixture(''); + it('Duplex range view changes when typing the value', async () => { + const el = await fixture(''); typeText(inputElement(el), '2020-01-21'); typeText(inputToElement(el), '2020-04-21'); await elementUpdated(el); - expect(el.views[0]).to.be.equal('2020-01', 'Duplex split range: view from did not change when typing text'); - expect(el.views[1]).to.be.equal('2020-04', 'Duplex split range: view to did not change when typing text'); + expect(el.views[0]).to.be.equal('2020-01', 'Duplex range: view from did not change when typing text'); + expect(el.views[1]).to.be.equal('2020-04', 'Duplex range: view to did not change when typing text'); }); it('Setting invalid view should reset view and warn a user', async () => { const el = await fixture(''); @@ -78,14 +61,14 @@ describe('datetime-picker/View', () => { expect(el.view).to.be.equal(formatToView(now), 'Invalid view should reset view'); }); it('Views are propagated to calendars', async () => { - const el = await fixture(''); + const el = await fixture(''); el.views = ['2020-01', '2020-04']; await elementUpdated(el); expect(calendarElement(el).view).to.be.equal('2020-01', 'From view is not propagated to calendar'); expect(calendarToElement(el).view).to.be.equal('2020-04', 'To view is not propagated to calendar'); }); it('Passing empty string should reset views to default', async () => { - const el = await fixture(''); + const el = await fixture(''); el.view = ''; await elementUpdated(el); expect(el.views[0]).to.be.equal(formatToView(now), 'View from is not reset'); @@ -100,24 +83,7 @@ describe('datetime-picker/View', () => { expect(el.view).to.be.equal('2020-05', 'View did not change on next click'); }); it('In duplex mode calendar view should be in sync', async () => { - const el = await fixture(''); - const calendarFrom = calendarElement(el); - const calendarTo = calendarToElement(el); - await elementUpdated(calendarFrom); - await elementUpdated(calendarTo); - calendarClickNext(calendarFrom); - await elementUpdated(); - expect(calendarFrom.view).to.equal('2020-05', 'Calendar from is not in sync'); - expect(calendarTo.view).to.equal('2020-06', 'Calendar to is not in sync'); - expect(String(el.views)).to.equal('2020-05,2020-06', 'Clicking next on from calendar did not synchronise views'); - calendarClickNext(calendarTo); - await elementUpdated(); - expect(calendarFrom.view).to.equal('2020-06', 'Calendar from is not in sync'); - expect(calendarTo.view).to.equal('2020-07', 'Calendar to is not in sync'); - expect(String(el.views)).to.equal('2020-06,2020-07', 'Clicking next on to calendar did not synchronise views'); - }); - it('In duplex="split" mode calendar view should be in sync', async () => { - const el = await fixture(''); + const el = await fixture(''); el.views = ['2020-04', '2020-05']; await elementUpdated(el); const calendarFrom = calendarElement(el); From c7cdfda4d6dab39934d04b75c0a74cde8aa91b1e Mon Sep 17 00:00:00 2001 From: AG <81616437+goremikins@users.noreply.github.com> Date: Mon, 15 Aug 2022 11:09:33 +0100 Subject: [PATCH 16/21] feat(datetime-picker): independent From & To calendars in duplex/range mode --- .../src/datetime-picker/__demo__/index.html | 13 ++++++---- .../elements/src/datetime-picker/index.ts | 26 +++++++++++++++---- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/elements/src/datetime-picker/__demo__/index.html b/packages/elements/src/datetime-picker/__demo__/index.html index af0d38cc1b..69cbe08420 100644 --- a/packages/elements/src/datetime-picker/__demo__/index.html +++ b/packages/elements/src/datetime-picker/__demo__/index.html @@ -145,6 +145,12 @@ setConsole(); + const eventLog = []; + const clearEventLog = () => { + eventLog.length = 0; + document.getElementById('events').value = eventLog.join('\n'); + }; + const resetValue = () => { dateValue.value = ''; dateFrom.value = ''; @@ -153,6 +159,7 @@ dateValue.view = ''; dateFrom.view = ''; dateTo.view = ''; + clearEventLog(); }; const setRange = (value) => { @@ -379,7 +386,6 @@ dateTimePicker.popupDisabled = value || undefined; }); - const eventLog = []; const onEvent = (event) => { eventLog.unshift(`${event.type}: ${JSON.stringify(event.detail)}`); if (eventLog.length > 50) { @@ -391,10 +397,7 @@ dateTimePicker.addEventListener('opened-changed', onEvent); dateTimePicker.addEventListener('value-changed', onEvent); dateTimePicker.addEventListener('view-changed', onEvent); - document.getElementById('clear-events').addEventListener('click', () => { - eventLog.length = 0; - document.getElementById('events').value = eventLog.join('\n'); - }); + document.getElementById('clear-events').addEventListener('click', clearEventLog); diff --git a/packages/elements/src/datetime-picker/index.ts b/packages/elements/src/datetime-picker/index.ts index 25fe278791..cdf5e1ecb4 100644 --- a/packages/elements/src/datetime-picker/index.ts +++ b/packages/elements/src/datetime-picker/index.ts @@ -368,7 +368,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { const to = formatToView(this.values[1]); // default duplex mode - if (!from || !to || from === to || isBefore(to, from)) { + if (!from || !to || isBefore(to, from)) { return this.composeViews(from || to || now, !from && to ? 1 : 0, []); } @@ -697,7 +697,19 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @returns {void} */ private onCalendarValueChanged (event: ValueChangedEvent): void { - const values = (event.target as Calendar).values; + const target = event.target as Calendar; + let values; + + if (this.range && this.duplex) { + // 0 - from, single; 1 - to + const index = event.target === this.calendarToRef.value ? 1 : 0; + values = [...this.values]; + values[index] = target.value; + } + else { + values = target.values; + } + void this.synchroniseCalendarValues(values); // in duplex mode, avoid jumping on views @@ -815,7 +827,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { ${ref(isTo ? this.timepickerToRef : this.timepickerRef)} part="time-picker" .amPm=${this.hasAmPm} - .value=${formatToTime(isTo ? (this.values[1] || '') : (this.values[0] || ''), this.hasSeconds)} + .value=${formatToTime(isTo ? this.values[1] : this.values[0], this.hasSeconds)} @value-changed=${this.onTimePickerValueChanged}>`; } @@ -825,19 +837,23 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @returns template result */ private getCalendarTemplate (isTo = false): TemplateResult { + const values = this.range && this.duplex + ? [formatToDate(isTo ? this.values[1] : this.values[0])] + : this.values.map(value => formatToDate(value)); + return html` formatToDate(value))} + .values=${values} .filter=${this.filter} .view=${isTo ? (this.views[1] || '') : (this.views[0] || '')} @view-changed=${this.onCalendarViewChanged} From 3214411d9ccac4ea090248126a91f911c94e8f92 Mon Sep 17 00:00:00 2001 From: Sarin Udompanish Date: Tue, 16 Aug 2022 16:00:02 +0700 Subject: [PATCH 17/21] fix(datetime-picker): change focus outline styles of calendar button --- .../src/custom-elements/ef-datetime-picker.less | 5 +++++ .../halo-theme/src/custom-elements/ef-datetime-picker.less | 3 +++ 2 files changed, 8 insertions(+) diff --git a/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less b/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less index 96c6ea0b99..e3771266f3 100644 --- a/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less +++ b/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less @@ -47,6 +47,10 @@ background: none; color: inherit; font-size: inherit; + + &:focus { + outline: @input-border-width @control-border-style @input-focused-border-color; + } } [part=calendar] { @@ -82,3 +86,4 @@ height: 1px; } } + diff --git a/packages/halo-theme/src/custom-elements/ef-datetime-picker.less b/packages/halo-theme/src/custom-elements/ef-datetime-picker.less index 768b6bd634..acc46d2eab 100644 --- a/packages/halo-theme/src/custom-elements/ef-datetime-picker.less +++ b/packages/halo-theme/src/custom-elements/ef-datetime-picker.less @@ -26,6 +26,9 @@ & when (@variant = light) { color: @control-border-color; } + &:focus { + outline-width: 2px; + } } &[warning]:not([focused]) { From 46967cf44025751bd63c298269b12e5811187d5c Mon Sep 17 00:00:00 2001 From: Sarin Udompanish Date: Tue, 16 Aug 2022 18:05:08 +0700 Subject: [PATCH 18/21] fix(datetime-picker): change focus selector to focus-visible --- .../elemental-theme/src/custom-elements/ef-datetime-picker.less | 2 +- packages/halo-theme/src/custom-elements/ef-datetime-picker.less | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less b/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less index e3771266f3..065f2bd5fa 100644 --- a/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less +++ b/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less @@ -48,7 +48,7 @@ color: inherit; font-size: inherit; - &:focus { + &:focus-visible { outline: @input-border-width @control-border-style @input-focused-border-color; } } diff --git a/packages/halo-theme/src/custom-elements/ef-datetime-picker.less b/packages/halo-theme/src/custom-elements/ef-datetime-picker.less index acc46d2eab..72066b0048 100644 --- a/packages/halo-theme/src/custom-elements/ef-datetime-picker.less +++ b/packages/halo-theme/src/custom-elements/ef-datetime-picker.less @@ -26,7 +26,7 @@ & when (@variant = light) { color: @control-border-color; } - &:focus { + &:focus-visible { outline-width: 2px; } } From d4c94bbd42a51cd311e3b50ec58a724865bcf8bc Mon Sep 17 00:00:00 2001 From: Sarin Udompanish Date: Wed, 17 Aug 2022 17:45:20 +0700 Subject: [PATCH 19/21] fix(datetime-picker): change calendar button styles to similar with combo-box --- .../custom-elements/ef-datetime-picker.less | 4 -- .../custom-elements/ef-datetime-picker.less | 62 ++++++++++++++----- 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less b/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less index 065f2bd5fa..1a18f34104 100644 --- a/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less +++ b/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less @@ -47,10 +47,6 @@ background: none; color: inherit; font-size: inherit; - - &:focus-visible { - outline: @input-border-width @control-border-style @input-focused-border-color; - } } [part=calendar] { diff --git a/packages/halo-theme/src/custom-elements/ef-datetime-picker.less b/packages/halo-theme/src/custom-elements/ef-datetime-picker.less index 72066b0048..76f5c26ef6 100644 --- a/packages/halo-theme/src/custom-elements/ef-datetime-picker.less +++ b/packages/halo-theme/src/custom-elements/ef-datetime-picker.less @@ -23,11 +23,51 @@ [part=button] { color: inherit; + outline: none; + border-left: @input-border; + border-left-color: inherit; + & when (@variant = light) { color: @control-border-color; } - &:focus-visible { - outline-width: 2px; + + &:hover, &:focus { + border-left-color: @input-focused-border-color; + color: @button-hover-text-color; + background: @button-hover-background-color; + + &::after { // draws faux border on control + content: ''; + display: block; + position: absolute; + border-color: transparent; + top: -@input-border-width; + right: -@input-border-width; + bottom: -@input-border-width; + left: -@input-border-width; + border: @input-border; + border-color: @input-focused-border-color; + pointer-events: none; + } + } + } + + &[disabled] { + border-color: @input-disabled-border-color; + color: @input-disabled-text-color; + } + + &[disabled], &[popup-disabled] { + [part=button] { + color: @input-disabled-text-color; + } + } + + &[readonly]:not([focused]) { + border-color: @input-disabled-border-color; + + [part=button] { + color: @input-disabled-text-color; } } @@ -57,16 +97,11 @@ border-color: fade(@control-hover-error-color, 50%); } - &[disabled], &[popup-disabled] { - [part=button] { - color: @input-disabled-text-color - } - } - &[focused], - &[focused][error][warning], - &:not([disabled]):not([popup-disabled]):not([error]):not([warning]):hover { - [part=button] { + &:not([readonly]):not([popup-disabled]):not([error]):not([warning]):hover { + color: @input-hover-text-color; + + [part=button]:not(:hover):not(:focus) { color: @scheme-color-secondary; & when (@variant = light) { @@ -74,9 +109,4 @@ } } } - - &:not([readonly]):not([error]):not([warning]):not([focused]):hover { - border-color: @input-hover-border-color; - color: @input-hover-text-color; - } } From f240bdc9261b737a11e877583bb236bfb559ccc7 Mon Sep 17 00:00:00 2001 From: Sarin Udompanish Date: Mon, 22 Aug 2022 11:10:44 +0700 Subject: [PATCH 20/21] fix(datetime-picker): resolve conflicts --- .../src/custom-elements/ef-datetime-picker.less | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/halo-theme/src/custom-elements/ef-datetime-picker.less b/packages/halo-theme/src/custom-elements/ef-datetime-picker.less index 82db0a77c4..bc1828a3d4 100644 --- a/packages/halo-theme/src/custom-elements/ef-datetime-picker.less +++ b/packages/halo-theme/src/custom-elements/ef-datetime-picker.less @@ -97,16 +97,10 @@ border-color: fade(@control-hover-error-color, 50%); } - &[disabled], &[popup-disabled] { - [part=button] { - color: @input-disabled-text-color - } - } - &[focused], &[focused][error][warning], &:not([disabled]):not([popup-disabled]):not([error]):not([warning]):hover { - [part=button] { + [part=button]:not(:hover):not(:focus) { color: @scheme-color-secondary; & when (@variant = light) { From dd0c2c15d84efba6c2a9cdd5c974b67583e85650 Mon Sep 17 00:00:00 2001 From: Sarin Udompanish Date: Mon, 22 Aug 2022 11:58:52 +0700 Subject: [PATCH 21/21] chore(datetime-picker): update package lock --- package-lock.json | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 00e55179f5..f9f1367c06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9820,13 +9820,6 @@ "node": ">=4.0" } }, - "node_modules/estr,averse": { - "version": "5.3.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, "node_modules/estree-walker": { "version": "1.0.1", "dev": true, @@ -27704,9 +27697,6 @@ "estraverse": "^5.2.0" } }, - "estraverse": { - "version": "5.3.0" - }, "estree-walker": { "version": "1.0.1", "dev": true