From 51e653aeeee2f61618d2d825ccce0bb576410cca Mon Sep 17 00:00:00 2001 From: Sarin Udompanish Date: Thu, 21 Jul 2022 15:01:30 +0700 Subject: [PATCH 1/6] feat: replace input with datetimeField --- .../custom-elements/ef-datetime-picker.less | 4 +- .../src/datetime-picker/__demo__/index.html | 11 - .../elements/src/datetime-picker/index.ts | 464 ++++++++---------- .../elements/src/datetime-picker/locales.ts | 70 --- 4 files changed, 215 insertions(+), 334 deletions(-) delete mode 100644 packages/elements/src/datetime-picker/locales.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 10ff8d044d..97ee89ab88 100644 --- a/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less +++ b/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less @@ -2,7 +2,7 @@ @import 'element:ef-icon'; @import 'element:ef-overlay'; @import 'element:ef-time-picker'; -@import 'element:ef-text-field'; +@import 'element:ef-datetime-field'; @import (reference) 'ef-text-field'; @@ -48,7 +48,7 @@ } &[range] { - ef-text-field { + ef-datetime-field { text-align: center; } } diff --git a/packages/elements/src/datetime-picker/__demo__/index.html b/packages/elements/src/datetime-picker/__demo__/index.html index 0789ef0f28..67a46f21fe 100644 --- a/packages/elements/src/datetime-picker/__demo__/index.html +++ b/packages/elements/src/datetime-picker/__demo__/index.html @@ -80,12 +80,6 @@ TH ZH-CN - - Default Format - do MMMM yyyy - d MMMM yyyy - Year: yyyy; Month: MMMM; Date: d -

@@ -128,7 +122,6 @@ showSeconds: dateTimePicker.showSeconds, lang: dateTimePicker.lang, placeholder: dateTimePicker.placeholder, - format: dateTimePicker.format, monthsDesc: dateTimePicker.monthsDesc, yearsDesc: dateTimePicker.yearsDesc, weekdaysOnly: dateTimePicker.weekdaysOnly, @@ -238,10 +231,6 @@ // resetValue(); dateTimePicker.lang = value ? value : ''; }); - document.getElementById('format').addEventListener('value-changed', ({ detail: { value } }) => { - resetValue(); - dateTimePicker.format = value ? value : ''; - }); document.getElementById('placeholder').addEventListener('value-changed', ({ detail: { value } }) => { resetValue(); dateTimePicker.placeholder = value ? value : ''; diff --git a/packages/elements/src/datetime-picker/index.ts b/packages/elements/src/datetime-picker/index.ts index 2575066c78..f0bcf8253a 100644 --- a/packages/elements/src/datetime-picker/index.ts +++ b/packages/elements/src/datetime-picker/index.ts @@ -7,14 +7,15 @@ import { PropertyValues, CSSResultGroup, TapEvent, - WarningNotice + WarningNotice, + DeprecationNotice } 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 { ifDefined } from '@refinitiv-ui/core/directives/if-defined.js'; import { VERSION } from '../version.js'; -import type { OpenedChangedEvent, ViewChangedEvent, ValueChangedEvent } from '../events'; +import type { OpenedChangedEvent, ViewChangedEvent, ValueChangedEvent, ErrorChangedEvent } from '../events'; import type { DatetimePickerDuplex, DatetimePickerFilter @@ -22,7 +23,7 @@ import type { import '../calendar/index.js'; import '../icon/index.js'; import '../overlay/index.js'; -import '../text-field/index.js'; +import '../datetime-field/index.js'; import '../time-picker/index.js'; import type { Icon } from '../icon'; import type { Calendar } from '../calendar'; @@ -32,23 +33,17 @@ import { getLocale, TranslatePropertyKey } from '@refinitiv-ui/translate'; -import { - getDateFNSLocale -} from './locales.js'; -import inputFormat from 'date-fns/esm/format/index.js'; -import inputParse from 'date-fns/esm/parse/index.js'; -import isValid from 'date-fns/esm/isValid/index.js'; import { addMonths, subMonths, isAfter, isBefore, isValidDate, - isValidDateTime, + getFormat, DateFormat, - DateTimeFormat, parse, - format + format, + Locale } from '@refinitiv-ui/utils/date.js'; import { @@ -61,7 +56,7 @@ 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'; preload('calendar', 'down', 'left', 'right'); /* preload calendar icons for faster loading */ @@ -72,14 +67,6 @@ export type { const POPUP_POSITION = ['bottom-start', 'top-start', 'bottom-end', 'top-end', 'bottom-middle', 'top-middle']; -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' -}; - /** * Control to pick date and time * @@ -155,7 +142,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { cursor: pointer; } :host([popup-disabled]) [part=icon], :host([readonly]) [part=icon] { - pointer-event: none; + pointer-events: none; } `; } @@ -169,10 +156,10 @@ export class DatetimePicker extends ControlElement implements MultiValue { private _min = ''; private minDate = ''; /** - * Set minimum date - * @param min date - * @default - - */ + * Set minimum date + * @param min date + * @default - + */ @property({ type: String }) public set min (min: string) { if (!this.isValidValue(min)) { @@ -194,10 +181,10 @@ export class DatetimePicker extends ControlElement implements MultiValue { private _max = ''; private maxDate = ''; /** - * Set maximum date - * @param max date - * @default - - */ + * Set maximum date + * @param max date + * @default - + */ @property({ type: String }) public set max (max: string) { if (!this.isValidValue(max)) { @@ -217,21 +204,21 @@ export class DatetimePicker extends ControlElement implements MultiValue { } /** - * Only enable weekdays - */ + * Only enable weekdays + */ @property({ type: Boolean, attribute: 'weekdays-only' }) public weekdaysOnly = false; /** - * Only enable weekends - */ + * Only enable weekends + */ @property({ type: Boolean, attribute: 'weekends-only' }) public weekendsOnly = false; /** - * Custom filter, used for enabling/disabling certain dates - * @type {DatetimePickerFilter | null} - */ + * Custom filter, used for enabling/disabling certain dates + * @type {DatetimePickerFilter | null} + */ @property({ attribute: false }) public filter: DatetimePickerFilter | null = null; @@ -244,33 +231,32 @@ export class DatetimePicker extends ControlElement implements MultiValue { public firstDayOfWeek?: number; /** - * Set to switch to range select mode - */ + * Set to switch to range select mode + */ @property({ type: Boolean, reflect: true }) public range = false; /** - * Set to switch to multiple select mode - * @ignore - * @param multiple Multiple - */ - /* istanbul ignore next */ + * Set to switch to multiple select mode + * @ignore + * @param multiple Multiple + */ @property({ type: Boolean }) public set multiple (multiple: boolean) { new WarningNotice('multiple is not currently supported').show(); } /** - * @ignore - */ + * @ignore + */ public get multiple (): boolean { return false; } /** - * Current date time value - * @param value Calendar value - * @default - - */ + * Current date time value + * @param value Calendar value + * @default - + */ @property({ type: String }) public set value (value: string) { this.values = value ? [value] : []; @@ -282,11 +268,11 @@ 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 - * @type {string[]} - * @default [] - */ + * Set multiple selected values + * @param values Values to set + * @type {string[]} + * @default [] + */ @property({ converter: { fromAttribute: function (value: string): string[] { @@ -321,10 +307,10 @@ export class DatetimePicker extends ControlElement implements MultiValue { private _placeholder = ''; /** - * Placeholder to display when no value is set - * @param placeholder Placeholder - * @default - - */ + * Placeholder to display when no value is set + * @param placeholder Placeholder + * @default - + */ @property({ type: String }) public set placeholder (placeholder: string) { const oldPlaceholder = this._placeholder; @@ -334,12 +320,12 @@ export class DatetimePicker extends ControlElement implements MultiValue { } } public get placeholder (): string { - return this._placeholder || this.format; + return this._placeholder; } /** - * Toggles the opened state of the list - */ + * Toggles the opened state of the list + */ @property({ type: Boolean, reflect: true }) public opened = false; @@ -356,70 +342,82 @@ export class DatetimePicker extends ControlElement implements MultiValue { public warning = false; /** - * Only open picker panel when calendar icon is clicked. - * Clicking on the input will no longer open the picker. - */ + * 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 - */ + * Disable input part of the picker + */ @property({ type: Boolean, attribute: 'input-disabled', reflect: true }) public inputDisabled = false; /** - * Disable the popup - */ + * Disable the popup + */ @property({ type: Boolean, attribute: 'popup-disabled', reflect: true }) public popupDisabled = false; - private _format = ''; /** - * Set the datetime format - * Based on dane-fns datetime formats - * @param format Date format - * @default - + * Set the datetime format + * Based on dane-fns datetime formats + * @ignore + * @param format Date format */ @property({ type: String }) public set format (format: string) { - const oldFormat = this._format; - if (oldFormat !== format) { - this._format = format; - this.requestUpdate('format', oldFormat); - } + new DeprecationNotice('`format` attribute and property are deprecated. Use `formatOptions` property instead.').show(); } + /** + * @ignore + */ public get format (): string { - return this._format || ( - this.timepicker - ? ( - this.showSeconds - ? (this.amPm ? INPUT_FORMAT.DATETIME_SECONDS_AM_PM : INPUT_FORMAT.DATETIME_SECONDS) - : (this.amPm ? INPUT_FORMAT.DATETIME_AM_PM : INPUT_FORMAT.DATETIME) - ) - : INPUT_FORMAT.DATE - ); + return ''; } + private _formatOptions: Intl.DateTimeFormatOptions | null = null; /** - * Toggle to display the time picker - */ + * 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; + } + + /** + * Toggle to display the time picker + */ @property({ type: Boolean, reflect: true }) public timepicker = false; /** - * Display two calendar pickers. - * @type {"" | "consecutive" | "split"} - */ + * Display two calendar pickers. + * @type {"" | "consecutive" | "split"} + */ @property({ type: String, reflect: true }) public duplex: DatetimePickerDuplex | null = null; /** - * Set the current calendar view. - * Accepted format: 'yyyy-MM' - * @param view view date - * @default - - */ + * Set the current calendar view. + * Accepted format: 'yyyy-MM' + * @param view view date + * @default - + */ @property({ type: String }) public set view (view: string) { this.views = view ? [view] : []; @@ -430,12 +428,12 @@ export class DatetimePicker extends ControlElement implements MultiValue { private _views: string[] = []; /** - * Set the current calendar views for duplex mode - * Accepted format: 'yyyy-MM' - * @param views view dates - * @type {string[]} - * @default [] - */ + * Set the current calendar views for duplex mode + * Accepted format: 'yyyy-MM' + * @param views view dates + * @type {string[]} + * @default [] + */ @property({ attribute: false }) public set views (views: string[]) { const oldViews = this._views; @@ -468,16 +466,42 @@ export class DatetimePicker extends ControlElement implements MultiValue { return [formatToView(from), formatToView(to)]; } + /** + * Format, which is based on locale + */ + 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)}`); + } + /** * Validates the input, marking the element as invalid if its value does not meet the validation criteria. * @returns {void} */ public validateInput (): void { - const hasError = this.hasError(); - if (this.error !== hasError) { - this.error = hasError; - this.notifyPropertyChange('error', this.error); - } + const hasError = !this.isFromBeforeTo(); + this.setErrorAndNotify(hasError); } /** @@ -491,8 +515,8 @@ export class DatetimePicker extends ControlElement implements MultiValue { @query('#timepicker-to') private timepickerToEl?: TimePicker | null; @query('#calendar') private calendarEl?: Calendar | null; @query('#calendar-to') private calendarToEl?: Calendar | null; - @query('#input') private inputEl?: TextField | null; - @query('#input-to') private inputToEl?: TextField | null; + @query('#input') private inputEl?: DatetimeField | null; + @query('#input-to') private inputToEl?: DatetimeField | null; /** * Updates the element @@ -512,7 +536,8 @@ export class DatetimePicker extends ControlElement implements MultiValue { this.syncInputValues(); } - if (this.shouldValidateValue(changedProperties)) { + // re-validation + if (changedProperties.has('_values') && changedProperties.get('_values') !== undefined) { this.validateInput(); } @@ -540,20 +565,42 @@ export class DatetimePicker extends ControlElement implements MultiValue { if (value === '') { return true; } - // Need to check for the attribute to cover the case when - // timepicker and value attributes are set - return (this.timepicker || this.hasAttribute('timepicker')) - ? isValidDateTime(value) - : isValidDate(value, DateFormat.yyyyMMdd); + // value format depends on locale. + return getFormat(value) === this.locale.isoFormat; } /** - * Used to show a warning when the value does not pass the validation - * @param value that is invalid - * @returns {void} - */ + * 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'); + } + + /** + * Used to show a warning when the value does not pass the validation + * @param value that is invalid + * @returns {void} + */ protected warnInvalidValue (value: string): void { - new WarningNotice(`The specified value "${value}" does not conform to the required format. The format is ${this.timepicker ? '"yyyy-MM-ddThh:mm" followed by optional ":ss" or ":ss.SSS"' : '"yyyy-MM-dd"'}.`).show(); + new WarningNotice(`${this.localName}: the specified value "${value}" does not conform to the required format. The format is '${this.locale.isoFormat}'.`).show(); } /** @@ -576,23 +623,6 @@ export class DatetimePicker extends ControlElement implements MultiValue { this.interimSegments = newSegments; } - /** - * Check if the value needs re-validation against min/max and format - * @param changedProperties Properties which have changed - * @returns needs re-validation - */ - private shouldValidateValue (changedProperties: PropertyValues): boolean { - // do not validate default value - if (changedProperties.has('_values') && changedProperties.get('_values') !== undefined - || changedProperties.has('min') && changedProperties.get('min') !== undefined - || changedProperties.has('max') && changedProperties.get('max') !== undefined - || changedProperties.has('showSeconds') && changedProperties.get('showSeconds') !== undefined) { - return true; - } - - return false; - } - /** * A helper method to make sure that only valid values are passed * Warn if passed value is invalid @@ -676,19 +706,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { return; } // input values cannot be populated off interim segments as require a valid date - // date-fns formats to local if there is time info - this.inputValues = this._segments.map(segment => this.formatSegment(segment)); - } - - /** - * Format date segment according to format and locale - * @param segment Date segment - * @returns formatted string - */ - private formatSegment (segment: DateTimeSegment): string { - return segment.value ? inputFormat(segment.getTime(), this.format, { - locale: getDateFNSLocale(getLocale(this)) - }) : ''; + this.inputValues = this._segments.map(segment => segment.value); } /** @@ -784,6 +802,18 @@ export class DatetimePicker extends ControlElement implements MultiValue { return true; } + /** + * Notify error if it has changed + * @param hasError true if the element has an error + * @returns {void} + */ + protected setErrorAndNotify (hasError: boolean): void { + if (this.error !== hasError) { + this.error = hasError; + this.notifyPropertyChange('error', this.error); + } + } + /** * Notify that values array has been changed * @param values A collection of string dates @@ -959,6 +989,16 @@ export class DatetimePicker extends ControlElement implements MultiValue { this.submitInterimSegments(); } + /** + * Run on input error-changed event + * @param event error-changed event + * @returns {void} + */ + private onInputErrorChanged (event: ErrorChangedEvent): void { + const hasError = event.detail.value; + this.setErrorAndNotify(hasError); + } + /** * Run on input focus * @returns {void} @@ -969,27 +1009,10 @@ export class DatetimePicker extends ControlElement implements MultiValue { /** * Run on input blur - * @param event blur event * @returns {void} */ - private onInputBlur (event: FocusEvent): void { + private onInputBlur (): void { this.enableInputSync(); - - // remove all code once strict formatting is supported in date-fns - const index = event.target === this.inputToEl ? 1 : 0; - const segment = this._segments[index]; - - if (!segment || !segment.value) { - return; - } - - const formattedValue = segment ? this.formatSegment(segment) : ''; - if (formattedValue !== this.inputValues[index]) { - const inputValues = [...this.inputValues]; - inputValues[index] = formattedValue; - this.inputValues = inputValues; - this.requestUpdate(); - } } /** @@ -1000,69 +1023,11 @@ export class DatetimePicker extends ControlElement implements MultiValue { private onInputValueChanged (event: ValueChangedEvent): void { const target = event.target as TextField; const index = target === this.inputToEl ? 1 : 0; /* 0 - from, single; 1 - to */ - const inputValue = target.value; - const inputValues = [...this.inputValues]; - inputValues[index] = inputValue; - this.inputValues = inputValues; - - let dateString = ''; - - if (inputValue) { - const recoveryDate = (this.interimSegments[index] || new DateTimeSegment()).getTime(); - const date = inputParse(inputValue, this.format, recoveryDate, { - locale: getDateFNSLocale(getLocale(this)) - }); - - if (isValid(date)) { - dateString = inputFormat(date, this.timepicker ? this.showSeconds ? DateTimeFormat.yyyMMddTHHmmss : DateTimeFormat.yyyMMddTHHmm : DateFormat.yyyyMMdd); - this.resetViews(); /* user input should be treated similar to manually switching the views */ - } - } - else { - this.resetViews(); - } - - this.interimSegments[index] = DateTimeSegment.fromString(dateString); + const segment = this.interimSegments[index] || new DateTimeSegment(); + this.resetViews(); + segment.dateSegment = target.value; + this.interimSegments[index] = segment; this.submitInterimSegments(); - this.validateInput(); - } - - /** - * Check if input format conforms to value format - * @returns true if valid format - */ - private isValidFormat (): boolean { - const inputValues = this.inputValues; - const values = this.values; - - // No need to format values to validate. - // If input is invalid, value is not populated - for (let i = 0; i < inputValues.length; i += 1) { - if (inputValues[i] && !values[i]) { - return false; - } - } - - return true; - } - - /** - * Check if `value` is within `min` and `max` - * @returns true if value is within - */ - private isValueWithinMinMax (): boolean { - if (this.min || this.max) { - const minTime = this.min ? parse(this.min).getTime() : -Infinity; - const maxTime = this.max ? parse(this.max).getTime() : Infinity; - for (let i = 0; i < this.values.length; i += 1) { - const valueTime = parse(this.values[i]).getTime(); - if (minTime > valueTime || maxTime < valueTime) { - return false; - } - } - } - - return true; } /** @@ -1084,14 +1049,6 @@ export class DatetimePicker extends ControlElement implements MultiValue { return true; } - /** - * Check if datetime picker has an error - * @returns true if error - */ - private hasError (): boolean { - return !(this.isValidFormat() && this.isValueWithinMinMax() && this.isFromBeforeTo()); - } - /** * Toggles the opened state of the list * @returns {void} @@ -1204,19 +1161,26 @@ export class DatetimePicker extends ControlElement implements MultiValue { */ private getInputTemplate (id: 'input' | 'input-to', value = ''): TemplateResult { return html` - - `; + @blur=${this.onInputBlur} + @value-changed=${this.onInputValueChanged} + @error-changed=${this.onInputErrorChanged}>`; } /** @@ -1224,9 +1188,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { */ private get iconTemplate (): TemplateResult { return html` - + `; } diff --git a/packages/elements/src/datetime-picker/locales.ts b/packages/elements/src/datetime-picker/locales.ts deleted file mode 100644 index 61cbe8cece..0000000000 --- a/packages/elements/src/datetime-picker/locales.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Phrasebook } from '@refinitiv-ui/phrasebook'; -import { resolveLocale, DEFAULT_LOCALE } from '@refinitiv-ui/i18n'; -import type { Locale } from 'date-fns'; - -import enGB from 'date-fns/esm/locale/en-GB/index.js'; -import enUS from 'date-fns/esm/locale/en-US/index.js'; -import de from 'date-fns/esm/locale/de/index.js'; -import es from 'date-fns/esm/locale/es/index.js'; -import fr from 'date-fns/esm/locale/fr/index.js'; -import it from 'date-fns/esm/locale/it/index.js'; -import ja from 'date-fns/esm/locale/ja/index.js'; -import ko from 'date-fns/esm/locale/ko/index.js'; -import pl from 'date-fns/esm/locale/pl/index.js'; -import ru from 'date-fns/esm/locale/ru/index.js'; -import th from 'date-fns/esm/locale/th/index.js'; -import zhCN from 'date-fns/esm/locale/zh-CN/index.js'; - -// This file is a transition between using date-fns and Intl object to format dates -// As of now, use Phraseboook to just resolve languages and locales -// and match against the date-fns locales. - -// match locales against date-fns -// This will be used with resolveLocale function -const globals = {}; -const scope = 'ef-datetime-picker'; -Phrasebook.define('en', scope, globals); -Phrasebook.define('en-GB', scope, globals); -Phrasebook.define('de', scope, globals); -Phrasebook.define('es', scope, globals); -Phrasebook.define('fr', scope, globals); -Phrasebook.define('it', scope, globals); -Phrasebook.define('ja', scope, globals); -Phrasebook.define('ko', scope, globals); -Phrasebook.define('pl', scope, globals); -Phrasebook.define('ru', scope, globals); -Phrasebook.define('th', scope, globals); -Phrasebook.define('zh', scope, globals); - -type LangMap = { - [key: string]: Locale; -} - -const locales: LangMap = { - 'en': enUS, - 'en-GB': enGB, - de, - es, - fr, - it, - ja, - ko, - pl, - ru, - th, - 'zh': zhCN -}; - -/** - * Get date-fns locale or default locale - * @param [locale] BCP47 locale tag - * @returns DateFNS Locale object - */ -const getDateFNSLocale = (locale: string): Locale => { - locale = resolveLocale(scope, locale) || DEFAULT_LOCALE; - return locales[locale] || enUS; -}; - -export { - getDateFNSLocale -}; From 24e8aac2356e05ebd1aef773b8dc2037bb0821c0 Mon Sep 17 00:00:00 2001 From: Aleksejs <81616437+goremikins@users.noreply.github.com> Date: Fri, 19 Aug 2022 15:27:09 +0100 Subject: [PATCH 2/6] Refactor datetime-picker (#413) * feat(datetime-field): make locale function public * feat(datetime-picker): refactor element * refactor(datetime-field): make resolveLocale function reusable * feat(phrasebook): add datetime-picker strings * feat(utils): add format checks to Locale object --- .../src/pages/elements/datetime-picker.md | 312 ++---- package-lock.json | 162 +-- .../custom-elements/ef-datetime-picker.less | 21 +- packages/elements/src/datetime-field/index.ts | 146 +-- .../src/datetime-field/resolvedLocale.ts | 179 ++++ .../src/datetime-picker/__demo__/index.html | 163 ++- .../{DOMStructure.md => DatetimePicker.md} | 423 ++++++-- .../__test__/datetime-picker.default.test.js | 131 +-- .../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 | 84 +- .../src/datetime-picker/__test__/utils.js | 39 +- .../elements/src/datetime-picker/index.ts | 930 +++++++----------- .../elements/src/datetime-picker/types.ts | 3 - .../elements/src/datetime-picker/utils.ts | 134 +-- .../custom-elements/ef-datetime-picker.less | 10 +- packages/phrasebook/package.json | 7 +- .../src/locale/de/datetime-picker.ts | 17 + .../src/locale/en/datetime-picker.ts | 17 + .../src/locale/ja/datetime-picker.ts | 17 + .../src/locale/zh-hant/datetime-picker.ts | 17 + .../src/locale/zh/datetime-picker.ts | 17 + packages/utils/src/date/Locale.ts | 32 + 24 files changed, 1515 insertions(+), 1561 deletions(-) create mode 100644 packages/elements/src/datetime-field/resolvedLocale.ts rename packages/elements/src/datetime-picker/__snapshots__/{DOMStructure.md => DatetimePicker.md} (52%) delete mode 100644 packages/elements/src/datetime-picker/__test__/datetime-picker.snapshot.test.js 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/documents/src/pages/elements/datetime-picker.md b/documents/src/pages/elements/datetime-picker.md index ce79867dcf..0af98c48c6 100644 --- a/documents/src/pages/elements/datetime-picker.md +++ b/documents/src/pages/elements/datetime-picker.md @@ -16,153 +16,95 @@ section { height: 315px; padding: 0 3px; } -[range][timepicker] { - width: 400px; -} ``` ```html

- - +
``` :: -`ef-datetime-picker` allows the user to select a date or date range, and optionally a time, and display the selection in a specific format. +Datetime Picker allows the user to enter a date and time either through text input, or by choosing from the calendar. ## Usage -By default, Datetime Picker only allows the user to select the date. Use the `timepicker` attribute to allow the user to select both a date and time. - -An initial value for the Datetime Picker can be set using the `value` attribute/property. +Datetime Picker allows the user to enter a date. Use the `timepicker` attribute to enter both date and time. -:: -```javascript -::datetime-picker:: - document.querySelector('[timepicker]').value = '2019-03-20'; -``` -```css -section { - height: 315px; - padding: 0 3px; -} -``` -```html -
- - -
-``` -:: +The value of the Datetime Picker is set and got using `value`. ```html - + + ``` -```javascript -datetimePicker.value = '2019-03-20'; -``` +*> The value must confirm [ISO8601](https://developer.mozilla.org/en-US/docs/Web/HTML/Date_and_time_formats) for date and datetime strings. -## Setting the date - -The displayed date is based on `format` and user language. However, `value` must be in `yyyy-MM-dd` format (e.g. "2019-03-20"). -If `timepicker` is on, `value` must be in `yyyy-MM-ddTHH:mm` or `yyyy-MM-ddTHH:mm:ss` format (e.g. "2019-03-20T23:40" or "2019-03-20T23:40:59"). - -x>Wrong -x>```html -x> -x> -x> -x> -x> -x> -x> -x> -x> -x> -x> -x> -x> -x> -x>``` - -o>Correct -o>```html -o> -o> -o> -o> -o> -o>``` - -x>Wrong -x>```javascript -x>// Date object is an invalid input -x>datetimePicker.value = new Date(2019, 02, 20); -x>// `toLocaleString()` is based on current locale and might not give correct results in different regions -x>datetimePicker.value = new Date(2019, 02, 20).toLocaleString(); -x>// `toISOString()` contains time-zone information and cannot be used -x>datetimePicker.value = new Date(2019, 02, 20).toISOString(); -x>``` - -o>Correct -o>```javascript -o>datetimePicker.value = '2019-03-20'; /* if `timepicker` is off */ -o>datetimePicker.value = '2019-03-20T09:00'; /* if `timepicker` is on */ -o>``` - -## Range select - -Use `range` to switch the Datetime Picker to date range selection mode. By default, `range` provides a single calendar that allows users to choose start and end dates. - -Datetime Picker provides an optional attribute, `duplex` and `duplex="split"`, where it will popup with 2 calendars. - -* Use `duplex` to show two calendars that are automatically shifted together when users navigate to the next month. -* Use `duplex="split"` to show two calendars such that each can be navigated independently. - -An initial range value for Datetime Picker can be set using `values`. +## Range integration -:: -```javascript -::datetime-picker:: -document.querySelector('[timepicker]').values = ['2019-01-01T12:01', '2019-01-07T14:54']; +Use `range` attribute to switch Datetime Picker into range select mode. + +Datetime Picker supports two range modes: single calendar (default); and dual calendar. The latter is set using `duplex`. + +Range value is set and got using `values`. + +```html + + ``` + +:: ```css section { height: 315px; padding: 0 3px; } -[range][timepicker] { +ef-datetime-picker { width: 400px; } ``` ```html
- - + +
``` :: -```html - - -``` - -```javascript -datetimePicker.values = ['2019-01-01T12:01', '2019-01-07T14:54']; -``` +## Date format -## Custom formats +Datetime Picker format is based on [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat). -Custom date and time formats can be set using `format` attribute/property. Use `show-seconds` to allow the user to select second. Use `am-pm` to switch time picker into AM/PM time format. +Custom date and time formats are set using `formatOptions` property. The values correspond to `Intl.DateTimeFormat` `options` property. -@> Format is based on [date-fns](https://date-fns.org/docs/format). +```javascript +dateTimePicker.formatOptions = { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + hour12: true, + dayPeriod: 'long' +} +``` :: ```javascript ::datetime-picker:: + +const dateTimePicker = document.getElementById('formatOptions'); +dateTimePicker.formatOptions = { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + hour12: true, + dayPeriod: 'long' +} ``` ```css section { @@ -170,131 +112,70 @@ section { padding: 0 3px; } ef-datetime-picker { - width: 180px; + width: 400px; } ``` ```html
- - - - +
``` :: +## Setting locale + +Datetime Picker uses system default locale. You can override the locale by setting [lang](https://www.w3.org/International/questions/qa-html-language-declarations) attribute on control or globally on *document*. + ```html - - - - + ``` -## Defining min and max values +## Defining min and max You can restrict the available date range by passing in `min` and `max` values. -*> If `timepicker` is on, `min` and `max` must contain time information. - -:: -```javascript -::datetime-picker:: -``` -```css -section { - height: 315px; - padding: 0 3px; -} -``` -```html -
- - -
-``` -:: +*> `min` and `max` values must confirm picker formatting. ```html - + ``` -## Setting locale -Datetime Picker uses system default locale (or US-English if locale is not present). You can change the locale by setting [lang](https://www.w3.org/International/questions/qa-html-language-declarations) attribute either globally on *document* tag or locally. +## Set content to slots -The first day of the week is defined by the locale. You can override it by setting `first-day-of-week`. +Use slots to add additional content into the Datetime Picker. :: ```javascript ::datetime-picker:: -``` -```css -section { - height: 290px; - padding: 0 3px; -} -``` -```html -
- - -
-``` -:: - -```html - - -``` +import { format, parse, addUnit, DateTimeFormat } from 'https://cdn.skypack.dev/@refinitiv-ui/utils/date.js?min'; -## Set content to slots -Use slots to add additional content into the Datetime Picker. +const toValue = (date) => format(date, DateTimeFormat.yyyMMddTHHmmss); +const today = () => toValue(new Date()); -:: -```javascript -::datetime-picker:: -const pad = (number, size) => { - let s = String(Math.abs(number)); - while (s.length < size) { - s = '0' + s; - } - return (number < 0 ? '-' : '') + s; -}; - -const formatToDateTime = (value) => { - value = new Date(value); - const year = pad(value.getFullYear(), 4); - const month = pad(value.getMonth() + 1, 2); - const date = pad(value.getDate(), 2); - const hours = pad(value.getHours(), 2); - const minutes = pad(value.getMinutes(), 2); - const seconds = pad(value.getSeconds(), 2); - return year + '-' + month + '-' + date + 'T' + hours + ':' + minutes + ':' + seconds; -}; - -const toValues = (from, to) => [formatToDateTime(from), formatToDateTime(to)]; const rangePicker = document.querySelector('ef-datetime-picker'); + document.getElementById('today').addEventListener('tap', () => { - const to = new Date().setSeconds(0, 0); - const from = new Date(to).setHours(0, 0, 0, 0); - rangePicker.values = toValues(from, to); + const to = today(); + const startOfToday = parse(to); + startOfToday.setHours(0, 0); + const from = toValue(startOfToday); + rangePicker.values = [from, to]; }); document.getElementById('1-week').addEventListener('tap', () => { - const to = new Date().setSeconds(0, 0); - const from = new Date(to) - 7 * 24 * 60 * 60 * 1000; - rangePicker.values = toValues(from, to); + const to = today(); + const from = addUnit(to, 'day', -7); + rangePicker.values = [from, to]; }); document.getElementById('1-month').addEventListener('tap', () => { - const now = new Date(); - const to = now.setSeconds(0, 0); - const from = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate(), now.getHours(), now.getMinutes()); - rangePicker.values = toValues(from, to); + const to = today(); + const from = addUnit(to, 'month', -1); + rangePicker.values = [from, to]; }); document.getElementById('3-months').addEventListener('tap', () => { - const now = new Date(); - const to = now.setSeconds(0, 0); - const from = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate(), now.getHours(), now.getMinutes()); - rangePicker.values = toValues(from, to); + const to = today(); + const from = addUnit(to, 'month', -3); + rangePicker.values = [from, to]; }); ``` ```css @@ -302,9 +183,6 @@ section { height: 315px; padding: 0 3px; } -[range][timepicker] { - width: 400px; -} .range-nav-bar { display: flex; flex-direction: column; @@ -317,10 +195,13 @@ section { min-width: 50px; font-size: 75%; } +ef-datetime-picker { + width: 400px; +} ``` ```html
- +
Today 1 Week @@ -332,23 +213,19 @@ section { ``` :: -```html - -
- Today - 1 Week - 1 Month - 3 Months -
-
-``` +## Additional parameters + +Datetime picker allows [filtering dates](./calendar#filtering-dates) and setting [views](./calendar#defining-the-view). ## Accessibility + ::a11y-intro:: -`ef-datetime-picker` provides input fields for users to enter date string values or date with time values. Users can open the popup with calendar element and use keyboard navigation to select the date from the UI. +Datetime Picker provides input field for users to enter date/datetime string values either by typing the value or by using navigation keys. -`ef-datetime-picker` has implemented keyboard navigation for users to navigate on the UI. You must ensure that the element has associated label by using `aria-label` or `aria-labelledby`. +Users can open the calendar popup and use keyboard navigation to select the date. + +The developer must ensure that the element has associated `aria-label` or `aria-labelledby`. ```html ``` ```html - - + + ``` 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/elemental-theme/src/custom-elements/ef-datetime-picker.less b/packages/elemental-theme/src/custom-elements/ef-datetime-picker.less index 97ee89ab88..96c6ea0b99 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,23 @@ } // #endregion + padding: 0; + + [part~=button] { + height: 100%; + width: @button-height; + display: flex; + justify-content: center; + align-items: center; + flex: none; + padding: 0; + margin: 0; + border: 0; + background: none; + color: inherit; + font-size: inherit; + } + [part=calendar] { width: @calendar-width; padding: 0 calc((@input-width - @calendar-width) / 2); @@ -57,10 +74,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 1aa347b891..22b04f2099 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; /** - * Format, which is based on locale + * Used for translations */ - private _locale: Locale | null = null; - protected 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 /** @@ -265,37 +209,19 @@ export class DatetimeField extends TextField { protected partLabel = ''; /** - * Transform Date object to date string - * @param value Date - * @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 + * Get resolved locale for current element */ - protected get hasSeconds (): boolean { - return this.showSeconds || this.hasAttribute('show-seconds'); + protected get resolvedLocale (): Locale { + return resolvedLocale(this); } /** - * Returns true if the datetime field has am-pm - * @returns hasAmPm + * Transform Date object to date string + * @param value Date + * @returns dateSting */ - protected get hasAmPm (): boolean { - return this.amPm || this.hasAttribute('am-pm'); + protected dateToString (value: Date): string { + return isNaN(value.getTime()) ? '' : utcFormat(value, this.resolvedLocale.isoFormat); } /** @@ -320,10 +246,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 +265,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 +315,7 @@ export class DatetimeField extends TextField { return true; } // value format depends on locale. - return getFormat(value) === this.locale.isoFormat; + return getFormat(value) === this.resolvedLocale.isoFormat; } /** @@ -401,33 +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 '${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 '${this.resolvedLocale.isoFormat}'.`).show(); } /** * Get Intl.DateTimeFormat object from locale */ protected get formatter (): Intl.DateTimeFormat { - return this.locale.formatter; + return this.resolvedLocale.formatter; } /** @@ -458,7 +362,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 ? 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 new file mode 100644 index 0000000000..e063bc8aa7 --- /dev/null +++ b/packages/elements/src/datetime-field/resolvedLocale.ts @@ -0,0 +1,179 @@ +import { Locale } from '@refinitiv-ui/utils/date.js'; +import { getLocale as getLang } from '@refinitiv-ui/translate'; + +type LocaleMapOptions = { + resolvedLocale: Locale, + 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; +}; + +/** + * 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 locale properties + * @param lang Resolved language (locale) + * @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 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({ + 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 + ...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 + * @returns locale Resolved Locale object or null + */ +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 + // 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, 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); + + return setLocaleMap(element, { + resolvedLocale: localeFromProperties(lang, timepicker, amPm, showSeconds, options), + lang, + amPm, + timepicker, + showSeconds + }); +}; + +/** + * 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, options: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'short', + day: 'numeric' +}): Locale => getLocale(element) || setLocale(element, options); + +export { + LocaleDateElement, + resolvedLocale +}; diff --git a/packages/elements/src/datetime-picker/__demo__/index.html b/packages/elements/src/datetime-picker/__demo__/index.html index 67a46f21fe..a033729597 100644 --- a/packages/elements/src/datetime-picker/__demo__/index.html +++ b/packages/elements/src/datetime-picker/__demo__/index.html @@ -32,7 +32,7 @@

- +

@@ -40,7 +40,8 @@

- + +

@@ -49,15 +50,11 @@

Range + Duplex

Reset View

-

- Single - Duplex - Duplex Split -

Timepicker AM-PM mode @@ -82,6 +79,16 @@

+

+ + Default + Full + Short + Time Only + Month and Day + Year and Month + +

Weekdays Only Weekends Only @@ -91,7 +98,6 @@ Error

- Input Trigger Disabled Input Disabled Popup Disabled

@@ -126,7 +132,6 @@ yearsDesc: dateTimePicker.yearsDesc, weekdaysOnly: dateTimePicker.weekdaysOnly, weekendsOnly: dateTimePicker.weekendsOnly, - inputTriggerDisabled: dateTimePicker.inputTriggerDisabled, inputDisabled: dateTimePicker.inputDisabled, popupDisabled: dateTimePicker.popupDisabled }; @@ -140,6 +145,12 @@ setConsole(); + const eventLog = []; + const clearEventLog = () => { + eventLog.length = 0; + document.getElementById('events').value = eventLog.join('\n'); + }; + const resetValue = () => { dateValue.value = ''; dateFrom.value = ''; @@ -148,6 +159,7 @@ dateValue.view = ''; dateFrom.view = ''; dateTo.view = ''; + clearEventLog(); }; const setRange = (value) => { @@ -172,13 +184,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) => { @@ -218,19 +226,100 @@ 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 } }) => { - // 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; + case 'month-day': + formatOptions = { + month: 'long', + day: 'numeric' + }; + break; + case 'year-month': + formatOptions = { + year: 'numeric', + month: 'long' + }; + break; + // no default + } + resetValue(); + resetFormatAttributes(); + + dateValue.formatOptions = formatOptions; + dateFrom.formatOptions = formatOptions; + dateTo.formatOptions = formatOptions; + dateTimePicker.formatOptions = formatOptions; + }); document.getElementById('placeholder').addEventListener('value-changed', ({ detail: { value } }) => { resetValue(); dateTimePicker.placeholder = value ? value : ''; @@ -278,9 +367,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; }); @@ -300,7 +386,6 @@ dateTimePicker.popupDisabled = value || undefined; }); - const eventLog = []; const onEvent = (event) => { eventLog.unshift(`${event.type}: ${JSON.stringify(event.detail)}`); if (eventLog.length > 50) { @@ -312,6 +397,7 @@ dateTimePicker.addEventListener('opened-changed', onEvent); dateTimePicker.addEventListener('value-changed', onEvent); dateTimePicker.addEventListener('view-changed', onEvent); + document.getElementById('clear-events').addEventListener('click', clearEventLog); @@ -346,7 +432,7 @@ } - +
Today 1 Week @@ -355,33 +441,34 @@
diff --git a/packages/elements/src/datetime-picker/__snapshots__/DOMStructure.md b/packages/elements/src/datetime-picker/__snapshots__/DatetimePicker.md similarity index 52% rename from packages/elements/src/datetime-picker/__snapshots__/DOMStructure.md rename to packages/elements/src/datetime-picker/__snapshots__/DatetimePicker.md index b324fb2356..d85d1092b9 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,34 @@ ```html
- - +
- - + + +
@@ -62,7 +73,6 @@
- - +
- - +
- - + + +
@@ -133,7 +150,6 @@
- - +
- - + + +
@@ -196,7 +218,6 @@
- - +
- - + + +
@@ -266,7 +292,6 @@
- - +
- - + + +
@@ -337,7 +367,6 @@
- - +
- - +
- - + + +
@@ -417,7 +452,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..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 @@ -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.duplex).to.be.equal(false); 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..8da72b56f9 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, inputElement, inputToElement, calendarElement, calendarToElement } from './utils'; // import element and theme import '@refinitiv-ui/elements/datetime-picker'; @@ -16,12 +16,7 @@ describe('datetime-picker/View', () => { it('Check default view duplex', async () => { const el = await fixture(''); 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'); + expect(el.views[1]).to.be.equal(formatToView(now), 'Default view duplex to should be set to this month'); }); it('Check view when value set', async () => { const el = await fixture(''); @@ -30,52 +25,34 @@ 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 () => { 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'); - 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'); + expect(el.views[1]).to.be.equal('2020-04', 'Duplex: 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'); + 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(''); @@ -84,52 +61,33 @@ 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); - 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(''); + const el = await fixture(''); el.view = ''; await elementUpdated(el); expect(el.views[0]).to.be.equal(formatToView(now), 'View from is not reset'); - expect(el.views[1]).to.be.equal(formatToView(addMonths(now, 1)), 'View to is not reset'); + expect(el.views[1]).to.be.equal(formatToView(now), 'View to is not reset'); }); 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'); 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 = el.calendarEl; - const calendarTo = el.calendarToEl; - 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 = 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 f0bcf8253a..72ebc04d2c 100644 --- a/packages/elements/src/datetime-picker/index.ts +++ b/packages/elements/src/datetime-picker/index.ts @@ -6,18 +6,17 @@ import { MultiValue, PropertyValues, 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'; -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 { - DatetimePickerDuplex, DatetimePickerFilter } from './types'; import '../calendar/index.js'; @@ -25,44 +24,38 @@ 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, - TranslateDirective, - getLocale, - TranslatePropertyKey + TranslateDirective } from '@refinitiv-ui/translate'; import { - addMonths, - subMonths, isAfter, isBefore, - isValidDate, - getFormat, - DateFormat, - parse, format, - Locale + toSegment, + Locale, + DateFormat, + getFormat } from '@refinitiv-ui/utils/date.js'; import { - DateTimeSegment, + getCurrentSegment, formatToView, - getCurrentTime + formatToDate, + formatToTime } 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'; +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 */ export type { - DatetimePickerFilter, - DatetimePickerDuplex + DatetimePickerFilter }; const POPUP_POSITION = ['bottom-start', 'top-start', 'bottom-end', 'top-end', 'bottom-middle', 'top-middle']; @@ -132,76 +125,73 @@ 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; } `; } - 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 */ + // speed up rendering by not populating popup window on first load + private lazyRendered = false; - 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 = ''; - } + @property({ type: String, reflect: true }) + public min: string | null = null; - 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; - } + /** + * 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, reflect: true }) + public max: string | null = null; - private _max = ''; - private maxDate = ''; /** - * Set maximum date - * @param max date - * @default - + * Toggle to display the time picker */ - @property({ type: String }) - public set max (max: string) { - if (!this.isValidValue(max)) { - this.warnInvalidValue(max); - max = ''; - } + @property({ type: Boolean, reflect: true }) + public timepicker = false; - 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; - } + /** + * 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](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; + + /** + * Set the Locale object. + * `Locale` overrides `formatOptions`, `timepicker` and `showSeconds` properties. + * @type {Locale | null} + */ + @property({ attribute: false }) + public locale: Locale | null = null; /** * Only enable weekdays @@ -225,10 +215,9 @@ 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; + public firstDayOfWeek: number | null = null; /** * Set to switch to range select mode @@ -255,18 +244,18 @@ export class DatetimePicker extends ControlElement implements MultiValue { /** * Current date time value * @param value Calendar value + * @type {string} * @default - */ @property({ type: String }) public set value (value: string) { - this.values = value ? [value] : []; + this.values = value === '' ? [] : [value]; } public get value (): string { return this.values[0] || ''; } 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 @@ -282,46 +271,18 @@ 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.valuesToSegments(); - this.requestUpdate('_values', oldValues); /* segments are populated in update */ - } + this._values = this.filterAndWarnInvalidValues(values); + this.requestUpdate('values', oldValues); } public get values (): string[] { - return this._segments.map(segment => segment.value); + 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; - - 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 @@ -341,13 +302,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 */ @@ -360,57 +314,11 @@ 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 - * `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; - } - - /** - * Toggle to display the time picker - */ - @property({ type: Boolean, reflect: true }) - public timepicker = false; - /** * 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. @@ -437,8 +345,8 @@ 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()) { + views = this.filterInvalidViews(views); + if (String(oldViews) !== String(views)) { this._views = views; this.requestUpdate('views', oldViews); } @@ -448,60 +356,41 @@ 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)]; + if (!this.duplex) { + 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 (!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.inputRef.value ? this.inputRef.value.checkValidity() : true) + && (this.inputToRef.value ? this.inputToRef.value.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; } /** @@ -509,118 +398,138 @@ 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(); /** - * Updates the element - * @param changedProperties Properties that has changed - * @returns {void} + * Get resolved locale for current element */ - 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 */ - } + protected get resolvedLocale (): Locale { + return resolvedLocale(this); + } - if (changedProperties.has('_values') || changedProperties.has(TranslatePropertyKey)) { - this.syncInputValues(); - } + /** + * Returns true if Locale has time picker + */ + protected get hasTimePicker (): boolean { + return this.resolvedLocale.hasTimePicker; + } - // re-validation - if (changedProperties.has('_values') && changedProperties.get('_values') !== undefined) { - this.validateInput(); - } + /** + * Returns true if Locale has seconds + */ + protected get hasSeconds (): boolean { + return this.resolvedLocale.hasSeconds; + } - super.update(changedProperties); + /** + * Returns true if Locale has date picker + */ + protected get hasDatePicker (): boolean { + return this.resolvedLocale.hasDatePicker; } /** - * Called after the component is first rendered - * @param changedProperties Properties which have changed - * @returns {void} + * Returns true if Locale has 12h time format */ - protected firstUpdated (changedProperties: PropertyValues): void { - super.firstUpdated(changedProperties); - this.addEventListener('keydown', this.onKeyDown); - this.addEventListener('tap', this.onTap); + protected get hasAmPm (): boolean { + return this.resolvedLocale.hasAmPm; } /** - * Overwrite validation method for value - * - * @param value value - * @returns {boolean} result + * Called after render life-cycle finished + * @param changedProperties Properties which have changed + * @returns {void} */ - protected isValidValue (value: string): boolean { - if (value === '') { - return true; + 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(); } - // value format depends on locale. - return getFormat(value) === this.locale.isoFormat; } /** - * Returns true if the datetime field has timepicker - * @returns hasTimePicker + * Force synchronise input values with picker values + * @returns {void} */ - 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 syncInputValues (): void { + this.inputRef.value && (this.inputRef.value.value = this.values[0] || ''); + this.inputToRef.value && (this.inputToRef.value.value = this.values[1] || ''); } /** - * Returns true if the datetime field has seconds - * @returns hasSeconds + * Updates the element + * @param changedProperties Properties that has changed + * @returns {void} */ - protected get hasSeconds (): boolean { - return this.showSeconds || this.hasAttribute('show-seconds'); + protected 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; + } + + if (this.shouldValidateInput(changedProperties)) { + this.validateInput(); + } } - + /** - * Returns true if the datetime field has am-pm - * @returns hasAmPm + * Check if input should be re-validated + * @param changedProperties Properties that has changed + * @returns True if input should be re-validated */ - protected get hasAmPm (): boolean { - return this.amPm || this.hasAttribute('am-pm'); + protected shouldValidateInput (changedProperties: PropertyValues): boolean { + // TODO: this needs refactoring with all other fields to support common validation patterns + return (changedProperties.has(FocusedPropertyKey) && !this.focused); } /** - * Used to show a warning when the value does not pass the validation - * @param value that is invalid + * Validate input according `pattern`, `minLength` and `maxLength` properties + * change state of `error` property according pattern validation * @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(); + protected validateInput (): void { + this.reportValidity(); } /** - * Show invalid view message - * @param value Invalid value + * Reset error state on input * @returns {void} */ - 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 resetError (): void { + if (this.error && this.checkValidity()) { + this.reportValidity(); + } } /** - * Convert value string array to date segments - * Warn invalid value if passed value does not confirm a segment - * @returns {void} + * Check if `from` is before or the same as `to` + * @returns true if `from` is before or the same as `to` */ - private valuesToSegments (): void { - const newSegments = this.filterAndWarnInvalidValues(this._values).map(value => DateTimeSegment.fromString(value)); - this._segments = newSegments; - this.interimSegments = newSegments; + 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 true; } /** @@ -634,7 +543,6 @@ export class DatetimePicker extends ControlElement implements MultiValue { if (this.isValidValue(value)) { return value; } - this.warnInvalidValue(value); return ''; }); @@ -642,71 +550,39 @@ export class DatetimePicker extends ControlElement implements MultiValue { /** * 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 + * @returns Filtered collection of views */ - 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 */ - } + private filterInvalidViews (views: string[]): string[] { + // views must match in duplex mode + if (views.length !== (this.duplex ? 2 : 1)) { + return []; } - return views; - } - /** - * 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'; - } + // cannot have empty or invalid views + if (views.findIndex(view => typeof view !== 'string' || view === '' || getFormat(view) !== DateFormat.yyyyMM) !== -1) { + return []; + } - /** - * Stop syncing input values and picker values - * @returns {void} - */ - private disableInputSync (): void { - this.inputSyncing = false; + return views; } /** - * Start syncing input values and picker values + * Show invalid value message + * @param value Invalid value * @returns {void} */ - private enableInputSync (): void { - this.inputSyncing = true; + protected override warnInvalidValue (value: string): void { + new WarningNotice(`The specified value "${value}" does not conform to the required format. The format is ${this.resolvedLocale.isoFormat}.`).once(); } /** - * Synchronise input values and values - * @return {void} + * Check if passed value is valid + * @param value Value + * @returns valid Validity */ - 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); + protected isValidValue (value: string): boolean { + return value === '' ? true : typeof value === 'string' && getFormat(value) === this.resolvedLocale.isoFormat; } /** @@ -719,95 +595,37 @@ 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, formatToView(addMonths(view, 1))]; - } - else { /* to */ - return [formatToView(subMonths(view, 1)), view]; - } - } - - // duplex split if (index === 0) { /* from. to must be after or the same */ - let after = views[1] || addMonths(view, 1); + let after = views[1] || view; if (isBefore(after, view)) { after = view; } - return [view, formatToView(after)]; + return [view, after]; } if (index === 1) { /* to. from must be before or the same */ - let before = views[0] || subMonths(view, 1); + let before = views[0] || view; if (isAfter(before, view)) { 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); @@ -820,8 +638,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); } } @@ -839,85 +660,11 @@ 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 - * @returns {void} - */ - private onCalendarKeyDown (event: KeyboardEvent): void { - switch (event.key) { - case 'Esc': - case 'Escape': - this.resetViews(); - this.setOpened(false); - break; - default: - return; - } - - event.preventDefault(); - } - - /** - * Handles key input on text field - * @param event Key down event object + * Run on icon tap event * @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 - * @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 onButtonTap (): void { + this.setOpened(true); } /** @@ -936,7 +683,8 @@ 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 */ + // 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)); } @@ -947,30 +695,24 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @returns {void} */ 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; - - if (this.timepicker && !segment.timeSegment) { - segment.timeSegment = getCurrentTime(this.showSeconds); /* populate time, as otherwise time picker looks broken */ - } + const target = event.target as Calendar; + let values; - return segment; - }); - - this.submitInterimSegments(); - - // 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.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); // 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); } } @@ -982,37 +724,38 @@ 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(); + // 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); } /** - * Run on input error-changed event - * @param event error-changed event + * Make sure that calendar and time-picker values + * are merged together + * @param values New values * @returns {void} */ - private onInputErrorChanged (event: ErrorChangedEvent): void { - const hasError = event.detail.value; - this.setErrorAndNotify(hasError); - } + 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.resolvedLocale.isoFormat) : ''); - /** - * Run on input focus - * @returns {void} - */ - private onInputFocus (): void { - this.disableInputSync(); + this.notifyValuesChange(newValues); + + await this.updateComplete; + this.resetError(); } /** - * Run on input blur + * Run on input error-changed event + * @param event error-changed event * @returns {void} */ - private onInputBlur (): void { - this.enableInputSync(); + private onInputErrorChanged (event: ErrorChangedEvent): void { + const hasError = event.detail.value; + this.notifyErrorChange(hasError); } /** @@ -1021,40 +764,14 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @returns {void} */ private onInputValueChanged (event: ValueChangedEvent): void { - const target = event.target as TextField; - 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(); - } + const target = event.target as DatetimeField; + // 0 - from, single; 1 - to + const index = target === this.inputToRef.value ? 1 : 0; + const newValues = [...this.values]; + newValues[index] = target.value; - /** - * 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]; - - if (from && to) { - if (parse(from).getTime() > parse(to).getTime()) { - return false; - } - } - } - - return true; - } - - /** - * Toggles the opened state of the list - * @returns {void} - */ - private toggleOpened (): void { - this.setOpened(!this.opened); + this.notifyValuesChange(newValues); + this.resetError(); } /** @@ -1075,6 +792,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; } } @@ -1089,43 +811,43 @@ 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 { + const values = this.range && this.duplex + ? [formatToDate(isTo ? this.values[1] : this.values[0])] + : this.values.map(value => formatToDate(value)); + return html``; } @@ -1135,8 +857,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.duplex ? this.getCalendarTemplate(true) : undefined} `; } @@ -1145,50 +867,50 @@ 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.range ? html`
` : undefined} - ${this.range ? this.getTimepickerTemplate('timepicker-to', 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` `; } /** - * Template for rendering an icon + * Template for rendering a button */ - private get iconTemplate (): TemplateResult { + private get buttonTemplate (): TemplateResult { return html` - + `; } @@ -1197,13 +919,12 @@ 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.range ? html`
` : undefined} - ${this.range ? this.getInputTemplate('input-to', values[1]) : undefined} + ${this.getInputTemplate()} + ${this.range + ? html`
${this.getInputTemplate(true)}` + : undefined}
`; } @@ -1213,14 +934,23 @@ 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.calendarsTemplate} -
- ${this.timepicker ? html`
${this.timepickersTemplate}
` : undefined} + ${hasDate ? html`
${this.calendarsTemplate}
` : undefined} + ${hasTime ? html`
${this.timepickersTemplate}
` : undefined}
@@ -1249,7 +977,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/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 }; diff --git a/packages/elements/src/datetime-picker/utils.ts b/packages/elements/src/datetime-picker/utils.ts index 8aec73792b..f1db62db2a 100644 --- a/packages/elements/src/datetime-picker/utils.ts +++ b/packages/elements/src/datetime-picker/utils.ts @@ -1,123 +1,53 @@ 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 at midday Local time + * @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) : ''; export { - DateTimeSegment, - getCurrentTime, + getCurrentSegment, + formatToDate, + formatToTime, formatToView }; 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 b50b7ce5ec..768b6bd634 100644 --- a/packages/halo-theme/src/custom-elements/ef-datetime-picker.less +++ b/packages/halo-theme/src/custom-elements/ef-datetime-picker.less @@ -21,7 +21,7 @@ padding: 0 3px 4px 3px; } - [part=icon] { + [part=button] { color: inherit; & when (@variant = light) { color: @control-border-color; @@ -54,16 +54,16 @@ border-color: fade(@control-hover-error-color, 50%); } - &[disabled] { - [part=icon] { + &[disabled], &[popup-disabled] { + [part=button] { color: @input-disabled-text-color } } &[focused], &[focused][error][warning], - &:not([disabled]):not([error]):not([warning]):hover { - [part=icon] { + &:not([disabled]):not([popup-disabled]):not([error]):not([warning]):hover { + [part=button] { color: @scheme-color-secondary; & when (@variant = light) { 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..382772782d --- /dev/null +++ b/packages/phrasebook/src/locale/de/datetime-picker.ts @@ -0,0 +1,17 @@ +import { Phrasebook } from '../../translation.js'; + +const translations = { + CHOOSE_DATE: 'Choose date', + CHOOSE_DATE_TIME: 'Choose date and time', + CHOOSE_TIME: 'Choose time', + CHOOSE_DATE_RANGE: 'Choose date range', + CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', + CHOOSE_TIME_RANGE: 'Choose time range', + VALUE_FROM: 'From', + VALUE_TO: 'To', + OPEN_CALENDAR: 'Open calendar' +}; + +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..74bd216ff4 --- /dev/null +++ b/packages/phrasebook/src/locale/en/datetime-picker.ts @@ -0,0 +1,17 @@ +import { Phrasebook } from '../../translation.js'; + +const translations = { + CHOOSE_DATE: 'Choose date', + CHOOSE_DATE_TIME: 'Choose date and time', + CHOOSE_TIME: 'Choose time', + CHOOSE_DATE_RANGE: 'Choose date range', + CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', + CHOOSE_TIME_RANGE: 'Choose time range', + VALUE_FROM: 'From', + VALUE_TO: 'To', + OPEN_CALENDAR: 'Open calendar' +}; + +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..c63ce065bd --- /dev/null +++ b/packages/phrasebook/src/locale/ja/datetime-picker.ts @@ -0,0 +1,17 @@ +import { Phrasebook } from '../../translation.js'; + +const translations = { + CHOOSE_DATE: 'Choose date', + CHOOSE_DATE_TIME: 'Choose date and time', + CHOOSE_TIME: 'Choose time', + CHOOSE_DATE_RANGE: 'Choose date range', + CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', + CHOOSE_TIME_RANGE: 'Choose time range', + VALUE_FROM: 'From', + VALUE_TO: 'To', + OPEN_CALENDAR: 'Open calendar' +}; + +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..442fb4418c --- /dev/null +++ b/packages/phrasebook/src/locale/zh-hant/datetime-picker.ts @@ -0,0 +1,17 @@ +import { Phrasebook } from '../../translation.js'; + +const translations = { + CHOOSE_DATE: 'Choose date', + CHOOSE_DATE_TIME: 'Choose date and time', + CHOOSE_TIME: 'Choose time', + CHOOSE_DATE_RANGE: 'Choose date range', + CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', + CHOOSE_TIME_RANGE: 'Choose time range', + VALUE_FROM: 'From', + VALUE_TO: 'To', + OPEN_CALENDAR: 'Open calendar' +}; + +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..c39129cdc5 --- /dev/null +++ b/packages/phrasebook/src/locale/zh/datetime-picker.ts @@ -0,0 +1,17 @@ +import { Phrasebook } from '../../translation.js'; + +const translations = { + CHOOSE_DATE: 'Choose date', + CHOOSE_DATE_TIME: 'Choose date and time', + CHOOSE_TIME: 'Choose time', + CHOOSE_DATE_RANGE: 'Choose date range', + CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', + CHOOSE_TIME_RANGE: 'Choose time range', + VALUE_FROM: 'From', + VALUE_TO: 'To', + OPEN_CALENDAR: 'Open calendar' +}; + +Phrasebook.define('zh', 'ef-datetime-picker', translations); + +export default translations; 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 a0bae4e00650c36e23347b1ecaf640fb644ab4d9 Mon Sep 17 00:00:00 2001 From: AG <81616437+goremikins@users.noreply.github.com> Date: Fri, 19 Aug 2022 16:03:58 +0100 Subject: [PATCH 3/6] docs(datetime-picker): missing import --- documents/src/pages/elements/datetime-picker.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/documents/src/pages/elements/datetime-picker.md b/documents/src/pages/elements/datetime-picker.md index 0af98c48c6..39d592f82b 100644 --- a/documents/src/pages/elements/datetime-picker.md +++ b/documents/src/pages/elements/datetime-picker.md @@ -54,6 +54,9 @@ Range value is set and got using `values`. ``` :: +```javascript +::datetime-picker:: +``` ```css section { height: 315px; From c136cd3bd93d8692d60430872af41818c69ca7cc Mon Sep 17 00:00:00 2001 From: Domrongpon Tanpaibul Date: Thu, 8 Sep 2022 10:31:16 +0700 Subject: [PATCH 4/6] fix(datetime-picker): message translated for zh and zh-hant --- .../src/locale/zh-hant/datetime-picker.ts | 18 +++++++++--------- .../src/locale/zh/datetime-picker.ts | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/phrasebook/src/locale/zh-hant/datetime-picker.ts b/packages/phrasebook/src/locale/zh-hant/datetime-picker.ts index 442fb4418c..5ffdb89c60 100644 --- a/packages/phrasebook/src/locale/zh-hant/datetime-picker.ts +++ b/packages/phrasebook/src/locale/zh-hant/datetime-picker.ts @@ -1,15 +1,15 @@ import { Phrasebook } from '../../translation.js'; const translations = { - CHOOSE_DATE: 'Choose date', - CHOOSE_DATE_TIME: 'Choose date and time', - CHOOSE_TIME: 'Choose time', - CHOOSE_DATE_RANGE: 'Choose date range', - CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', - CHOOSE_TIME_RANGE: 'Choose time range', - VALUE_FROM: 'From', - VALUE_TO: 'To', - OPEN_CALENDAR: 'Open calendar' + CHOOSE_DATE: '選擇日期', + CHOOSE_DATE_TIME: '選擇日期與時間', + CHOOSE_TIME: '選擇時間', + CHOOSE_DATE_RANGE: '選擇日期範圍', + CHOOSE_DATE_TIME_RANGE: '選擇日期與時間範圍', + CHOOSE_TIME_RANGE: '選擇時間範圍', + VALUE_FROM: '從', + VALUE_TO: '至', + 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 c39129cdc5..4bf9859ca8 100644 --- a/packages/phrasebook/src/locale/zh/datetime-picker.ts +++ b/packages/phrasebook/src/locale/zh/datetime-picker.ts @@ -1,15 +1,15 @@ import { Phrasebook } from '../../translation.js'; const translations = { - CHOOSE_DATE: 'Choose date', - CHOOSE_DATE_TIME: 'Choose date and time', - CHOOSE_TIME: 'Choose time', - CHOOSE_DATE_RANGE: 'Choose date range', - CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', - CHOOSE_TIME_RANGE: 'Choose time range', - VALUE_FROM: 'From', - VALUE_TO: 'To', - OPEN_CALENDAR: 'Open calendar' + CHOOSE_DATE: '选择日期', + CHOOSE_DATE_TIME: '选择日期与时间', + CHOOSE_TIME: '选择时间', + CHOOSE_DATE_RANGE: '选择日期范围', + CHOOSE_DATE_TIME_RANGE: '选择日期与时间范围', + CHOOSE_TIME_RANGE: '选择时间范围', + VALUE_FROM: '从', + VALUE_TO: '至', + OPEN_CALENDAR: '打开日历' }; Phrasebook.define('zh', 'ef-datetime-picker', translations); From f25e6e2471e0e01e87c8c336b940b11c7ec6b34c Mon Sep 17 00:00:00 2001 From: Domrongpon Tanpaibul Date: Wed, 14 Sep 2022 15:35:28 +0700 Subject: [PATCH 5/6] fix(datetime-picker): update translation for german and japanese --- .../src/locale/de/datetime-picker.ts | 18 +++++++++--------- .../src/locale/ja/datetime-picker.ts | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/phrasebook/src/locale/de/datetime-picker.ts b/packages/phrasebook/src/locale/de/datetime-picker.ts index 382772782d..d140df64e0 100644 --- a/packages/phrasebook/src/locale/de/datetime-picker.ts +++ b/packages/phrasebook/src/locale/de/datetime-picker.ts @@ -1,15 +1,15 @@ import { Phrasebook } from '../../translation.js'; const translations = { - CHOOSE_DATE: 'Choose date', - CHOOSE_DATE_TIME: 'Choose date and time', - CHOOSE_TIME: 'Choose time', - CHOOSE_DATE_RANGE: 'Choose date range', - CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', - CHOOSE_TIME_RANGE: 'Choose time range', - VALUE_FROM: 'From', - VALUE_TO: 'To', - OPEN_CALENDAR: 'Open calendar' + CHOOSE_DATE: 'Datum auswählen', + CHOOSE_DATE_TIME: 'Datum und Uhrzeit auswählen', + CHOOSE_TIME: 'Uhrzeit auswählen', + CHOOSE_DATE_RANGE: 'Zeitraum auswählen', + CHOOSE_DATE_TIME_RANGE: 'Datum und Zeitraum auswählen', + CHOOSE_TIME_RANGE: 'Zeitraum auswählen', + VALUE_FROM: ' Von', + VALUE_TO: ' Bis', + OPEN_CALENDAR: 'Kalender öffnen' }; Phrasebook.define('de', '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 c63ce065bd..b97d3fa7ff 100644 --- a/packages/phrasebook/src/locale/ja/datetime-picker.ts +++ b/packages/phrasebook/src/locale/ja/datetime-picker.ts @@ -1,15 +1,15 @@ import { Phrasebook } from '../../translation.js'; const translations = { - CHOOSE_DATE: 'Choose date', - CHOOSE_DATE_TIME: 'Choose date and time', - CHOOSE_TIME: 'Choose time', - CHOOSE_DATE_RANGE: 'Choose date range', - CHOOSE_DATE_TIME_RANGE: 'Choose date and time range', - CHOOSE_TIME_RANGE: 'Choose time range', - VALUE_FROM: 'From', - VALUE_TO: 'To', - OPEN_CALENDAR: 'Open calendar' + CHOOSE_DATE: '日付を選択', + CHOOSE_DATE_TIME: '日付と時刻を選択', + CHOOSE_TIME: '時刻を選択', + CHOOSE_DATE_RANGE: '日付範囲を選択', + CHOOSE_DATE_TIME_RANGE: '日付と時刻の範囲を選択', + CHOOSE_TIME_RANGE: '時刻の範囲を選択', + VALUE_FROM: '開始', + VALUE_TO: '終了', + OPEN_CALENDAR: 'カレンダーを開く' }; Phrasebook.define('ja', 'ef-datetime-picker', translations); From 87c9f7cd61f44bfaff9fcdbb9515645a0870c684 Mon Sep 17 00:00:00 2001 From: Sarin Udompanish Date: Wed, 14 Sep 2022 16:56:18 +0700 Subject: [PATCH 6/6] chore(elements): remove date-fns from dependencies --- packages/elements/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/elements/package.json b/packages/elements/package.json index 049b1714cd..ba5fab88ee 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -333,7 +333,6 @@ "@types/chart.js": "^2.9.31", "chart.js": "~2.9.4", "d3-interpolate": "^3.0.1", - "date-fns": "^2.22.1", "lightweight-charts": "^3.3.0", "tslib": "^2.3.1" },