diff --git a/src/components/date-range-picker/date-range-input.ts b/src/components/date-range-picker/date-range-input.ts new file mode 100644 index 000000000..207f5d74c --- /dev/null +++ b/src/components/date-range-picker/date-range-input.ts @@ -0,0 +1,435 @@ +import { LitElement } from 'lit'; +import { property } from 'lit/decorators.js'; +import { CalendarDay } from '../calendar/model.js'; +import { watch } from '../common/decorators/watch.js'; +import { registerComponent } from '../common/definitions/register.js'; +import { FormValueDateRangeTransformers } from '../common/mixins/forms/form-transformers.js'; +import { createFormValueState } from '../common/mixins/forms/form-value.js'; +import { createCounter, equal } from '../common/util.js'; +import { IgcDateTimeInputBaseComponent } from '../date-time-input/date-time-input.base.js'; +import { + DatePart, + type DatePartDeltas, + DateParts, + type DateRangePart, + type DateRangePartInfo, + DateRangePosition, + DateTimeUtil, +} from '../date-time-input/date-util.js'; +import type { DateRangeValue } from './date-range-picker.js'; +import { isCompleteDateRange } from './validators.js'; + +const SINGLE_INPUT_SEPARATOR = ' - '; + +export default class IgcDateRangeInputComponent extends IgcDateTimeInputBaseComponent< + DateRangeValue | null, + DateRangePart, + DateRangePartInfo +> { + public static readonly tagName = 'igc-date-range-input'; + + protected static shadowRootOptions = { + ...LitElement.shadowRootOptions, + delegatesFocus: true, + }; + + /* blazorSuppress */ + public static register() { + registerComponent(IgcDateRangeInputComponent); + } + + // #region Properties + + private static readonly increment = createCounter(); + + protected override inputId = `date-range-input-${IgcDateRangeInputComponent.increment()}`; + protected override _datePartDeltas: DatePartDeltas = { + date: 1, + month: 1, + year: 1, + }; + + private _oldRangeValue: DateRangeValue | null = null; + + protected override _inputFormat!: string; + + protected override readonly _formValue = createFormValueState(this, { + initialValue: { start: null, end: null }, + transformers: FormValueDateRangeTransformers, + }); + + protected override get targetDatePart(): DateRangePart | undefined { + let result: DateRangePart | undefined; + + if (this.focused) { + const part = this._inputDateParts.find( + (p) => + p.start <= this.inputSelection.start && + this.inputSelection.start <= p.end && + p.type !== DateParts.Literal + ); + const partType = part?.type as string as DatePart; + + if (partType) { + result = { part: partType, position: part?.position! }; + } + } else { + result = { + part: this._inputDateParts[0].type as string as DatePart, + position: DateRangePosition.Start, + }; + } + + return result; + } + + public get value(): DateRangeValue | null { + return this._formValue.value; + } + + public set value(value: DateRangeValue | null) { + this._formValue.setValueAndFormState(value as DateRangeValue | null); + this.updateMask(); + } + + /** + * The date format to apply on the input. + * @attr input-format + */ + @property({ attribute: 'input-format' }) + public override get inputFormat(): string { + return ( + this._inputFormat || this._defaultMask?.split(SINGLE_INPUT_SEPARATOR)[0] + ); + } + + public override set inputFormat(value: string) { + if (value) { + this._inputFormat = value; + this.setMask(value); + if (this.value) { + this.updateMask(); + } + } + } + + // #endregion + + // #region Methods + + @watch('displayFormat') + protected _onDisplayFormatChange() { + this.updateMask(); + } + + protected override setMask(string: string) { + const oldFormat = this._inputDateParts?.map((p) => p.format).join(''); + this._inputDateParts = DateTimeUtil.parseDateTimeFormat(string); + const startParts = this._inputDateParts.map((part) => ({ + ...part, + position: DateRangePosition.Start, + })) as DateRangePartInfo[]; + + const separatorStart = startParts[startParts.length - 1].end; + const separatorParts: DateRangePartInfo[] = []; + + for (let i = 0; i < SINGLE_INPUT_SEPARATOR.length; i++) { + const element = SINGLE_INPUT_SEPARATOR.charAt(i); + + separatorParts.push({ + type: DateParts.Literal, + format: element, + start: separatorStart + i, + end: separatorStart + i + 1, + position: DateRangePosition.Separator, + }); + } + + let currentPosition = separatorStart + SINGLE_INPUT_SEPARATOR.length; + + // Clone original parts, adjusting positions + const endParts: DateRangePartInfo[] = startParts.map((part) => { + const length = part.end - part.start; + const newPart: DateRangePartInfo = { + type: part.type, + format: part.format, + start: currentPosition, + end: currentPosition + length, + position: DateRangePosition.End, + }; + currentPosition += length; + return newPart; + }); + + this._inputDateParts = [...startParts, ...separatorParts, ...endParts]; + + this._defaultMask = this._inputDateParts.map((p) => p.format).join(''); + + const value = this._defaultMask; + this._mask = (value || DateTimeUtil.DEFAULT_INPUT_FORMAT).replace( + new RegExp(/(?=[^t])[\w]/, 'g'), + '0' + ); + + this.parser.mask = this._mask; + this.parser.prompt = this.prompt; + + if (!this.placeholder || oldFormat === this.placeholder) { + this.placeholder = value; + } + } + + protected override getMaskedValue() { + let mask = this.emptyMask; + + if (DateTimeUtil.isValidDate(this.value?.start)) { + const startParts = this._inputDateParts.filter( + (p) => p.position === DateRangePosition.Start + ); + mask = this._setDatePartInMask(mask, startParts, this.value.start); + } + if (DateTimeUtil.isValidDate(this.value?.end)) { + const endParts = this._inputDateParts.filter( + (p) => p.position === DateRangePosition.End + ); + mask = this._setDatePartInMask(mask, endParts, this.value.end); + return mask; + } + + return this.maskedValue === '' ? mask : this.maskedValue; + } + + protected override getNewPosition(value: string, direction = 0): number { + let cursorPos = this.selection.start; + + const separatorPart = this._inputDateParts.find( + (part) => part.position === DateRangePosition.Separator + ); + + if (!direction) { + const firstSeparator = + this._inputDateParts.find( + (p) => p.position === DateRangePosition.Separator + )?.start ?? 0; + const lastSeparator = + this._inputDateParts.findLast( + (p) => p.position === DateRangePosition.Separator + )?.end ?? 0; + // Last literal before the current cursor position or start of input value + let part = this._inputDateParts.findLast( + (part) => part.type === DateParts.Literal && part.end < cursorPos + ); + // skip over the separator parts + if ( + part?.position === DateRangePosition.Separator && + cursorPos === lastSeparator + ) { + cursorPos = firstSeparator; + part = this._inputDateParts.findLast( + (part) => part.type === DateParts.Literal && part.end < cursorPos + ); + } + return part?.end ?? 0; + } + + if ( + separatorPart && + cursorPos >= separatorPart.start && + cursorPos <= separatorPart.end + ) { + // Cursor is inside the separator; skip over it + cursorPos = separatorPart.end + 1; + } + // First literal after the current cursor position or end of input value + const part = this._inputDateParts.find( + (part) => part.type === DateParts.Literal && part.start > cursorPos + ); + return part?.start ?? value.length; + } + + protected override updateValue(): void { + if (this.isComplete()) { + const parsedRange = this._parseRangeValue(this.maskedValue); + this.value = parsedRange; + } else { + this.value = null; + } + } + + protected override async handleFocus() { + this.focused = true; + + if (this.readOnly) { + return; + } + this._oldRangeValue = this.value; + const areFormatsDifferent = this.displayFormat !== this.inputFormat; + + if (!this.value || !this.value.start || !this.value.end) { + this.maskedValue = this.emptyMask; + await this.updateComplete; + this.select(); + } else if (areFormatsDifferent) { + this.updateMask(); + } + } + + protected override async handleBlur() { + const isEmptyMask = this.maskedValue === this.emptyMask; + const isSameValue = equal(this._oldRangeValue, this.value); + + this.focused = false; + + if (!(this.isComplete() || isEmptyMask)) { + const parse = this._parseRangeValue(this.maskedValue); + + if (parse) { + this.value = parse; + } else { + this.value = null; + this.maskedValue = ''; + } + } else { + this.updateMask(); + } + + if (!(this.readOnly || isSameValue)) { + this.emitEvent('igcChange', { detail: this.value }); + } + + this._handleBlur(); + } + + protected override spinValue( + datePart: DateRangePart, + delta: number + ): DateRangeValue { + if (!isCompleteDateRange(this.value)) { + return { start: CalendarDay.today.native, end: CalendarDay.today.native }; + } + + let newDate = this.value?.start + ? CalendarDay.from(this.value.start).native + : CalendarDay.today.native; + if (datePart.position === DateRangePosition.End) { + newDate = this.value?.end + ? CalendarDay.from(this.value.end).native + : CalendarDay.today.native; + } + + switch (datePart.part) { + case DatePart.Date: + DateTimeUtil.spinDate(delta, newDate, this.spinLoop); + break; + case DatePart.Month: + DateTimeUtil.spinMonth(delta, newDate, this.spinLoop); + break; + case DatePart.Year: + DateTimeUtil.spinYear(delta, newDate); + break; + } + const value = { + ...this.value, + [datePart.position]: newDate, + } as DateRangeValue; + return value; + } + + protected override updateMask() { + if (this.focused) { + this.maskedValue = this.getMaskedValue(); + } else { + if (!isCompleteDateRange(this.value)) { + this.maskedValue = ''; + return; + } + + const { formatDate, predefinedToDateDisplayFormat } = DateTimeUtil; + + const { start, end } = this.value; + const format = + predefinedToDateDisplayFormat(this.displayFormat) ?? + this.displayFormat ?? + this.inputFormat; + + this.maskedValue = format + ? `${formatDate(start, this.locale, format)} - ${formatDate(end, this.locale, format)}` + : `${start.toLocaleDateString()} - ${end.toLocaleDateString()}`; + } + } + + protected override handleInput() { + this._setTouchedState(); + this.emitEvent('igcInput', { detail: JSON.stringify(this.value) }); + } + + private _setDatePartInMask( + mask: string, + parts: DateRangePartInfo[], + value: Date | null + ): string { + let resultMask = mask; + for (const part of parts) { + if (part.type === DateParts.Literal) { + continue; + } + + const targetValue = DateTimeUtil.getPartValue( + part, + part.format.length, + value + ); + + resultMask = this.parser.replace( + resultMask, + targetValue, + part.start, + part.end + ).value; + } + return resultMask; + } + + private _parseRangeValue(value: string): DateRangeValue | null { + const dates = value.split(SINGLE_INPUT_SEPARATOR); + if (dates.length !== 2) { + return null; + } + + const startParts = this._inputDateParts.filter( + (p) => p.position === DateRangePosition.Start + ); + + const endPartsOriginal = this._inputDateParts.filter( + (p) => p.position === DateRangePosition.End + ); + + // Rebase endParts to start from 0, so they can be parsed on their own + const offset = endPartsOriginal.length > 0 ? endPartsOriginal[0].start : 0; + const endParts = endPartsOriginal.map((p) => ({ + ...p, + start: p.start - offset, + end: p.end - offset, + })); + + const start = DateTimeUtil.parseValueFromMask( + dates[0], + startParts, + this.prompt + ); + + const end = DateTimeUtil.parseValueFromMask( + dates[1], + endParts, + this.prompt + ); + + return { start: start ?? null, end: end ?? null }; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'igc-date-range-input': IgcDateRangeInputComponent; + } +} diff --git a/src/components/date-range-picker/date-range-picker-single.form.spec.ts b/src/components/date-range-picker/date-range-picker-single.form.spec.ts index 77d592d4a..943fbb117 100644 --- a/src/components/date-range-picker/date-range-picker-single.form.spec.ts +++ b/src/components/date-range-picker/date-range-picker-single.form.spec.ts @@ -11,7 +11,8 @@ import { type ValidationContainerTestsParams, ValidityHelpers, } from '../common/validity-helpers.spec.js'; -import IgcInputComponent from '../input/input.js'; +import type IgcDateRangeInputComponentComponent from './date-range-input.js'; +import IgcDateRangeInputComponent from './date-range-input.js'; import IgcDateRangePickerComponent, { type DateRangeValue, } from './date-range-picker.js'; @@ -21,7 +22,7 @@ describe('Date Range Picker Single Input - Form integration', () => { before(() => defineComponents(IgcDateRangePickerComponent)); let picker: IgcDateRangePickerComponent; - let input: IgcInputComponent; + let input: IgcDateRangeInputComponentComponent; let startKey = ''; let endKey = ''; @@ -67,8 +68,8 @@ describe('Date Range Picker Single Input - Form integration', () => { await elementUpdated(spec.element); input = picker.renderRoot.querySelector( - IgcInputComponent.tagName - ) as IgcInputComponent; + IgcDateRangeInputComponent.tagName + ) as IgcDateRangeInputComponent; checkSelectedRange(spec.element, value, false); @@ -76,7 +77,7 @@ describe('Date Range Picker Single Input - Form integration', () => { await elementUpdated(spec.element); expect(spec.element.value).to.deep.equal(initial); - expect(input.value).to.equal(''); + expect(input.value).to.deep.equal({ start: null, end: null }); }); it('should not be in invalid state on reset for a required control which previously had value', () => { @@ -84,7 +85,9 @@ describe('Date Range Picker Single Input - Form integration', () => { spec.setProperties({ required: true }); spec.assertSubmitPasses(); - const input = picker.renderRoot.querySelector(IgcInputComponent.tagName)!; + const input = picker.renderRoot.querySelector( + IgcDateRangeInputComponent.tagName + )!; expect(input.invalid).to.be.false; spec.setProperties({ value: null }); @@ -100,7 +103,7 @@ describe('Date Range Picker Single Input - Form integration', () => { spec.assertSubmitFails(); await elementUpdated(spec.element); const input = spec.element.renderRoot.querySelector( - IgcInputComponent.tagName + IgcDateRangeInputComponent.tagName )!; expect(input.invalid).to.be.true; @@ -118,10 +121,10 @@ describe('Date Range Picker Single Input - Form integration', () => { await elementUpdated(spec.element); input = spec.element.renderRoot.querySelector( - IgcInputComponent.tagName - ) as IgcInputComponent; + IgcDateRangeInputComponent.tagName + ) as IgcDateRangeInputComponent; - expect(input.value).to.equal(''); + expect(input.value).to.deep.equal({ start: null, end: null }); spec.reset(); await elementUpdated(spec.element); @@ -146,7 +149,7 @@ describe('Date Range Picker Single Input - Form integration', () => { spec.assertSubmitFails(); await elementUpdated(spec.element); const input = spec.element.renderRoot.querySelector( - IgcInputComponent.tagName + IgcDateRangeInputComponent.tagName )!; expect(input.invalid).to.be.true; @@ -162,7 +165,7 @@ describe('Date Range Picker Single Input - Form integration', () => { spec.assertSubmitPasses(); await elementUpdated(spec.element); const input = spec.element.renderRoot.querySelector( - IgcInputComponent.tagName + IgcDateRangeInputComponent.tagName )!; expect(input.invalid).to.be.false; @@ -205,7 +208,7 @@ describe('Date Range Picker Single Input - Form integration', () => { spec.assertSubmitPasses(); await elementUpdated(spec.element); const input = spec.element.renderRoot.querySelector( - IgcInputComponent.tagName + IgcDateRangeInputComponent.tagName )!; expect(input.invalid).to.be.false; @@ -246,7 +249,7 @@ describe('Date Range Picker Single Input - Form integration', () => { spec.assertSubmitPasses(); await elementUpdated(spec.element); const input = spec.element.renderRoot.querySelector( - IgcInputComponent.tagName + IgcDateRangeInputComponent.tagName )!; expect(input.invalid).to.be.false; @@ -279,7 +282,7 @@ describe('Date Range Picker Single Input - Form integration', () => { spec.assertSubmitPasses(); await elementUpdated(spec.element); const input = spec.element.renderRoot.querySelector( - IgcInputComponent.tagName + IgcDateRangeInputComponent.tagName )!; expect(input.invalid).to.be.false; @@ -323,7 +326,7 @@ describe('Date Range Picker Single Input - Form integration', () => { spec.assertSubmitFails(); await elementUpdated(spec.element); const input = spec.element.renderRoot.querySelector( - IgcInputComponent.tagName + IgcDateRangeInputComponent.tagName )!; expect(input.invalid).to.be.true; @@ -383,7 +386,7 @@ describe('Date Range Picker Single Input - Form integration', () => { spec.assertSubmitFails(); await elementUpdated(spec.element); const input = spec.element.renderRoot.querySelector( - IgcInputComponent.tagName + IgcDateRangeInputComponent.tagName )!; expect(input.invalid).to.be.true; @@ -416,8 +419,8 @@ describe('Date Range Picker Single Input - Form integration', () => { html`` ); const input = picker.renderRoot.querySelector( - IgcInputComponent.tagName - ) as IgcInputComponent; + IgcDateRangeInputComponent.tagName + ) as IgcDateRangeInputComponent; expect(picker.invalid).to.be.false; expect(input.invalid).to.be.false; diff --git a/src/components/date-range-picker/date-range-picker-single.spec.ts b/src/components/date-range-picker/date-range-picker-single.spec.ts index 0f16f9e26..8afceb60c 100644 --- a/src/components/date-range-picker/date-range-picker-single.spec.ts +++ b/src/components/date-range-picker/date-range-picker-single.spec.ts @@ -11,21 +11,26 @@ import { CalendarDay } from '../calendar/model.js'; import { altKey, arrowDown, + arrowLeft, + arrowRight, arrowUp, + ctrlKey, escapeKey, } from '../common/controllers/key-bindings.js'; import { defineComponents } from '../common/definitions/defineComponents.js'; import { isFocused, simulateClick, + simulateInput, simulateKeyboard, } from '../common/utils.spec.js'; import type IgcDialogComponent from '../dialog/dialog.js'; -import IgcInputComponent from '../input/input.js'; +import IgcDateRangeInputComponent from './date-range-input.js'; import IgcDateRangePickerComponent from './date-range-picker.js'; import { checkSelectedRange, getIcon, + getInput, selectDates, } from './date-range-picker.utils.spec.js'; @@ -33,7 +38,8 @@ describe('Date range picker - single input', () => { before(() => defineComponents(IgcDateRangePickerComponent)); let picker: IgcDateRangePickerComponent; - let input: IgcInputComponent; + let rangeInput: IgcDateRangeInputComponent; + let input: HTMLInputElement; let calendar: IgcCalendarComponent; const clearIcon = 'input_clear'; @@ -44,7 +50,10 @@ describe('Date range picker - single input', () => { picker = await fixture( html`` ); - input = picker.renderRoot.querySelector(IgcInputComponent.tagName)!; + rangeInput = picker.renderRoot.querySelector( + IgcDateRangeInputComponent.tagName + )!; + rangeInput.renderRoot.querySelector('input')!; calendar = picker.renderRoot.querySelector(IgcCalendarComponent.tagName)!; }); @@ -135,9 +144,10 @@ describe('Date range picker - single input', () => { const eventSpy = spy(picker, 'emitEvent'); // current implementation of DRP single input is not editable; // to refactor when the input is made editable - //picker.nonEditable = true; + picker.nonEditable = true; await elementUpdated(picker); + input = getInput(picker); input.focus(); simulateKeyboard(input, arrowDown); await elementUpdated(picker); @@ -169,7 +179,9 @@ describe('Date range picker - single input', () => { Object.assign(picker, propsSingle); await elementUpdated(picker); - const input = picker.renderRoot.querySelector(IgcInputComponent.tagName)!; + const input = picker.renderRoot.querySelector( + IgcDateRangeInputComponent.tagName + )!; for (const [prop, value] of Object.entries(propsSingle)) { expect((input as any)[prop], `Fail for ${prop}`).to.equal(value); } @@ -195,9 +207,7 @@ describe('Date range picker - single input', () => { picker.displayFormat = obj.format; await elementUpdated(picker); - const input = picker.renderRoot.querySelector( - IgcInputComponent.tagName - )!; + input = getInput(picker); expect(input.value).to.equal( `${obj.formattedValue} - ${obj.formattedValue}` ); @@ -212,7 +222,8 @@ describe('Date range picker - single input', () => { end: CalendarDay.from(new Date(2025, 3, 10)).native, }; await elementUpdated(picker); - const input = picker.renderRoot.querySelector(IgcInputComponent.tagName)!; + + input = getInput(picker); expect(input.value).to.equal('04/09/2025 - 04/10/2025'); picker.locale = 'bg'; @@ -225,7 +236,8 @@ describe('Date range picker - single input', () => { it('should set the default placeholder of the single input to the input format (like dd/MM/yyyy - dd/MM/yyyy)', async () => { picker.useTwoInputs = false; await elementUpdated(picker); - const input = picker.renderRoot.querySelector(IgcInputComponent.tagName)!; + + input = getInput(picker); expect(input.placeholder).to.equal( `${picker.inputFormat} - ${picker.inputFormat}` ); @@ -246,7 +258,7 @@ describe('Date range picker - single input', () => { }; await elementUpdated(picker); - const input = picker.renderRoot.querySelector(IgcInputComponent.tagName)!; + input = getInput(picker); expect(input.value).to.equal('04/09/2025 - 04/10/2025'); picker.displayFormat = 'yyyy-MM-dd'; @@ -264,6 +276,7 @@ describe('Date range picker - single input', () => { }; await elementUpdated(picker); + input = getInput(picker); expect(input.value).to.equal('04/09/2025 - 04/10/2025'); picker.clear(); @@ -288,6 +301,7 @@ describe('Date range picker - single input', () => { start, end, }); + input = getInput(picker); expect(input.value).to.equal('04/09/2025 - 04/10/2025'); expect(eventSpy).not.called; }); @@ -494,7 +508,7 @@ describe('Date range picker - single input', () => { dialog = picker.renderRoot.querySelector('igc-dialog'); expect(dialog?.hasAttribute('open')).to.equal(false); - input = picker.renderRoot.querySelector(IgcInputComponent.tagName)!; + input = getInput(picker); calendar = picker.renderRoot.querySelector( IgcCalendarComponent.tagName )!; @@ -553,6 +567,9 @@ describe('Date range picker - single input', () => { describe('Interactions with the inputs and the open and clear buttons', () => { it('should not open the picker when clicking the input in dropdown mode', async () => { + const input = picker.renderRoot.querySelector( + IgcDateRangeInputComponent.tagName + )!; simulateClick(input); await elementUpdated(picker); @@ -563,9 +580,13 @@ describe('Date range picker - single input', () => { picker.mode = 'dialog'; await elementUpdated(picker); - simulateClick(input.renderRoot.querySelector('input')!); + const rangeInput = picker.renderRoot.querySelector( + IgcDateRangeInputComponent.tagName + )!; + const input = rangeInput.renderRoot.querySelector('input')!; + input.focus(); + simulateClick(input); await elementUpdated(picker); - expect(picker.open).to.be.true; }); @@ -575,30 +596,236 @@ describe('Date range picker - single input', () => { picker.value = { start: today.native, end: tomorrow.native }; await elementUpdated(picker); - const input = picker.renderRoot!.querySelector( - IgcInputComponent.tagName - )!; + input = getInput(picker); input.focus(); await elementUpdated(input); + simulateClick(getIcon(picker, clearIcon)); await elementUpdated(picker); + input.blur(); await elementUpdated(input); expect(isFocused(input)).to.be.false; expect(eventSpy).to.be.calledWith('igcChange', { - detail: null, + detail: { start: null, end: null }, }); expect(picker.open).to.be.false; - expect(picker.value).to.deep.equal(null); + expect(picker.value).to.deep.equal({ start: null, end: null }); expect(input.value).to.equal(''); }); + it('should support date-range typing for single input', async () => { + const eventSpy = spy(picker, 'emitEvent'); + picker.useTwoInputs = false; + + await elementUpdated(picker); + + const expectedStart = CalendarDay.today.set({ + year: 2025, + month: 3, + date: 22, + }); + + const expectedEnd = expectedStart.add('day', 1); + input = getInput(picker); + input.focus(); + input.setSelectionRange(0, 1); + await elementUpdated(picker); + + let value = '04/22/2025 - 04/23/2025'; + simulateInput(input as unknown as HTMLInputElement, { + value, + inputType: 'insertText', + }); + input.setSelectionRange(value.length - 1, value.length); + await elementUpdated(picker); + + expect(eventSpy.lastCall).calledWith('igcInput', { + detail: { start: expectedStart.native, end: expectedEnd.native }, + }); + + eventSpy.resetHistory(); + picker.clear(); + await elementUpdated(picker); + + input.focus(); + input.setSelectionRange(0, 1); + await elementUpdated(picker); + + value = '04/22/202504/23/2025'; //not typing the separator characters + simulateInput(input as unknown as HTMLInputElement, { + value, + inputType: 'insertText', + }); + + input.setSelectionRange(value.length - 1, value.length); + await elementUpdated(picker); + + expect(eventSpy.lastCall).calledWith('igcInput', { + detail: { start: expectedStart.native, end: expectedEnd.native }, + }); + + eventSpy.resetHistory(); + input.blur(); + await elementUpdated(picker); + + expect(eventSpy).calledWith('igcChange', { + detail: { start: expectedStart.native, end: expectedEnd.native }, + }); + }); + + it('should in/decrement the different date parts with arrow up/down', async () => { + const expectedStart = CalendarDay.today.set({ + year: 2025, + month: 3, + date: 22, + }); + + const expectedEnd = expectedStart.add('day', 1); + const value = { start: expectedStart.native, end: expectedEnd.native }; + picker.useTwoInputs = false; + picker.value = value; + await elementUpdated(picker); + + input = getInput(picker); + input.focus(); + input.setSelectionRange(2, 2); + + expect(isFocused(input)).to.be.true; + //Move selection to the end of 'day' part. + simulateKeyboard(input, [ctrlKey, arrowRight]); + await elementUpdated(rangeInput); + + expect(input.selectionStart).to.equal(5); + expect(input.selectionEnd).to.equal(5); + + simulateKeyboard(input, arrowUp); + await elementUpdated(picker); + + expect(input.value).to.equal('04/23/2025 - 04/23/2025'); + + //Move selection to the end of 'year' part. + simulateKeyboard(input, [ctrlKey, arrowRight]); + await elementUpdated(rangeInput); + + expect(input.selectionStart).to.equal(10); + expect(input.selectionEnd).to.equal(10); + + simulateKeyboard(input, arrowUp); + await elementUpdated(picker); + + expect(input.value).to.equal('04/23/2026 - 04/23/2025'); + + //Move selection to the end of 'month' part of the end date. + // skips the separator parts when navigating (right direction) + simulateKeyboard(input, [ctrlKey, arrowRight]); + await elementUpdated(rangeInput); + + expect(input.selectionStart).to.equal(15); + expect(input.selectionEnd).to.equal(15); + + simulateKeyboard(input, arrowUp); + await elementUpdated(picker); + + expect(input.value).to.equal('04/23/2026 - 05/23/2025'); + + // skips the separator parts when navigating (left direction) + input.setSelectionRange(13, 13); // set selection to the start of the month part of the end date + + simulateKeyboard(input, [ctrlKey, arrowLeft]); + await elementUpdated(rangeInput); + + expect(input.selectionStart).to.equal(6); + expect(input.selectionEnd).to.equal(6); + + simulateKeyboard(input, arrowUp); + await elementUpdated(picker); + + expect(input.value).to.equal('04/23/2027 - 05/23/2025'); + }); + + it('should set the range to the current date (start-end) if no value and arrow up/down pressed', async () => { + picker.useTwoInputs = false; + picker.value = null; + await elementUpdated(picker); + + input = getInput(picker); + input.focus(); + input.setSelectionRange(0, 1); + + expect(isFocused(input)).to.be.true; + + simulateKeyboard(input, arrowUp); + await elementUpdated(picker); + checkSelectedRange( + picker, + + { start: today.native, end: today.native }, + + false + ); + }); + + it('should delete the value on pressing enter (single input)', async () => { + picker.useTwoInputs = false; + picker.value = null; + await elementUpdated(picker); + + input = getInput(picker); + input.focus(); + input.setSelectionRange(0, input.value.length); + + expect(isFocused(input)).to.be.true; + + simulateInput(input as unknown as HTMLInputElement, { + inputType: 'deleteContentBackward', + }); + await elementUpdated(picker); + + input.blur(); + await elementUpdated(rangeInput); + await elementUpdated(picker); + + expect(input.value).to.equal(''); + expect(picker.value).to.deep.equal(null); + }); + + it('should fill in missing date values (single input)', async () => { + const expectedStart = CalendarDay.today.set({ + year: 2025, + month: 3, + date: 22, + }); + + const expectedEnd = expectedStart.add('day', 1); + picker.useTwoInputs = false; + picker.value = { start: expectedStart.native, end: expectedEnd.native }; + + await elementUpdated(picker); + + input = getInput(picker); + input.focus(); + + // select start date + input.setSelectionRange(0, 10); + expect(isFocused(input)).to.be.true; + + // delete the year value + simulateKeyboard(input, 'Backspace'); + simulateInput(input as unknown as HTMLInputElement, { + inputType: 'deleteContentBackward', + }); + await elementUpdated(picker); + + input.blur(); + + await elementUpdated(picker); + expect(input.value).to.equal('01/01/2000 - 04/23/2025'); + }); it('should toggle the calendar with keyboard combinations and keep focus', async () => { const eventSpy = spy(picker, 'emitEvent'); - const input = picker.renderRoot!.querySelector( - IgcInputComponent.tagName - )!; + input = getInput(picker); input.focus(); expect(isFocused(input)).to.be.true; @@ -653,10 +880,11 @@ describe('Date range picker - single input', () => { expect(eventSpy).not.called; checkSelectedRange(picker, testValue, false); }); + it('should not open the calendar on clicking the input - dropdown mode', async () => { const eventSpy = spy(picker, 'emitEvent'); const igcInput = picker.renderRoot.querySelector( - IgcInputComponent.tagName + IgcDateRangeInputComponent.tagName )!; const nativeInput = igcInput.renderRoot.querySelector('input')!; simulateClick(nativeInput); @@ -670,7 +898,7 @@ describe('Date range picker - single input', () => { picker.mode = 'dialog'; await elementUpdated(picker); const igcInput = picker.renderRoot.querySelector( - IgcInputComponent.tagName + IgcDateRangeInputComponent.tagName )!; const nativeInput = igcInput.renderRoot.querySelector('input')!; diff --git a/src/components/date-range-picker/date-range-picker-two-inputs.form.spec.ts b/src/components/date-range-picker/date-range-picker-two-inputs.form.spec.ts index 259e56d0f..4c453d28e 100644 --- a/src/components/date-range-picker/date-range-picker-two-inputs.form.spec.ts +++ b/src/components/date-range-picker/date-range-picker-two-inputs.form.spec.ts @@ -13,7 +13,7 @@ import { ValidityHelpers, } from '../common/validity-helpers.spec.js'; import IgcDateTimeInputComponent from '../date-time-input/date-time-input.js'; -import IgcInputComponent from '../input/input.js'; +import IgcDateRangeInputComponent from './date-range-input.js'; import IgcDateRangePickerComponent, { type DateRangeValue, } from './date-range-picker.js'; @@ -24,7 +24,9 @@ import { } from './date-range-picker.utils.spec.js'; describe('Date Range Picker Two Inputs - Form integration', () => { - before(() => defineComponents(IgcDateRangePickerComponent)); + before(() => + defineComponents(IgcDateRangePickerComponent, IgcDateRangeInputComponent) + ); let picker: IgcDateRangePickerComponent; let dateTimeInputs: IgcDateTimeInputComponent[]; @@ -483,7 +485,7 @@ describe('Date Range Picker Two Inputs - Form integration', () => { await elementUpdated(spec.element); let singleInput = spec.element.renderRoot.querySelector( - IgcInputComponent.tagName + IgcDateRangeInputComponent.tagName )!; expect(singleInput.invalid).to.be.true; @@ -523,7 +525,7 @@ describe('Date Range Picker Two Inputs - Form integration', () => { await elementUpdated(spec.element); singleInput = spec.element.renderRoot.querySelector( - IgcInputComponent.tagName + IgcDateRangeInputComponent.tagName )!; expect(singleInput.invalid).to.be.true; }); diff --git a/src/components/date-range-picker/date-range-picker-two-inputs.spec.ts b/src/components/date-range-picker/date-range-picker-two-inputs.spec.ts index 9415c83c4..5791e15fd 100644 --- a/src/components/date-range-picker/date-range-picker-two-inputs.spec.ts +++ b/src/components/date-range-picker/date-range-picker-two-inputs.spec.ts @@ -24,6 +24,7 @@ import { } from '../common/utils.spec.js'; import IgcDateTimeInputComponent from '../date-time-input/date-time-input.js'; import type IgcDialogComponent from '../dialog/dialog.js'; +import IgcDateRangeInputComponent from './date-range-input.js'; import IgcDateRangePickerComponent from './date-range-picker.js'; import { checkSelectedRange, @@ -32,7 +33,9 @@ import { } from './date-range-picker.utils.spec.js'; describe('Date range picker - two inputs', () => { - before(() => defineComponents(IgcDateRangePickerComponent)); + before(() => + defineComponents(IgcDateRangePickerComponent, IgcDateRangeInputComponent) + ); let picker: IgcDateRangePickerComponent; let dateTimeInputs: Array; diff --git a/src/components/date-range-picker/date-range-picker.common.spec.ts b/src/components/date-range-picker/date-range-picker.common.spec.ts index a6c11f51b..6b603982d 100644 --- a/src/components/date-range-picker/date-range-picker.common.spec.ts +++ b/src/components/date-range-picker/date-range-picker.common.spec.ts @@ -21,6 +21,7 @@ import { } from '../common/utils.spec.js'; import IgcDialogComponent from '../dialog/dialog.js'; import IgcPopoverComponent from '../popover/popover.js'; +import IgcDateRangeInputComponent from './date-range-input.js'; import IgcDateRangePickerComponent, { type CustomDateRange, type DateRangeValue, @@ -33,7 +34,9 @@ import { import IgcPredefinedRangesAreaComponent from './predefined-ranges-area.js'; describe('Date range picker - common tests for single and two inputs mode', () => { - before(() => defineComponents(IgcDateRangePickerComponent)); + before(() => + defineComponents(IgcDateRangePickerComponent, IgcDateRangeInputComponent) + ); let picker: IgcDateRangePickerComponent; let calendar: IgcCalendarComponent; diff --git a/src/components/date-range-picker/date-range-picker.ts b/src/components/date-range-picker/date-range-picker.ts index 1be401732..b246cb53c 100644 --- a/src/components/date-range-picker/date-range-picker.ts +++ b/src/components/date-range-picker/date-range-picker.ts @@ -4,7 +4,6 @@ import { query, queryAll, queryAssignedElements, - state, } from 'lit/decorators.js'; import { cache } from 'lit/directives/cache.js'; import { ifDefined } from 'lit/directives/if-defined.js'; @@ -47,14 +46,17 @@ import { isEmpty, } from '../common/util.js'; import IgcDateTimeInputComponent from '../date-time-input/date-time-input.js'; -import { DateTimeUtil } from '../date-time-input/date-util.js'; +import { + DateRangePosition, + DateTimeUtil, +} from '../date-time-input/date-util.js'; import IgcDialogComponent from '../dialog/dialog.js'; import IgcFocusTrapComponent from '../focus-trap/focus-trap.js'; import IgcIconComponent from '../icon/icon.js'; -import IgcInputComponent from '../input/input.js'; import IgcPopoverComponent from '../popover/popover.js'; import type { ContentOrientation, PickerMode } from '../types.js'; import IgcValidationContainerComponent from '../validation-container/validation-container.js'; +import IgcDateRangeInputComponent from './date-range-input.js'; import { styles } from './date-range-picker.base.css.js'; import IgcPredefinedRangesAreaComponent from './predefined-ranges-area.js'; import { styles as shared } from './themes/shared/date-range-picker.common.css.js'; @@ -192,9 +194,9 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM public static register(): void { registerComponent( IgcDateRangePickerComponent, + IgcDateRangeInputComponent, IgcCalendarComponent, IgcDateTimeInputComponent, - IgcInputComponent, IgcFocusTrapComponent, IgcIconComponent, IgcPopoverComponent, @@ -248,14 +250,11 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM return this.value?.start ?? this.value?.end ?? null; } - @state() - private _maskedRangeValue = ''; - @queryAll(IgcDateTimeInputComponent.tagName) private readonly _inputs!: IgcDateTimeInputComponent[]; - @query(IgcInputComponent.tagName) - private readonly _input!: IgcInputComponent; + @query(IgcDateRangeInputComponent.tagName) + private readonly _input!: IgcDateRangeInputComponent; @query(IgcCalendarComponent.tagName) private readonly _calendar!: IgcCalendarComponent; @@ -292,7 +291,6 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM public set value(value: DateRangeValue | string | null | undefined) { this._formValue.setValueAndFormState(convertToDateRange(value)); this._setCalendarRangeValues(); - this._updateMaskedRangeValue(); } public get value(): DateRangeValue | null { @@ -428,7 +426,6 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM @property({ attribute: 'display-format' }) public set displayFormat(value: string) { this._displayFormat = value; - this._updateMaskedRangeValue(); } public get displayFormat(): string { @@ -443,7 +440,6 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM @property({ attribute: 'input-format' }) public set inputFormat(value: string) { this._inputFormat = value; - this._updateMaskedRangeValue(); } public get inputFormat(): string { @@ -599,7 +595,6 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM protected override formResetCallback() { super.formResetCallback(); this._setCalendarRangeValues(); - this._updateMaskedRangeValue(); } // #endregion @@ -613,6 +608,9 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM if (this.useTwoInputs) { this._inputs[0]?.clear(); this._inputs[1]?.clear(); + } else { + this._input.value = null; + this._input?.clear(); } } @@ -637,13 +635,11 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM @watch('locale') protected _updateDefaultMask(): void { this._defaultMask = DateTimeUtil.getDefaultMask(this.locale); - this._updateMaskedRangeValue(); } @watch('useTwoInputs') protected async _updateDateRange() { await this._calendar?.updateComplete; - this._updateMaskedRangeValue(); this._setCalendarRangeValues(); this._delegateInputsValidity(); } @@ -693,8 +689,6 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM protected _handleInputEvent(event: CustomEvent) { event.stopPropagation(); - this._setTouchedState(); - if (this.nonEditable) { event.preventDefault(); return; @@ -711,13 +705,46 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM protected _handleInputChangeEvent(event: CustomEvent) { event.stopPropagation(); - this._setTouchedState(); const input = event.target as IgcDateTimeInputComponent; const newValue = input.value ? CalendarDay.from(input.value).native : null; const updatedRange = this._getUpdatedDateRange(input, newValue); - const { start, end } = this._swapDates(updatedRange); + const { start, end } = this._swapDates(updatedRange) ?? { + start: null, + end: null, + }; + + this._setCalendarRangeValues(); + this.value = { start, end }; + this.emitEvent('igcChange', { detail: this.value }); + } + + protected async _handleDateRangeInputEvent(event: CustomEvent) { + event.stopPropagation(); + if (this.nonEditable) { + event.preventDefault(); + return; + } + const input = event.target as IgcDateRangeInputComponent; + const newValue = input.value; + + this.value = newValue; + this._calendar.activeDate = newValue?.start; + + this.emitEvent('igcInput', { detail: this.value }); + } + + protected _handleDateRangeInputChangeEvent(event: CustomEvent) { + event.stopPropagation(); + + const input = event.target as IgcDateRangeInputComponent; + const newValue = input.value!; + + const { start, end } = this._swapDates(newValue) ?? { + start: null, + end: null, + }; this._setCalendarRangeValues(); this.value = { start, end }; @@ -727,12 +754,6 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM protected _handleFocusOut({ relatedTarget }: FocusEvent) { if (!this.contains(relatedTarget as Node)) { this._handleBlur(); - - const isSameValue = equal(this.value, this._oldValue); - if (!(this.useTwoInputs || this.readOnly || isSameValue)) { - this.emitEvent('igcChange', { detail: this.value }); - this._oldValue = this.value; - } } } @@ -857,29 +878,6 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM this._dateConstraints = isEmpty(dates) ? [] : dates; } - private _updateMaskedRangeValue() { - if (this.useTwoInputs) { - return; - } - - if (!isCompleteDateRange(this.value)) { - this._maskedRangeValue = ''; - return; - } - - const { formatDate, predefinedToDateDisplayFormat } = DateTimeUtil; - - const { start, end } = this.value; - const format = - predefinedToDateDisplayFormat(this._displayFormat) ?? - this._displayFormat ?? - this.inputFormat; - - this._maskedRangeValue = format - ? `${formatDate(start, this.locale, format)} - ${formatDate(end, this.locale, format)}` - : `${start.toLocaleDateString()} - ${end.toLocaleDateString()}`; - } - private _setCalendarRangeValues() { if (!this._calendar) { return; @@ -932,7 +930,7 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM // #region Rendering - private _renderClearIcon(picker: DateRangePickerInput = 'start') { + private _renderClearIcon(picker = DateRangePosition.Start) { const clearIcon = this.useTwoInputs ? `clear-icon-${picker}` : 'clear-icon'; return this._firstDefinedInRange ? html` @@ -953,7 +951,7 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM : nothing; } - private _renderCalendarIcon(picker: DateRangePickerInput = 'start') { + private _renderCalendarIcon(picker = DateRangePosition.Start) { const defaultIcon = html` `; @@ -1095,20 +1093,28 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM return IgcValidationContainerComponent.create(this); } - protected _renderInput(id: string, picker: DateRangePickerInput = 'start') { + protected _renderInput(id: string, picker = DateRangePosition.Start) { const readOnly = !this._isDropDown || this.readOnly || this.nonEditable; const placeholder = - picker === 'start' ? this.placeholderStart : this.placeholderEnd; - const label = picker === 'start' ? this.labelStart : this.labelEnd; + picker === DateRangePosition.Start + ? this.placeholderStart + : this.placeholderEnd; + const label = + picker === DateRangePosition.Start ? this.labelStart : this.labelEnd; const format = DateTimeUtil.predefinedToDateDisplayFormat( this._displayFormat! ); - const value = picker === 'start' ? this.value?.start : this.value?.end; + const value = + picker === DateRangePosition.Start ? this.value?.start : this.value?.end; const prefixes = - picker === 'start' ? this._startPrefixes : this._endPrefixes; + picker === DateRangePosition.Start + ? this._startPrefixes + : this._endPrefixes; const suffixes = - picker === 'start' ? this._startSuffixes : this._endSuffixes; + picker === DateRangePosition.Start + ? this._startSuffixes + : this._endSuffixes; const prefix = isEmpty(prefixes) ? undefined : 'prefix'; const suffix = isEmpty(suffixes) ? undefined : 'suffix'; @@ -1147,32 +1153,40 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM private _renderInputs(idStart: string, idEnd: string) { return html`
- ${this._renderInput(idStart, 'start')} + ${this._renderInput(idStart, DateRangePosition.Start)}
${this.resourceStrings.separator}
- ${this._renderInput(idEnd, 'end')} + ${this._renderInput(idEnd, DateRangePosition.End)}
${this._renderPicker(idStart)} ${this._renderHelperText()} `; } private _renderSingleInput(id: string) { + const readOnly = !this._isDropDown || this.readOnly || this.nonEditable; + const format = DateTimeUtil.predefinedToDateDisplayFormat( + this._displayFormat! + )!; const prefix = isEmpty(this._prefixes) ? undefined : 'prefix'; const suffix = isEmpty(this._suffixes) ? undefined : 'suffix'; return html` - ${this._renderClearIcon()} - + ${this._renderHelperText()} ${this._renderPicker(id)} `; } @@ -1205,5 +1219,3 @@ declare global { 'igc-date-range-picker': IgcDateRangePickerComponent; } } - -type DateRangePickerInput = 'start' | 'end'; diff --git a/src/components/date-range-picker/date-range-picker.utils.spec.ts b/src/components/date-range-picker/date-range-picker.utils.spec.ts index e0dbed229..de3c79d29 100644 --- a/src/components/date-range-picker/date-range-picker.utils.spec.ts +++ b/src/components/date-range-picker/date-range-picker.utils.spec.ts @@ -6,7 +6,7 @@ import { equal } from '../common/util.js'; import { checkDatesEqual, simulateClick } from '../common/utils.spec.js'; import IgcDateTimeInputComponent from '../date-time-input/date-time-input.js'; import { DateTimeUtil } from '../date-time-input/date-util.js'; -import IgcInputComponent from '../input/input.js'; +import IgcDateRangeInputComponent from './date-range-input.js'; import type IgcDateRangePickerComponent from './date-range-picker.js'; import type { DateRangeValue } from './date-range-picker.js'; @@ -50,7 +50,7 @@ export const checkSelectedRange = ( checkDatesEqual(inputs[1].value!, expectedValue.end); } } else { - const input = picker.renderRoot.querySelector(IgcInputComponent.tagName)!; + const input = getInput(picker); const start = expectedValue?.start ? DateTimeUtil.formatDate( expectedValue.start, @@ -96,3 +96,13 @@ export const checkInputsInvalidState = async ( expect(inputs[0].invalid).to.equal(first); expect(inputs[1].invalid).to.equal(second); }; + +export const getInput = ( + picker: IgcDateRangePickerComponent +): HTMLInputElement => { + const rangeInput = picker.renderRoot.querySelector( + IgcDateRangeInputComponent.tagName + )! as IgcDateRangeInputComponent; + const input = rangeInput.renderRoot.querySelector('input')!; + return input; +}; diff --git a/src/components/date-time-input/date-time-input.base.ts b/src/components/date-time-input/date-time-input.base.ts new file mode 100644 index 000000000..37c6c6d2c --- /dev/null +++ b/src/components/date-time-input/date-time-input.base.ts @@ -0,0 +1,393 @@ +import { html } from 'lit'; +import { eventOptions, property } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { live } from 'lit/directives/live.js'; +import { convertToDate } from '../calendar/helpers.js'; +import { + addKeybindings, + arrowDown, + arrowLeft, + arrowRight, + arrowUp, + ctrlKey, +} from '../common/controllers/key-bindings.js'; +import { watch } from '../common/decorators/watch.js'; +import type { AbstractConstructor } from '../common/mixins/constructor.js'; +import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; +import { partMap } from '../common/part-map.js'; +import type { DateRangeValue } from '../date-range-picker/date-range-picker.js'; +import type { IgcInputComponentEventMap } from '../input/input-base.js'; +import { + IgcMaskInputBaseComponent, + type MaskRange, +} from '../mask-input/mask-input-base.js'; +import type { + DatePart, + DatePartInfo, + DateRangePart, + DateRangePartInfo, +} from './date-util.js'; +import { type DatePartDeltas, DateParts, DateTimeUtil } from './date-util.js'; +import { dateTimeInputValidators } from './validators.js'; + +export interface IgcDateTimeInputComponentEventMap + extends Omit { + igcChange: CustomEvent; +} +export abstract class IgcDateTimeInputBaseComponent< + TValue extends Date | DateRangeValue | string | null, + TPart extends DatePart | DateRangePart, + TPartInfo extends DatePartInfo | DateRangePartInfo, +> extends EventEmitterMixin< + IgcDateTimeInputComponentEventMap, + AbstractConstructor +>(IgcMaskInputBaseComponent) { + // #region Internal state & properties + + protected override get __validators() { + return dateTimeInputValidators; + } + private _min: Date | null = null; + private _max: Date | null = null; + protected _defaultMask!: string; + protected _oldValue: TValue | null = null; + protected _inputDateParts!: TPartInfo[]; + protected _inputFormat!: string; + + protected abstract _datePartDeltas: DatePartDeltas; + protected abstract get targetDatePart(): TPart | undefined; + + protected get hasDateParts(): boolean { + const parts = + this._inputDateParts || + DateTimeUtil.parseDateTimeFormat(this.inputFormat); + + return parts.some( + (p) => + p.type === DateParts.Date || + p.type === DateParts.Month || + p.type === DateParts.Year + ); + } + + protected get hasTimeParts(): boolean { + const parts = + this._inputDateParts || + DateTimeUtil.parseDateTimeFormat(this.inputFormat); + return parts.some( + (p) => + p.type === DateParts.Hours || + p.type === DateParts.Minutes || + p.type === DateParts.Seconds + ); + } + + protected get datePartDeltas(): DatePartDeltas { + return Object.assign({}, this._datePartDeltas, this.spinDelta); + } + + // #endregion + + // #region Public properties + + public abstract override value: TValue | null; + + /** + * The date format to apply on the input. + * @attr input-format + */ + @property({ attribute: 'input-format' }) + public get inputFormat(): string { + return this._inputFormat || this._defaultMask; + } + + public set inputFormat(val: string) { + if (val) { + this.setMask(val); + this._inputFormat = val; + if (this.value) { + this.updateMask(); + } + } + } + + /** + * The minimum value required for the input to remain valid. + * @attr + */ + @property({ converter: convertToDate }) + public set min(value: Date | string | null | undefined) { + this._min = convertToDate(value); + this._validate(); + } + + public get min(): Date | null { + return this._min; + } + + /** + * The maximum value required for the input to remain valid. + * @attr + */ + @property({ converter: convertToDate }) + public set max(value: Date | string | null | undefined) { + this._max = convertToDate(value); + this._validate(); + } + + public get max(): Date | null { + return this._max; + } + + /** + * Format to display the value in when not editing. + * Defaults to the input format if not set. + * @attr display-format + */ + @property({ attribute: 'display-format' }) + public displayFormat!: string; + + /** + * Delta values used to increment or decrement each date part on step actions. + * All values default to `1`. + */ + @property({ attribute: false }) + public spinDelta!: DatePartDeltas; + + /** + * Sets whether to loop over the currently spun segment. + * @attr spin-loop + */ + @property({ type: Boolean, attribute: 'spin-loop' }) + public spinLoop = true; + + /** + * The locale settings used to display the value. + * @attr + */ + @property() + public locale = 'en'; + + // #endregion + + // #region Lifecycle & observers + + constructor() { + super(); + + addKeybindings(this, { + skip: () => this.readOnly, + bindingDefaults: { triggers: ['keydownRepeat'] }, + }) + .set([ctrlKey, ';'], this.setToday) + .set(arrowUp, this.keyboardSpin.bind(this, 'up')) + .set(arrowDown, this.keyboardSpin.bind(this, 'down')) + .set([ctrlKey, arrowLeft], this.navigateParts.bind(this, 0)) + .set([ctrlKey, arrowRight], this.navigateParts.bind(this, 1)); + } + + public override connectedCallback() { + super.connectedCallback(); + this.updateDefaultMask(); + this.setMask(this.inputFormat); + this._validate(); + if (this.value) { + this.updateMask(); + } + } + + @watch('locale', { waitUntilFirstUpdate: true }) + protected _setDefaultMask(): void { + if (!this._inputFormat) { + this.updateDefaultMask(); + this.setMask(this._defaultMask); + } + + if (this.value) { + this.updateMask(); + } + } + + @watch('displayFormat', { waitUntilFirstUpdate: true }) + protected _setDisplayFormat(): void { + if (this.value) { + this.updateMask(); + } + } + + @watch('prompt', { waitUntilFirstUpdate: true }) + protected _promptChange(): void { + if (!this.prompt) { + this.prompt = this.parser.prompt; + } else { + this.parser.prompt = this.prompt; + } + } + + // #endregion + + // #region Methods + + /** Increments a date/time portion. */ + public stepUp(datePart?: TPart, delta?: number): void { + const targetPart = datePart || this.targetDatePart; + + if (!targetPart) { + return; + } + + const { start, end } = this.inputSelection; + const newValue = this.trySpinValue(targetPart, delta); + this.value = newValue as TValue; + this.updateComplete.then(() => this.input.setSelectionRange(start, end)); + } + + /** Decrements a date/time portion. */ + public stepDown(datePart?: TPart, delta?: number): void { + const targetPart = datePart || this.targetDatePart; + + if (!targetPart) { + return; + } + + const { start, end } = this.inputSelection; + const newValue = this.trySpinValue(targetPart, delta, true); + this.value = newValue; + this.updateComplete.then(() => this.input.setSelectionRange(start, end)); + } + + /** Clears the input element of user input. */ + public clear(): void { + this.maskedValue = ''; + this.value = null; + } + + protected setToday() { + this.value = new Date() as TValue; + this.handleInput(); + } + + protected handleDragLeave() { + if (!this.focused) { + this.updateMask(); + } + } + + protected handleDragEnter() { + if (!this.focused) { + this.maskedValue = this.getMaskedValue(); + } + } + + protected async updateInput(string: string, range: MaskRange) { + const { value, end } = this.parser.replace( + this.maskedValue, + string, + range.start, + range.end + ); + + this.maskedValue = value; + + this.updateValue(); + this.requestUpdate(); + + if (range.start !== this.inputFormat.length) { + this.handleInput(); + } + await this.updateComplete; + this.input.setSelectionRange(end, end); + } + + protected trySpinValue( + datePart: TPart, + delta?: number, + negative = false + ): TValue { + // default to 1 if a delta is set to 0 or any other falsy value + const _delta = + delta || this.datePartDeltas[datePart as keyof DatePartDeltas] || 1; + + const spinValue = negative ? -Math.abs(_delta) : Math.abs(_delta); + return this.spinValue(datePart, spinValue); + } + + protected isComplete(): boolean { + return !this.maskedValue.includes(this.prompt); + } + + protected override _updateSetRangeTextValue() { + this.updateValue(); + } + + protected navigateParts(delta: number) { + const position = this.getNewPosition(this.input.value, delta); + this.setSelectionRange(position, position); + } + + protected async keyboardSpin(direction: 'up' | 'down') { + direction === 'up' ? this.stepUp() : this.stepDown(); + this.handleInput(); + await this.updateComplete; + this.setSelectionRange(this.selection.start, this.selection.end); + } + + @eventOptions({ passive: false }) + private async onWheel(event: WheelEvent) { + if (!this.focused || this.readOnly) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + const { start, end } = this.inputSelection; + event.deltaY > 0 ? this.stepDown() : this.stepUp(); + this.handleInput(); + + await this.updateComplete; + this.setSelectionRange(start, end); + } + + protected updateDefaultMask(): void { + this._defaultMask = DateTimeUtil.getDefaultMask(this.locale); + } + + protected override renderInput() { + return html` + + `; + } + + protected abstract override handleInput(): void; + protected abstract updateMask(): void; + protected abstract updateValue(): void; + protected abstract getNewPosition(value: string, direction: number): number; + protected abstract spinValue(datePart: TPart, delta: number): TValue; + protected abstract setMask(string: string): void; + protected abstract getMaskedValue(): string; + protected abstract handleBlur(): void; + protected abstract handleFocus(): Promise; + + // #endregion +} diff --git a/src/components/date-time-input/date-time-input.ts b/src/components/date-time-input/date-time-input.ts index 43d7e5572..a3f98faf5 100644 --- a/src/components/date-time-input/date-time-input.ts +++ b/src/components/date-time-input/date-time-input.ts @@ -1,30 +1,10 @@ -import { html } from 'lit'; -import { eventOptions, property } from 'lit/decorators.js'; -import { ifDefined } from 'lit/directives/if-defined.js'; -import { live } from 'lit/directives/live.js'; - +import { property } from 'lit/decorators.js'; import { convertToDate } from '../calendar/helpers.js'; -import { - addKeybindings, - arrowDown, - arrowLeft, - arrowRight, - arrowUp, - ctrlKey, -} from '../common/controllers/key-bindings.js'; -import { watch } from '../common/decorators/watch.js'; import { registerComponent } from '../common/definitions/register.js'; -import type { AbstractConstructor } from '../common/mixins/constructor.js'; -import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; import { FormValueDateTimeTransformers } from '../common/mixins/forms/form-transformers.js'; import { createFormValueState } from '../common/mixins/forms/form-value.js'; -import { partMap } from '../common/part-map.js'; -import type { IgcInputComponentEventMap } from '../input/input-base.js'; -import { - IgcMaskInputBaseComponent, - type MaskRange, -} from '../mask-input/mask-input-base.js'; import IgcValidationContainerComponent from '../validation-container/validation-container.js'; +import { IgcDateTimeInputBaseComponent } from './date-time-input.base.js'; import { DatePart, type DatePartDeltas, @@ -32,12 +12,6 @@ import { DateParts, DateTimeUtil, } from './date-util.js'; -import { dateTimeInputValidators } from './validators.js'; - -export interface IgcDateTimeInputComponentEventMap - extends Omit { - igcChange: CustomEvent; -} /** * A date time input is an input field that lets you set and edit the date and time in a chosen input element @@ -64,10 +38,11 @@ export interface IgcDateTimeInputComponentEventMap * @csspart suffix - The suffix wrapper. * @csspart helper-text - The helper text wrapper. */ -export default class IgcDateTimeInputComponent extends EventEmitterMixin< - IgcDateTimeInputComponentEventMap, - AbstractConstructor ->(IgcMaskInputBaseComponent) { +export default class IgcDateTimeInputComponent extends IgcDateTimeInputBaseComponent< + Date | null, + DatePart, + DatePartInfo +> { public static readonly tagName = 'igc-date-time-input'; /* blazorSuppress */ @@ -78,23 +53,12 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< ); } - protected override get __validators() { - return dateTimeInputValidators; - } - protected override readonly _formValue = createFormValueState(this, { initialValue: null, transformers: FormValueDateTimeTransformers, }); - protected _defaultMask!: string; - private _oldValue: Date | null = null; - private _min: Date | null = null; - private _max: Date | null = null; - - private _inputDateParts!: DatePartInfo[]; - private _inputFormat!: string; - private _datePartDeltas: DatePartDeltas = { + protected override _datePartDeltas: DatePartDeltas = { date: 1, month: 1, year: 1, @@ -103,25 +67,6 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< seconds: 1, }; - /** - * The date format to apply on the input. - * @attr input-format - */ - @property({ attribute: 'input-format' }) - public get inputFormat(): string { - return this._inputFormat || this._defaultMask; - } - - public set inputFormat(val: string) { - if (val) { - this.setMask(val); - this._inputFormat = val; - if (this.value) { - this.updateMask(); - } - } - } - public get value(): Date | null { return this._formValue.value; } @@ -137,117 +82,7 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< this.updateMask(); } - /** - * The minimum value required for the input to remain valid. - * @attr - */ - @property({ converter: convertToDate }) - public set min(value: Date | string | null | undefined) { - this._min = convertToDate(value); - this._validate(); - } - - public get min(): Date | null { - return this._min; - } - - /** - * The maximum value required for the input to remain valid. - * @attr - */ - @property({ converter: convertToDate }) - public set max(value: Date | string | null | undefined) { - this._max = convertToDate(value); - this._validate(); - } - - public get max(): Date | null { - return this._max; - } - - /** - * Format to display the value in when not editing. - * Defaults to the input format if not set. - * @attr display-format - */ - @property({ attribute: 'display-format' }) - public displayFormat!: string; - - /** - * Delta values used to increment or decrement each date part on step actions. - * All values default to `1`. - */ - @property({ attribute: false }) - public spinDelta!: DatePartDeltas; - - /** - * Sets whether to loop over the currently spun segment. - * @attr spin-loop - */ - @property({ type: Boolean, attribute: 'spin-loop' }) - public spinLoop = true; - - /** - * The locale settings used to display the value. - * @attr - */ - @property() - public locale = 'en'; - - @watch('locale', { waitUntilFirstUpdate: true }) - protected setDefaultMask(): void { - if (!this._inputFormat) { - this.updateDefaultMask(); - this.setMask(this._defaultMask); - } - - if (this.value) { - this.updateMask(); - } - } - - @watch('displayFormat', { waitUntilFirstUpdate: true }) - protected setDisplayFormat(): void { - if (this.value) { - this.updateMask(); - } - } - - @watch('prompt', { waitUntilFirstUpdate: true }) - protected promptChange(): void { - if (!this.prompt) { - this.prompt = this.parser.prompt; - } else { - this.parser.prompt = this.prompt; - } - } - - protected get hasDateParts(): boolean { - const parts = - this._inputDateParts || - DateTimeUtil.parseDateTimeFormat(this.inputFormat); - - return parts.some( - (p) => - p.type === DateParts.Date || - p.type === DateParts.Month || - p.type === DateParts.Year - ); - } - - protected get hasTimeParts(): boolean { - const parts = - this._inputDateParts || - DateTimeUtil.parseDateTimeFormat(this.inputFormat); - return parts.some( - (p) => - p.type === DateParts.Hours || - p.type === DateParts.Minutes || - p.type === DateParts.Seconds - ); - } - - private get targetDatePart(): DatePart | undefined { + protected override get targetDatePart(): DatePart | undefined { let result: DatePart | undefined; if (this.focused) { @@ -272,72 +107,6 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< return result; } - private get datePartDeltas(): DatePartDeltas { - return Object.assign({}, this._datePartDeltas, this.spinDelta); - } - - constructor() { - super(); - - addKeybindings(this, { - skip: () => this.readOnly, - bindingDefaults: { triggers: ['keydownRepeat'] }, - }) - .set([ctrlKey, ';'], this.setToday) - .set(arrowUp, this.keyboardSpin.bind(this, 'up')) - .set(arrowDown, this.keyboardSpin.bind(this, 'down')) - .set([ctrlKey, arrowLeft], this.navigateParts.bind(this, 0)) - .set([ctrlKey, arrowRight], this.navigateParts.bind(this, 1)); - } - - public override connectedCallback() { - super.connectedCallback(); - this.updateDefaultMask(); - this.setMask(this.inputFormat); - if (this.value) { - this.updateMask(); - } - } - - /** Increments a date/time portion. */ - public stepUp(datePart?: DatePart, delta?: number): void { - const targetPart = datePart || this.targetDatePart; - - if (!targetPart) { - return; - } - - const { start, end } = this.inputSelection; - const newValue = this.trySpinValue(targetPart, delta); - this.value = newValue; - this.updateComplete.then(() => this.input.setSelectionRange(start, end)); - } - - /** Decrements a date/time portion. */ - public stepDown(datePart?: DatePart, delta?: number): void { - const targetPart = datePart || this.targetDatePart; - - if (!targetPart) { - return; - } - - const { start, end } = this.inputSelection; - const newValue = this.trySpinValue(targetPart, delta, true); - this.value = newValue; - this.updateComplete.then(() => this.input.setSelectionRange(start, end)); - } - - /** Clears the input element of user input. */ - public clear(): void { - this.maskedValue = ''; - this.value = null; - } - - protected setToday() { - this.value = new Date(); - this.handleInput(); - } - protected updateMask() { if (this.focused) { this.maskedValue = this.getMaskedValue(); @@ -373,52 +142,7 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< this.emitEvent('igcInput', { detail: this.value?.toString() }); } - protected handleDragLeave() { - if (!this.focused) { - this.updateMask(); - } - } - - protected handleDragEnter() { - if (!this.focused) { - this.maskedValue = this.getMaskedValue(); - } - } - - protected async updateInput(string: string, range: MaskRange) { - const { value, end } = this.parser.replace( - this.maskedValue, - string, - range.start, - range.end - ); - - this.maskedValue = value; - - this.updateValue(); - this.requestUpdate(); - - if (range.start !== this.inputFormat.length) { - this.handleInput(); - } - await this.updateComplete; - this.input.setSelectionRange(end, end); - } - - private trySpinValue( - datePart: DatePart, - delta?: number, - negative = false - ): Date { - // default to 1 if a delta is set to 0 or any other falsy value - const _delta = - delta || this.datePartDeltas[datePart as keyof DatePartDeltas] || 1; - - const spinValue = negative ? -Math.abs(_delta) : Math.abs(_delta); - return this.spinValue(datePart, spinValue); - } - - private spinValue(datePart: DatePart, delta: number): Date { + protected override spinValue(datePart: DatePart, delta: number): Date { if (!(this.value && DateTimeUtil.isValidDate(this.value))) { return new Date(); } @@ -463,28 +187,26 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< return newDate; } - @eventOptions({ passive: false }) - private async onWheel(event: WheelEvent) { - if (!this.focused || this.readOnly) { + protected override async handleFocus() { + this.focused = true; + + if (this.readOnly) { return; } - event.preventDefault(); - event.stopPropagation(); - - const { start, end } = this.inputSelection; - event.deltaY > 0 ? this.stepDown() : this.stepUp(); - this.handleInput(); - - await this.updateComplete; - this.setSelectionRange(start, end); - } + this._oldValue = this.value; + const areFormatsDifferent = this.displayFormat !== this.inputFormat; - private updateDefaultMask(): void { - this._defaultMask = DateTimeUtil.getDefaultMask(this.locale); + if (!this.value) { + this.maskedValue = this.emptyMask; + await this.updateComplete; + this.select(); + } else if (areFormatsDifferent) { + this.updateMask(); + } } - private setMask(string: string): void { + protected setMask(string: string): void { const oldFormat = this._inputDateParts?.map((p) => p.format).join(''); this._inputDateParts = DateTimeUtil.parseDateTimeFormat(string); const value = this._inputDateParts.map((p) => p.format).join(''); @@ -508,13 +230,13 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< } } - private parseDate(val: string) { + private _parseDate(val: string) { return val ? DateTimeUtil.parseValueFromMask(val, this._inputDateParts, this.prompt) : null; } - private getMaskedValue(): string { + protected getMaskedValue(): string { let mask = this.emptyMask; if (DateTimeUtil.isValidDate(this.value)) { @@ -542,24 +264,16 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< return this.maskedValue === '' ? mask : this.maskedValue; } - private isComplete(): boolean { - return !this.maskedValue.includes(this.prompt); - } - - private updateValue(): void { + protected updateValue(): void { if (this.isComplete()) { - const parsedDate = this.parseDate(this.maskedValue); + const parsedDate = this._parseDate(this.maskedValue); this.value = DateTimeUtil.isValidDate(parsedDate) ? parsedDate : null; } else { this.value = null; } } - protected override _updateSetRangeTextValue() { - this.updateValue(); - } - - private getNewPosition(value: string, direction = 0): number { + protected getNewPosition(value: string, direction = 0): number { const cursorPos = this.selection.start; if (!direction) { @@ -577,32 +291,13 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< return part?.start ?? value.length; } - protected async handleFocus() { - this.focused = true; - - if (this.readOnly) { - return; - } - - this._oldValue = this.value; - const areFormatsDifferent = this.displayFormat !== this.inputFormat; - - if (!this.value) { - this.maskedValue = this.emptyMask; - await this.updateComplete; - this.select(); - } else if (areFormatsDifferent) { - this.updateMask(); - } - } - - protected handleBlur() { + protected override handleBlur() { const isEmptyMask = this.maskedValue === this.emptyMask; this.focused = false; if (!(this.isComplete() || isEmptyMask)) { - const parse = this.parseDate(this.maskedValue); + const parse = this._parseDate(this.maskedValue); if (parse) { this.value = parse; @@ -622,44 +317,6 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< super._handleBlur(); } - - protected navigateParts(delta: number) { - const position = this.getNewPosition(this.input.value, delta); - this.setSelectionRange(position, position); - } - - protected async keyboardSpin(direction: 'up' | 'down') { - direction === 'up' ? this.stepUp() : this.stepDown(); - this.handleInput(); - await this.updateComplete; - this.setSelectionRange(this.selection.start, this.selection.end); - } - - protected override renderInput() { - return html` - - `; - } } declare global { diff --git a/src/components/date-time-input/date-util.ts b/src/components/date-time-input/date-util.ts index 53181570e..2e107eb31 100644 --- a/src/components/date-time-input/date-util.ts +++ b/src/components/date-time-input/date-util.ts @@ -36,6 +36,24 @@ export interface DatePartInfo { format: string; } +/** @ignore */ +export enum DateRangePosition { + Start = 'start', + End = 'end', + Separator = 'separator', +} + +/** @ignore */ +export interface DateRangePart { + part: DatePart; + position: DateRangePosition; +} + +/** @ignore */ +export interface DateRangePartInfo extends DatePartInfo { + position?: DateRangePosition; +} + export interface DatePartDeltas { date?: number; month?: number; diff --git a/src/components/input/input-base.ts b/src/components/input/input-base.ts index 8e6357054..0b7655f2e 100644 --- a/src/components/input/input-base.ts +++ b/src/components/input/input-base.ts @@ -9,6 +9,7 @@ import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; import { FormAssociatedRequiredMixin } from '../common/mixins/forms/associated-required.js'; import { partMap } from '../common/part-map.js'; import { createCounter } from '../common/util.js'; +import type { DateRangeValue } from '../date-range-picker/date-range-picker.js'; import type { RangeTextSelectMode, SelectionRangeDirection } from '../types.js'; import IgcValidationContainerComponent from '../validation-container/validation-container.js'; import { styles } from './themes/input.base.css.js'; @@ -43,7 +44,7 @@ export abstract class IgcInputBaseComponent extends FormAssociatedRequiredMixin( /* blazorSuppress */ /** The value attribute of the control. */ - public abstract value: string | Date | null; + public abstract value: string | Date | DateRangeValue | null; @query('input') protected input!: HTMLInputElement; diff --git a/src/index.ts b/src/index.ts index e36bc2449..8f5480836 100644 --- a/src/index.ts +++ b/src/index.ts @@ -100,7 +100,7 @@ export type { IgcChipComponentEventMap } from './components/chip/chip.js'; export type { IgcComboComponentEventMap } from './components/combo/types.js'; export type { IgcDatePickerComponentEventMap } from './components/date-picker/date-picker.js'; export type { IgcDateRangePickerComponentEventMap } from './components/date-range-picker/date-range-picker.js'; -export type { IgcDateTimeInputComponentEventMap } from './components/date-time-input/date-time-input.js'; +export type { IgcDateTimeInputComponentEventMap } from './components/date-time-input/date-time-input.base.js'; export type { IgcDialogComponentEventMap } from './components/dialog/dialog.js'; export type { IgcDropdownComponentEventMap } from './components/dropdown/dropdown.js'; export type { IgcExpansionPanelComponentEventMap } from './components/expansion-panel/expansion-panel.js'; diff --git a/stories/date-range-picker.stories.ts b/stories/date-range-picker.stories.ts index adc89be89..0fb9c562e 100644 --- a/stories/date-range-picker.stories.ts +++ b/stories/date-range-picker.stories.ts @@ -448,7 +448,6 @@ export const Default: Story = { render: (args) => html` = { actions: { handles: ['igcInput', 'igcChange'] }, }, argTypes: { + value: { + type: 'string | Date | DateRangeValue', + description: 'The value of the input.', + options: ['string', 'Date', 'DateRangeValue'], + control: 'text', + }, inputFormat: { type: 'string', description: 'The date format to apply on the input.', control: 'text', }, - value: { - type: 'string | Date', - description: 'The value of the input.', - options: ['string', 'Date'], - control: 'text', - }, min: { type: 'Date', description: 'The minimum value required for the input to remain valid.', @@ -132,10 +133,10 @@ const metadata: Meta = { export default metadata; interface IgcDateTimeInputArgs { + /** The value of the input. */ + value: string | Date | DateRangeValue; /** The date format to apply on the input. */ inputFormat: string; - /** The value of the input. */ - value: string | Date; /** The minimum value required for the input to remain valid. */ min: Date; /** The maximum value required for the input to remain valid. */ diff --git a/stories/file-input.stories.ts b/stories/file-input.stories.ts index 0716b1117..ab93d4a9d 100644 --- a/stories/file-input.stories.ts +++ b/stories/file-input.stories.ts @@ -10,6 +10,7 @@ import { registerIcon, registerIconFromText, } from 'igniteui-webcomponents'; +import type { DateRangeValue } from '../src/components/date-range-picker/date-range-picker.js'; import { formControls, formSubmitHandler } from './story.js'; defineComponents(IgcFileInputComponent, IgcIconComponent); @@ -29,9 +30,9 @@ const metadata: Meta = { }, argTypes: { value: { - type: 'string | Date', + type: 'string | Date | DateRangeValue', description: 'The value of the control.', - options: ['string', 'Date'], + options: ['string', 'Date', 'DateRangeValue'], control: 'text', }, multiple: { @@ -110,7 +111,7 @@ export default metadata; interface IgcFileInputArgs { /** The value of the control. */ - value: string | Date; + value: string | Date | DateRangeValue; /** * The multiple attribute of the control. * Used to indicate that a file input allows the user to select more than one file. diff --git a/stories/input.stories.ts b/stories/input.stories.ts index 13d2f8a48..e1f6af6b8 100644 --- a/stories/input.stories.ts +++ b/stories/input.stories.ts @@ -10,6 +10,7 @@ import { registerIcon, registerIconFromText, } from 'igniteui-webcomponents'; +import type { DateRangeValue } from '../src/components/date-range-picker/date-range-picker.js'; import { disableStoryControls, formControls, @@ -33,9 +34,9 @@ const metadata: Meta = { }, argTypes: { value: { - type: 'string | Date', + type: 'string | Date | DateRangeValue', description: 'The value of the control.', - options: ['string', 'Date'], + options: ['string', 'Date', 'DateRangeValue'], control: 'text', }, type: { @@ -162,7 +163,7 @@ export default metadata; interface IgcInputArgs { /** The value of the control. */ - value: string | Date; + value: string | Date | DateRangeValue; /** The type attribute of the control. */ type: 'text' | 'email' | 'number' | 'password' | 'search' | 'tel' | 'url'; /** diff --git a/stories/mask-input.stories.ts b/stories/mask-input.stories.ts index 9687bf60f..72e053f82 100644 --- a/stories/mask-input.stories.ts +++ b/stories/mask-input.stories.ts @@ -9,6 +9,7 @@ import { defineComponents, registerIconFromText, } from 'igniteui-webcomponents'; +import type { DateRangeValue } from '../src/components/date-range-picker/date-range-picker.js'; import { disableStoryControls, formControls, @@ -41,10 +42,10 @@ const metadata: Meta = { table: { defaultValue: { summary: 'raw' } }, }, value: { - type: 'string | Date', + type: 'string | Date | DateRangeValue', description: 'The value of the input.\n\nRegardless of the currently set `value-mode`, an empty value will return an empty string.', - options: ['string', 'Date'], + options: ['string', 'Date', 'DateRangeValue'], control: 'text', }, mask: { @@ -129,7 +130,7 @@ interface IgcMaskInputArgs { * * Regardless of the currently set `value-mode`, an empty value will return an empty string. */ - value: string | Date; + value: string | Date | DateRangeValue; /** The mask pattern to apply on the input. */ mask: string; /** The prompt symbol to use for unfilled parts of the mask. */