diff --git a/documents/src/pages/elements/datetime-picker.md b/documents/src/pages/elements/datetime-picker.md index ce79867dcf..b761578dd4 100644 --- a/documents/src/pages/elements/datetime-picker.md +++ b/documents/src/pages/elements/datetime-picker.md @@ -248,6 +248,9 @@ section { ``` ## Set content to slots +Datetime Picker allows to add additional content to various placements around the calendar e.g. left, right. You can also slot your own content to any cells in calendar to do special styles on the cell. + +### Add contents around the calendar Use slots to add additional content into the Datetime Picker. :: @@ -343,6 +346,273 @@ section { ``` +### Custom cells +Datetime Picker allows you to customise cells on the calendar using slots. Each date cell provides slot with name in `yyyy-MM-dd` format. In case of year or month cell, the format is `yyyy` or `yyyy-MM`. + +:: +```javascript +::datetime-picker:: +const datetimePicker = document.querySelector('ef-datetime-picker'); +datetimePicker.view = '2023-04'; +``` +```html +
+
+ + +
7
+
10
+
1
+
18
+
29
+
+
+
+``` +```css +.date-input { + display: flex; + flex-direction: column; +} + +section { + display: flex; + height: 300px; + padding: 0 3px; +} + +ef-datetime-picker .holiday { + background-color: var(--color-scheme-negative); + color: var(--color-scheme-ticktext); + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} +``` +:: + +```html + +
7
+
10
+
1
+
18
+
29
+
+``` + +### Custom cells in duplex mode +In duplex mode, there are 2 calendars on the UI. Slot name of left calendar is prefixed with `from-` and another one is prefixed with `to-`. + +```html + +
7
+
10
+
1
+
18
+
29
+
7
+
10
+
1
+
18
+
29
+
+``` + +### Advanced custom cells +For more advanced use cases, you can use `before-cell-render` event to check states of each cell e.g. `range`, `selected`. See all cell states from [CalendarCell](https://github.com/Refinitiv/refinitiv-ui/blob/v6/packages/elements/src/calendar/types.ts). + +```javascript +datetimePicker.addEventListener('before-cell-render', (event) => { + const { cell, calendar } = event.detail; + ... +} +``` + +Event's `detail` object provides `cell` that being rendered and its parent calendar (in case of duplex). In duplex mode, the left calendar has id as `calendar-from` and the right calendar is `calendar-to`. + +The example below show calendar in duplex mode. You listen to `before-cell-render` event to query slot contents and uses state from `cell` and `calendar` to add CSS classes to the slot content properly. + +```html + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+``` +```javascript +const datetimePicker = document.querySelector('ef-datetime-picker'); + +datetimePicker.addEventListener('before-cell-render', (event) => { + const sourceDatetimePicker = event.target; + const { cell, calendar } = event.detail; + const prefix = calendar.id === 'calendar-to' ? 'to-' : 'from-'; + const customCell = sourceDatetimePicker.querySelector(`[slot="${prefix}${cell.value}"]`); + + // skip style overriding if there is no content for the cell + if (!customCell) { return; } + + // use text from component as calendar has built-in locale support + // for instance, Mai instead of May in German + customCell.textContent = cell.text; + + // modify classes that match to current cell state + const customCellClass = customCell.classList; + const keys = ['range', 'selected']; + for (const key of keys) { + cell[key] ? customCellClass.add(key) : customCellClass.remove(key); + } +}); +``` +```css +ef-datetime-picker .custom-cell { + ... +} +ef-datetime-picker .custom-cell.range { + ... +} +ef-datetime-picker .custom-cell.select { + ... +} +``` + +:: +```javascript +::datetime-picker:: +const datetimePicker = document.querySelector('ef-datetime-picker'); +datetimePicker.views = ['2023-04','2023-05']; + +datetimePicker?.addEventListener('before-cell-render', (event) => { + const sourceDatetimePicker = event.target; + const { cell, calendar } = event.detail; + const prefix = calendar.id === 'calendar-to' ? 'to-' : 'from-'; + const customCell = sourceDatetimePicker.querySelector(`[slot="${prefix}${cell.value}"]`); + if (!customCell) { + return; + } + + // skip style overriding if there is no content for the cell + if (!customCell) { return; } + + // use text from component as calendar has built-in locale support + // for instance, Mai instead of May in German + customCell.textContent = cell.text; + + // modify classes that match to current cell state + const customCellClass = customCell.classList; + const keys = ['range', 'selected']; + for (const key of keys) { + cell[key] ? customCellClass.add(key) : customCellClass.remove(key); + } +}); +``` +```html +
+
+
Germany Economic Events 2023
+
April
+

+ 04: Balance of Trade
+ 24: Ifo Business Climate
+ 28: GDP Growth Rate YoY Flash +

+
May
+

+ 16: ZEW Economic Sentiment Index
+ 25: GfK Consumer Confidence
+ 31: Inflation Rate YoY Prel +

+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+``` +```css +html { + --custom-cell-selected-background-color: #334bff; + --custom-cell-hover-color: #1429bd; + --custom-cell-range-background-color: #334bff33; + --custom-cell-highlight-color: #F5475B; +} + +html[prefers-color-scheme="light"] { + --custom-cell-background-color: #ffffff00; +} + +html[prefers-color-scheme="dark"] { + --custom-cell-background-color: #0d0d0d00; +} + + +section { + display: flex; + flex-direction: column; + height: 550px; + padding: 0 3px; + margin: 20px; +} + +h5, h6 { + margin-top: 0px; +} + +ef-datetime-picker .custom-cell { + background: linear-gradient(-135deg, var(--custom-cell-highlight-color) 5px, var(--custom-cell-background-color) 0); + overflow: hidden; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +ef-datetime-picker .custom-cell:hover { + background: linear-gradient(-135deg, var(--custom-cell-highlight-color) 5px, var(--custom-cell-hover-color) 0); +} + +ef-datetime-picker .custom-cell.range { + background: linear-gradient(-135deg, var(--custom-cell-highlight-color) 5px, #00000000 0); +} +ef-datetime-picker .custom-cell.selected { + background: linear-gradient(-135deg, var(--custom-cell-highlight-color) 5px, var(--custom-cell-selected-background-color) 0); +} +``` +:: ## Accessibility ::a11y-intro:: diff --git a/packages/elements/src/calendar/index.ts b/packages/elements/src/calendar/index.ts index 5b07de945f..84daebe6da 100644 --- a/packages/elements/src/calendar/index.ts +++ b/packages/elements/src/calendar/index.ts @@ -108,7 +108,7 @@ export type { CalendarFilter, BeforeCellRenderEvent }; * @attr {boolean} disabled - Set disabled state * @prop {boolean} [disabled=false] - Set disabled state * - * @slot yyyy-mm-dd - Adds slotted content into the specific date which use value in `ISO8601` date string format as a key e.g. `yyyy-MM-dd`, `yyyy-MM` and `yyyy` + * @slot yyyy-MM-dd - Adds slotted content into the specific date which use value in `ISO8601` date string format as a key e.g. `yyyy-MM-dd`, `yyyy-MM` and `yyyy` * @slot footer - Adds slotted content into the footer of the calendar control */ @customElement('ef-calendar') @@ -1423,7 +1423,8 @@ export class Calendar extends ControlElement implements MultiValue { cancelable: false, composed: true, // allow calendar customization within other elements e.g. datetime picker detail: { - cell: calendarCell + cell: calendarCell, + calendar: this } }); this.dispatchEvent(event); diff --git a/packages/elements/src/calendar/types.ts b/packages/elements/src/calendar/types.ts index c88b6959f7..0680c670e9 100644 --- a/packages/elements/src/calendar/types.ts +++ b/packages/elements/src/calendar/types.ts @@ -1,6 +1,7 @@ import { CellIndex } from '@refinitiv-ui/utils/navigation.js'; import { CalendarRenderView } from './constants.js'; +import type { Calendar } from './index.js'; export interface CellSelectionModel { selected?: boolean; @@ -62,4 +63,5 @@ export type CalendarCell = { // public API export type BeforeCellRenderEvent = CustomEvent<{ cell: CalendarCell; + calendar: Calendar; }>; diff --git a/packages/elements/src/datetime-picker/__demo__/index.html b/packages/elements/src/datetime-picker/__demo__/index.html index afcf3a3b66..92d9a1bfdd 100644 --- a/packages/elements/src/datetime-picker/__demo__/index.html +++ b/packages/elements/src/datetime-picker/__demo__/index.html @@ -16,6 +16,44 @@ ef-select { width: 300px; } + .custom-cell { + background-color: #4e7349; + border: 1px solid #fb8a03; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + .custom-cell.selected-range-boundary { + background-color: #ee82eeff; + border-color: #800080ff; + } + .custom-cell:hover { + background-color: #00c389; + border: 1px solid #00c389; + } + .custom-cell.active { + background-color: #0f1e8a; + color: #ffffff; + } + .custom-cell.range { + background-color: #1fa90a; + } + .custom-cell.now { + background-color: #f6e9afff; + } + .custom-cell.now:hover { + background-color: #f4ce27ff; + } + .custom-cell.disabled { + background-color: #df2929ff; + } + section { + display: flex; + align-items: center; + flex-direction: column; + } @@ -434,5 +472,129 @@ })(); + + + +
10
+
11
+
Oct
+
2023
+
+ +
10
+
11
+
Oct
+
2023
+
10
+
11
+
Nov
+
2023
+
10
+
11
+
Oct
+
2023
+
10
+
11
+
Nov
+
2023
+
+
+ +
+

+ Every 2 second, custom cell slot will be moved to another day and call + updateCalendarSlot() to cascade custom slot to calendar. +

+ +
+
diff --git a/packages/elements/src/datetime-picker/__snapshots__/DOMStructure.md b/packages/elements/src/datetime-picker/__snapshots__/DOMStructure.md index b324fb2356..833175d2e2 100644 --- a/packages/elements/src/datetime-picker/__snapshots__/DOMStructure.md +++ b/packages/elements/src/datetime-picker/__snapshots__/DOMStructure.md @@ -89,7 +89,7 @@ ```html
+ + +
+ + + + + +
+ + +
+
+
+ + +
+
+
+ + + + +
+
+
+ + +
+
+
+ + +
+
+ +``` + +#### `DOM structure is correct when add custom cell slot of calendar with prefix` + +```html +
+ + +
+ + + + + +
+ + +
+
+
+ + +
+
+
+ + + + + + + + +
+
+
+ + +
+
+
+ + +
+
+ +``` + +#### `DOM structure is correct when add custom cell slot of calendar while overlay is opened` + +```html +
+ + +
+ + + + + +
+ + +
+
+
+ + +
+
+
+ + + + + + + + +
+
+
+ + +
+
+
+ + +
+
+ +``` + +#### `DOM structure should not contain added custom cell slot when overlay is closed` + +```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 cad3f70257..9021a1d84f 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 @@ -126,7 +126,7 @@ describe('datetime-picker/DatetimePicker', function () { const el = await fixture(''); el.placeholder = placeholder; await elementUpdated(el); - const inputFrom = el.inputEl; + const inputFrom = el.inputFromEl; const inputTo = el.inputToEl; 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'); 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 index 6ae200ba8e..a1a5f42ebd 100644 --- a/packages/elements/src/datetime-picker/__test__/datetime-picker.snapshot.test.js +++ b/packages/elements/src/datetime-picker/__test__/datetime-picker.snapshot.test.js @@ -2,10 +2,15 @@ import '@refinitiv-ui/elements/datetime-picker'; import '@refinitiv-ui/elemental-theme/light/ef-datetime-picker'; -import { expect, fixture, nextFrame } from '@refinitiv-ui/test-helpers'; +import { elementUpdated, expect, fixture, nextFrame } from '@refinitiv-ui/test-helpers'; import { snapshotIgnore } from './utils.js'; +const slotFrom = document.createElement('div'); +const slotTo = document.createElement('div'); +slotFrom.setAttribute('slot', 'from-2020-04-01'); +slotTo.setAttribute('slot', 'to-2020-05-01'); + describe('datetime-picker/DOMStructure', function () { describe('DOM Structure', function () { it('DOM structure is correct', async function () { @@ -17,49 +22,91 @@ describe('datetime-picker/DOMStructure', function () { const el = await fixture( '' ); - await nextFrame(); - await nextFrame(); /* second frame required for IE11 as popup opened might not fit into one frame */ + await nextFrame(2); /* 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 function () { const el = await fixture( '' ); - await nextFrame(); - await nextFrame(); + await nextFrame(2); expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); }); it('DOM structure is correct when duplex', async function () { const el = await fixture( '' ); - await nextFrame(); - await nextFrame(); + await nextFrame(2); expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); }); it('DOM structure is correct when timepicker', async function () { const el = await fixture( '' ); - await nextFrame(); - await nextFrame(); + await nextFrame(2); expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); }); it('DOM structure is correct when timepicker and with-seconds', async function () { const el = await fixture( '' ); - await nextFrame(); - await nextFrame(); + await nextFrame(2); expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); }); it('DOM structure is correct when range timepicker', async function () { const el = await fixture( '' ); - await nextFrame(); - await nextFrame(); + await nextFrame(2); expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); }); + it('DOM structure is correct when add custom cell slot of calendar without prefix', async function () { + const el = await fixture( + '' + + '
' + + '
' + ); + el.opened = true; + + await elementUpdated(el); + await nextFrame(2); + await expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); + }); + it('DOM structure is correct when add custom cell slot of calendar with prefix', async function () { + const el = await fixture( + '' + ); + el.appendChild(slotFrom); + el.appendChild(slotTo); + el.opened = true; + + await elementUpdated(el); + await nextFrame(2); + await expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); + }); + it('DOM structure is correct when add custom cell slot of calendar while overlay is opened', async function () { + const el = await fixture( + '' + ); + el.appendChild(slotFrom); + el.appendChild(slotTo); + el.updateCalendarSlot(); + + await elementUpdated(el); + await nextFrame(2); + await expect(el).shadowDom.to.equalSnapshot(snapshotIgnore); + }); + it('DOM structure should not contain added custom cell slot when overlay is closed', async function () { + const el = await fixture( + '' + ); + el.appendChild(slotFrom); + el.appendChild(slotTo); + el.opened = false; + + await elementUpdated(el); + await nextFrame(2); + await 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 7d2876b287..6cb51402b9 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 @@ -59,7 +59,7 @@ describe('datetime-picker/Value', function () { el.values = ['2022-03-15', '2022-04-23']; await elementUpdated(el); expect(el.error).to.be.equal(true); - typeText(el.inputEl, ''); + typeText(el.inputFromEl, ''); await elementUpdated(el); expect(el.error).to.be.equal(false, 'input empty string must not make element error in range mode'); }); @@ -94,23 +94,23 @@ describe('datetime-picker/Value', function () { }); it('It should be able to clear input values when user type invalid format for range mode', async function () { const el = await fixture(''); - const input = el.inputEl; + const inputFrom = el.inputFromEl; const inputTo = el.inputToEl; - await triggerFocusFor(input); + await triggerFocusFor(inputFrom); await triggerFocusFor(inputTo); - typeText(input, 'Invalid Value 1'); + typeText(inputFrom, 'Invalid Value 1'); typeText(inputTo, 'Invalid Value 2'); await elementUpdated(el); - expect(el.inputEl.value).to.be.equal('Invalid Value 1'); + expect(el.inputFromEl.value).to.be.equal('Invalid Value 1'); expect(el.inputToEl.value).to.be.equal('Invalid Value 2'); el.values = []; await triggerFocusFor(el); await elementUpdated(el); - expect(el.inputEl.value).to.be.equal(''); + expect(el.inputFromEl.value).to.be.equal(''); expect(el.inputToEl.value).to.be.equal(''); expect(el.error).to.be.equal(false); }); @@ -182,8 +182,8 @@ describe('datetime-picker/Value', function () { await nextFrame(); await nextFrame(); - const calendarEl = el.calendarEl; - const fromCell = calendarEl.shadowRoot.querySelectorAll('div[tabindex]')[0]; // 2020-04-01 + const calendarFromEl = el.calendarFromEl; + const fromCell = calendarFromEl.shadowRoot.querySelectorAll('div[tabindex]')[0]; // 2020-04-01 fromCell.click(); await elementUpdated(el); await nextFrame(); @@ -197,7 +197,7 @@ describe('datetime-picker/Value', function () { 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.inputFromEl.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'); }); it('It should not be possible to deselect values in range duplex mode', async function () { @@ -206,8 +206,8 @@ describe('datetime-picker/Value', function () { await elementUpdated(el); await nextFrame(2); - const calendarEl = el.calendarEl; - const fromCell = calendarEl.shadowRoot.querySelectorAll('div[tabindex]')[0]; // 2020-04-01 + const calendarFromEl = el.calendarFromEl; + const fromCell = calendarFromEl.shadowRoot.querySelectorAll('div[tabindex]')[0]; // 2020-04-01 fromCell.click(); await elementUpdated(el); await nextFrame(); @@ -221,7 +221,7 @@ describe('datetime-picker/Value', function () { 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.inputFromEl.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'); toCell.click(); 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 2911e7f9d2..f4b699af53 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 @@ -86,7 +86,7 @@ describe('datetime-picker/View', function () { }); it('Duplex split range view changes when typing the value', async function () { const el = await fixture(''); - const inputFrom = el.inputEl; + const inputFrom = el.inputFromEl; const inputTo = el.inputToEl; typeText(inputFrom, '21-Jan-2020'); typeText(inputTo, '21-Apr-2020'); @@ -112,7 +112,7 @@ describe('datetime-picker/View', function () { ); el.views = ['2020-01', '2020-04']; await elementUpdated(el); - const calendarFrom = el.calendarEl; + const calendarFrom = el.calendarFromEl; 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'); @@ -142,7 +142,7 @@ describe('datetime-picker/View', function () { const el = await fixture( '' ); - const calendarFrom = el.calendarEl; + const calendarFrom = el.calendarFromEl; const calendarTo = el.calendarToEl; await elementUpdated(calendarFrom); await elementUpdated(calendarTo); @@ -169,7 +169,7 @@ describe('datetime-picker/View', function () { ); el.views = ['2020-04', '2020-05']; await elementUpdated(el); - const calendarFrom = el.calendarEl; + const calendarFrom = el.calendarFromEl; const calendarTo = el.calendarToEl; calendarClickNext(calendarFrom); await elementUpdated(el); @@ -207,7 +207,7 @@ describe('datetime-picker/View', function () { '' ); el.values = ['1997-04-01']; - const calendarFrom = el.calendarEl; + const calendarFrom = el.calendarFromEl; const calendarTo = el.calendarToEl; await elementUpdated(el); expect(calendarFrom.view).to.equal('1997-04', 'Calendar from view is not updated to the value'); diff --git a/packages/elements/src/datetime-picker/index.ts b/packages/elements/src/datetime-picker/index.ts index 55007ac05f..1d0bab03c2 100644 --- a/packages/elements/src/datetime-picker/index.ts +++ b/packages/elements/src/datetime-picker/index.ts @@ -63,6 +63,19 @@ const INPUT_FORMAT = { DATETIME_SECONDS_AM_PM: 'dd-MMM-yyyy hh:mm:ss aaa' }; +// public API +const CALENDAR_ID = 'calendar'; +const CALENDAR_FROM_ID = 'calendar-from'; +const CALENDAR_TO_ID = 'calendar-to'; + +const TIMEPICKER_ID = 'timepicker'; +const TIMEPICKER_FROM_ID = 'timepicker-from'; +const TIMEPICKER_TO_ID = 'timepicker-to'; + +const INPUT_ID = 'input'; +const INPUT_FROM_ID = 'input-from'; +const INPUT_TO_ID = 'input-to'; + /** * Control to pick date and time * @@ -81,6 +94,9 @@ const INPUT_FORMAT = { * @slot right - Slot to add custom contents at the right of popup * @slot footer - Slot to add custom contents at the bottom of popup * @slot left - Slot to add custom contents at the left of popup + * @slot yyyy-MM-dd - Slot to add custom contents on any date cells e.g. `2023-01-01`. Use `yyyy` or `yyyy-MM` if the cell is year or month. + * @slot from-yyyy-MM-dd - Slot to add custom contents on any date cells of left calendar in `duplex` mode e.g. `from-2023-01-01`. Use `from-yyyy` or `from-yyyy-MM` if the cell is year or month + * @slot to-yyyy-MM-dd - Slot to add custom contents on any date cells of right calendar in `duplex` mode e.g. `to-2023-01-01`. Use `to-yyyy` or `to-yyyy-MM` if the cell is year or month */ @customElement('ef-datetime-picker') export class DatetimePicker extends ControlElement implements MultiValue { @@ -478,10 +494,13 @@ export class DatetimePicker extends ControlElement implements MultiValue { @query('[part=icon]', true) private iconEl!: Icon; @query('[part=list]') private popupEl?: Overlay | null; @query('#timepicker') private timepickerEl?: TimePicker | null; + @query('#timepicker-from') private timepickerFromEl?: TimePicker | null; @query('#timepicker-to') private timepickerToEl?: TimePicker | null; @query('#calendar') private calendarEl?: Calendar | null; + @query('#calendar-from') private calendarFromEl?: Calendar | null; @query('#calendar-to') private calendarToEl?: Calendar | null; @query('#input') private inputEl?: TextField | null; + @query('#input-from') private inputFromEl?: TextField | null; @query('#input-to') private inputToEl?: TextField | null; /** @@ -940,14 +959,21 @@ export class DatetimePicker extends ControlElement implements MultiValue { // in duplex mode, avoid jumping on views // Therefore if any of values have changed, save the current view - if (this.isDuplex() && this.calendarEl && this.calendarToEl) { - this.notifyViewsChange([this.calendarEl?.view, this.calendarToEl?.view]); + if (this.isDuplex() && this.calendarFromEl && this.calendarToEl) { + this.notifyViewsChange([this.calendarFromEl.view, this.calendarToEl.view]); } // Close popup if there is no time picker const newValues = this.values; if (!this.timepicker && newValues[0] && (this.range ? newValues[1] : true)) { this.setOpened(false); + + /** + * Custom cell selection delegates focus back to the text field when the overlay is closed, + * causing a sync problem between the calendar and text field. + * A workaround involves blurring the text field again. + */ + this.isDuplex() ? this.inputFromEl?.blur() : this.inputEl?.blur(); } } @@ -1148,13 +1174,26 @@ export class DatetimePicker extends ControlElement implements MultiValue { this.notifyViewsChange([]); } + /** + * Request to update slot on the calendar while the overlay is open + * @returns void + */ + public updateCalendarSlot(): void { + if (this.opened) { + this.requestUpdate(); + } + } + /** * Get time picker template * @param id Timepicker identifier * @param value Time picker value * @returns template result */ - private getTimepickerTemplate(id: 'timepicker' | 'timepicker-to', value = ''): TemplateResult { + private getTimepickerTemplate( + id: 'timepicker' | 'timepicker-from' | 'timepicker-to', + value = '' + ): TemplateResult { return html``; } + /** + * Create calendar slot + * @param calendarId Calendar identifier + * @returns calendarSlots slots that will cascade to calendar + */ + private createCalendarSlots(calendarId: string): HTMLSlotElement[] | null { + if (!this.opened) { + return null; + } + + const isValidDateSlot = (slot: Element, prefix = '') => { + return new RegExp(`^${prefix}-?\\d{1,6}(-\\d{2}(-\\d{2})?)?$`).test(slot.slot); + }; + + const querySlots: Element[] | null = Array.from(this.querySelectorAll('[slot]')); + return querySlots + .filter((slot) => { + const isToSlot = calendarId === CALENDAR_TO_ID && isValidDateSlot(slot, 'to-'); + const isFromSlot = calendarId === CALENDAR_FROM_ID && isValidDateSlot(slot, 'from-'); + const isISODateSlot = calendarId === CALENDAR_ID && isValidDateSlot(slot); + return isToSlot || isFromSlot || isISODateSlot; + }) + .map((slot) => { + const newSlot = document.createElement('slot'); + newSlot.name = slot.slot; + newSlot.slot = slot.slot.replace(/^(to|from)-/, ''); + return newSlot; + }); + } + /** * Get calendar template * @param id Calendar identifier * @param view Calendar view * @returns template result */ - private getCalendarTemplate(id: 'calendar' | 'calendar-to', view = ''): TemplateResult { + private getCalendarTemplate(id: 'calendar' | 'calendar-from' | 'calendar-to', view = ''): TemplateResult { + const slotContent = this.createCalendarSlots(id); return html``; + >${slotContent}
`; } /** @@ -1198,8 +1269,8 @@ export class DatetimePicker extends ControlElement implements MultiValue { */ private get calendarsTemplate(): TemplateResult { return html` - ${this.getCalendarTemplate('calendar', this.views[0])} - ${this.isDuplex() ? this.getCalendarTemplate('calendar-to', this.views[1]) : undefined} + ${this.getCalendarTemplate(this.isDuplex() ? CALENDAR_FROM_ID : CALENDAR_ID, this.views[0])} + ${this.isDuplex() ? this.getCalendarTemplate(CALENDAR_TO_ID, this.views[1]) : undefined} `; } @@ -1210,9 +1281,9 @@ export class DatetimePicker extends ControlElement implements MultiValue { // TODO: how can we add support timepicker with multiple? const values = this.timepickerValues; return html` - ${this.getTimepickerTemplate('timepicker', values[0])} + ${this.getTimepickerTemplate(this.range ? TIMEPICKER_FROM_ID : TIMEPICKER_ID, values[0])} ${this.range ? html`
` : undefined} - ${this.range ? this.getTimepickerTemplate('timepicker-to', values[1]) : undefined} + ${this.range ? this.getTimepickerTemplate(TIMEPICKER_TO_ID, values[1]) : undefined} `; } @@ -1222,7 +1293,7 @@ export class DatetimePicker extends ControlElement implements MultiValue { * @param value Input value * @returns template result */ - private getInputTemplate(id: 'input' | 'input-to', value = ''): TemplateResult { + private getInputTemplate(id: 'input' | 'input-from' | 'input-to', value = ''): TemplateResult { return html` - ${this.getInputTemplate('input', values[0])} + ${this.getInputTemplate(this.range ? INPUT_FROM_ID : INPUT_ID, values[0])} ${this.range ? html`
` : undefined} - ${this.range ? this.getInputTemplate('input-to', values[1]) : undefined} + ${this.range ? this.getInputTemplate(INPUT_TO_ID, values[1]) : undefined}
`; }