+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+#### `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 c557d8c361..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,129 +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('It must not error when user input empty string value', async () => {
- const el = await fixture('
');
- el.value = '2022-05-15';
- await elementUpdated(el);
- expect(el.error).to.be.equal(true);
- typeText(el.inputEl, '');
- await elementUpdated(el);
- expect(el.error).to.be.equal(false, 'input empty string must not make element error');
-
- // Test range mode
- el.range = true;
- el.values = ['2022-03-15', '2022-04-23'];
- await elementUpdated(el);
- expect(el.error).to.be.equal(true);
- typeText(el.inputEl, '');
- await elementUpdated(el);
- expect(el.error).to.be.equal(false, 'input empty string must not make element error in range mode');
- });
-
- 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 361a22d9e9..72ebc04d2c 100644
--- a/packages/elements/src/datetime-picker/index.ts
+++ b/packages/elements/src/datetime-picker/index.ts
@@ -6,80 +6,60 @@ import {
MultiValue,
PropertyValues,
CSSResultGroup,
- TapEvent,
- WarningNotice
+ WarningNotice,
+ 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 } from '../events';
+import type { OpenedChangedEvent, ViewChangedEvent, ValueChangedEvent, ErrorChangedEvent } from '../events';
import type {
- DatetimePickerDuplex,
DatetimePickerFilter
} from './types';
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';
import {
translate,
- TranslateDirective,
- getLocale,
- TranslatePropertyKey
+ TranslateDirective
} 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,
+ format,
+ toSegment,
+ Locale,
DateFormat,
- DateTimeFormat,
- parse,
- format
+ 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'];
-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
*
@@ -145,148 +125,143 @@ 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 -
- */
- @property({ type: String })
- public set min (min: string) {
- if (!this.isValidValue(min)) {
- this.warnInvalidValue(min);
- min = '';
- }
+ * 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, 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 -
- */
- @property({ type: String })
- public set max (max: string) {
- if (!this.isValidValue(max)) {
- this.warnInvalidValue(max);
- max = '';
- }
+ * Toggle to display the time picker
+ */
+ @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;
/**
- * Only enable weekdays
- */
+ * 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
+ */
@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;
/**
* 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
- */
+ * 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
+ * @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
- * @type {string[]}
- * @default []
- */
+ * Set multiple selected values
+ * @param values Values to set
+ * @type {string[]}
+ * @default []
+ */
@property({
converter: {
fromAttribute: function (value: string): string[] {
@@ -296,50 +271,22 @@ 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
+ * Set placeholder text
*/
- @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 -
- */
@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 || this.format;
- }
+ public 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 +303,29 @@ 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.
- */
- @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 -
- */
- @property({ type: String })
- public set format (format: string) {
- const oldFormat = this._format;
- if (oldFormat !== format) {
- this._format = format;
- this.requestUpdate('format', oldFormat);
- }
- }
- 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
- );
- }
-
/**
- * Toggle to display the time picker
- */
+ * Display two calendar pickers.
+ */
@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;
+ public duplex = false;
/**
- * 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,17 +336,17 @@ 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;
- 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);
}
@@ -450,34 +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];
}
/**
- * Validates the input, marking the element as invalid if its value does not meet the validation criteria.
- * @returns {void}
+ * Returns true if an input element contains valid data.
+ * @returns true if input is valid
*/
- public validateInput (): void {
- const hasError = this.hasError();
- if (this.error !== hasError) {
- this.error = hasError;
- this.notifyPropertyChange('error', this.error);
- }
+ public checkValidity (): boolean {
+ return (this.inputRef.value ? this.inputRef.value.checkValidity() : true)
+ && (this.inputToRef.value ? this.inputToRef.value.checkValidity() : true)
+ && this.isFromBeforeTo();
+ }
+
+ /**
+ * Validate input. Mark as error if input is invalid
+ * @returns false if there is an error
+ */
+ public reportValidity (): boolean {
+ const hasError = !this.checkValidity();
+ this.notifyErrorChange(hasError);
+ return !hasError;
}
/**
@@ -485,112 +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?: TextField | null;
- @query('#input-to') private inputToEl?: TextField | 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;
+ }
- if (this.shouldValidateValue(changedProperties)) {
- 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;
+ }
+
+ /**
+ * Returns true if Locale has 12h time format
+ */
+ protected get hasAmPm (): boolean {
+ return this.resolvedLocale.hasAmPm;
}
/**
- * Called after the component is first rendered
+ * Called after render life-cycle finished
* @param changedProperties Properties which have changed
* @returns {void}
*/
- protected firstUpdated (changedProperties: PropertyValues): void {
- super.firstUpdated(changedProperties);
- this.addEventListener('keydown', this.onKeyDown);
- this.addEventListener('tap', this.onTap);
+ protected updated (changedProperties: PropertyValues): void {
+ super.updated(changedProperties);
+
+ // When the value is set externally it must override input values.
+ // Do force value update
+ if (changedProperties.has('values')) {
+ this.syncInputValues();
+ }
+ }
+
+ /**
+ * Force synchronise input values with picker values
+ * @returns {void}
+ */
+ protected syncInputValues (): void {
+ this.inputRef.value && (this.inputRef.value.value = this.values[0] || '');
+ this.inputToRef.value && (this.inputToRef.value.value = this.values[1] || '');
}
/**
- * Overwrite validation method for value
- *
- * @param value value
- * @returns {boolean} result
+ * Updates the element
+ * @param changedProperties Properties that has changed
+ * @returns {void}
*/
- protected isValidValue (value: string): boolean {
- if (value === '') {
- return true;
+ 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();
}
- // 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);
}
/**
- * 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();
+ * Check if input should be re-validated
+ * @param changedProperties Properties that has changed
+ * @returns True if input should be re-validated
+ */
+ protected shouldValidateInput (changedProperties: PropertyValues): boolean {
+ // TODO: this needs refactoring with all other fields to support common validation patterns
+ return (changedProperties.has(FocusedPropertyKey) && !this.focused);
}
/**
- * Show invalid view message
- * @param value Invalid value
+ * Validate input according `pattern`, `minLength` and `maxLength` properties
+ * change state of `error` property according pattern validation
* @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 validateInput (): void {
+ this.reportValidity();
}
/**
- * Convert value string array to date segments
- * Warn invalid value if passed value does not confirm a segment
+ * Reset error state on input
* @returns {void}
*/
- private valuesToSegments (): void {
- const newSegments = this.filterAndWarnInvalidValues(this._values).map(value => DateTimeSegment.fromString(value));
- this._segments = newSegments;
- this.interimSegments = newSegments;
+ protected resetError (): void {
+ if (this.error && this.checkValidity()) {
+ this.reportValidity();
+ }
}
/**
- * 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;
+ * Check if `from` is before or the same as `to`
+ * @returns true if `from` is before or the same as `to`
+ */
+ 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 false;
+ return true;
}
/**
@@ -604,7 +543,6 @@ export class DatetimePicker extends ControlElement implements MultiValue {
if (this.isValidValue(value)) {
return value;
}
-
this.warnInvalidValue(value);
return '';
});
@@ -612,83 +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;
- }
-
- /**
- * Synchronise input values and values
- * @return {void}
- */
- private syncInputValues (): void {
- if (!this.inputSyncing) {
- return;
- }
- // input values cannot be populated off interim segments as require a valid date
- // date-fns formats to local if there is time info
- this.inputValues = this._segments.map(segment => this.formatSegment(segment));
+ 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();
}
/**
- * Format date segment according to format and locale
- * @param segment Date segment
- * @returns formatted string
+ * Check if passed value is valid
+ * @param value Value
+ * @returns valid Validity
*/
- private formatSegment (segment: DateTimeSegment): string {
- return segment.value ? inputFormat(segment.getTime(), this.format, {
- locale: getDateFNSLocale(getLocale(this))
- }) : '';
+ protected isValidValue (value: string): boolean {
+ return value === '' ? true : typeof value === 'string' && getFormat(value) === this.resolvedLocale.isoFormat;
}
/**
@@ -701,87 +595,41 @@ 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
+ * Notify error if it has changed
+ * @param hasError true if the element has an error
+ * @returns {void}
*/
- 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;
- }
+ protected notifyErrorChange (hasError: boolean): void {
+ if (this.error !== hasError) {
+ this.error = hasError;
+ this.notifyPropertyChange('error', this.error);
}
-
- this.notifyValuesChange(newValues);
- return true;
}
/**
@@ -790,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);
}
}
@@ -809,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
- * @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
+ * Run on icon 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);
}
/**
@@ -906,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));
}
@@ -917,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);
}
}
@@ -952,44 +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 focus
+ * Make sure that calendar and time-picker values
+ * are merged together
+ * @param values New values
* @returns {void}
*/
- private onInputFocus (): void {
- this.disableInputSync();
+ 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) : '');
+
+ this.notifyValuesChange(newValues);
+
+ await this.updateComplete;
+ this.resetError();
}
/**
- * Run on input blur
- * @param event blur event
+ * Run on input error-changed event
+ * @param event error-changed event
* @returns {void}
*/
- private onInputBlur (event: FocusEvent): 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();
- }
+ private onInputErrorChanged (event: ErrorChangedEvent): void {
+ const hasError = event.detail.value;
+ this.notifyErrorChange(hasError);
}
/**
@@ -998,111 +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 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);
- 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) {
- for (let i = 0; i < this.values.length; i += 1) {
- const value = this.values[i];
- if (value) {
- // Value before min
- if (this.min && value !== this.min && isBefore(value, this.min)) {
- return false;
- }
- // Value after max
- if (this.max && value !== this.max && isAfter(value, this.max)) {
- return false;
- }
- }
- }
- }
-
- return true;
- }
-
- /**
- * 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;
- }
+ 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 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}
- */
- private toggleOpened (): void {
- this.setOpened(!this.opened);
+ this.notifyValuesChange(newValues);
+ this.resetError();
}
/**
@@ -1123,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;
}
}
@@ -1137,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` `;
}
@@ -1183,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}
`;
}
@@ -1193,45 +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`
-
- `;
+ min=${ifDefined(this.min || undefined)}
+ max=${ifDefined(this.max || undefined)}
+ ?disabled=${this.disabled}
+ ?readonly=${this.readonly || this.inputDisabled}
+ .locale=${this.resolvedLocale}
+ .value=${live(isTo ? (this.values[1] || '') : (this.values[0] || ''))}
+ .placeholder=${this.placeholder}
+ @value-changed=${this.onInputValueChanged}
+ @error-changed=${this.onInputErrorChanged}>`;
}
/**
- * Template for rendering an icon
+ * Template for rendering a button
*/
- private get iconTemplate (): TemplateResult {
+ private get buttonTemplate (): TemplateResult {
return html`
-
+
+
+
`;
}
@@ -1240,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}
`;
}
@@ -1256,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}
@@ -1292,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/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
-};
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 4ec56f5692..6633252b4c 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..d140df64e0
--- /dev/null
+++ b/packages/phrasebook/src/locale/de/datetime-picker.ts
@@ -0,0 +1,17 @@
+import { Phrasebook } from '../../translation.js';
+
+const translations = {
+ 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);
+
+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..b97d3fa7ff
--- /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_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);
+
+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..5ffdb89c60
--- /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_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);
+
+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..4bf9859ca8
--- /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_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);
+
+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