From a65bce2ce574790132dcd7191595e6b73a68cbde Mon Sep 17 00:00:00 2001 From: Sean Perkins Date: Fri, 19 Jan 2024 20:43:41 -0500 Subject: [PATCH] feat: support start/end object format for date range --- core/api.txt | 3 +- .../components/datetime/datetime-interface.ts | 5 ++ core/src/components/datetime/datetime.tsx | 69 ++++++++++++------- .../components/datetime/test/range/index.html | 5 ++ core/src/components/datetime/utils/format.ts | 18 ++++- .../components/datetime/utils/manipulation.ts | 19 +++-- core/src/components/datetime/utils/state.ts | 22 ++---- packages/angular/src/directives/proxies.ts | 4 +- packages/vue/src/proxies.ts | 1 + 9 files changed, 96 insertions(+), 50 deletions(-) diff --git a/core/api.txt b/core/api.txt index 39441b584f..f16897048c 100644 --- a/core/api.txt +++ b/core/api.txt @@ -408,6 +408,7 @@ ion-datetime,prop,multiple,boolean,false,false,false ion-datetime,prop,name,string,this.inputId,false,false ion-datetime,prop,preferWheel,boolean,false,false,false ion-datetime,prop,presentation,"date" | "date-time" | "month" | "month-year" | "time" | "time-date" | "year",'date-time',false,false +ion-datetime,prop,range,boolean,false,false,false ion-datetime,prop,readonly,boolean,false,false,false ion-datetime,prop,showClearButton,boolean,false,false,false ion-datetime,prop,showDefaultButtons,boolean,false,false,false @@ -415,7 +416,7 @@ ion-datetime,prop,showDefaultTimeLabel,boolean,true,false,false ion-datetime,prop,showDefaultTitle,boolean,false,false,false ion-datetime,prop,size,"cover" | "fixed",'fixed',false,false ion-datetime,prop,titleSelectedDatesFormatter,((selectedDates: string[]) => string) | undefined,undefined,false,false -ion-datetime,prop,value,null | string | string[] | undefined,undefined,false,false +ion-datetime,prop,value,null | string | string[] | undefined | { start: string; end: string; },undefined,false,false ion-datetime,prop,yearValues,number | number[] | string | undefined,undefined,false,false ion-datetime,method,cancel,cancel(closeOverlay?: boolean) => Promise ion-datetime,method,confirm,confirm(closeOverlay?: boolean) => Promise diff --git a/core/src/components/datetime/datetime-interface.ts b/core/src/components/datetime/datetime-interface.ts index 3f05601d5b..621286fb86 100644 --- a/core/src/components/datetime/datetime-interface.ts +++ b/core/src/components/datetime/datetime-interface.ts @@ -17,6 +17,11 @@ export interface DatetimeParts { ampm?: 'am' | 'pm'; } +export interface DatetimeRangeParts { + start: DatetimeParts; + end: DatetimeParts; +} + export type DatetimePresentation = 'date-time' | 'time-date' | 'date' | 'time' | 'month' | 'year' | 'month-year'; export type TitleSelectedDatesFormatter = (selectedDates: string[]) => string; diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 92935cb0ae..a992a3dbef 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -20,6 +20,7 @@ import type { DatetimeHighlightCallback, DatetimeHourCycle, DatetimeValue, + DatetimeRangeParts, } from './datetime-interface'; import { isSameDay, warnIfValueOutOfBounds, isBefore, isAfter } from './utils/comparison'; import type { WheelColumnOption } from './utils/data'; @@ -131,7 +132,7 @@ export class Datetime implements ComponentInterface { @State() showMonthAndYear = false; - @State() activeParts: DatetimeParts | DatetimeParts[] = []; + @State() activeParts: DatetimeParts | DatetimeParts[] | DatetimeRangeParts = []; @State() workingParts: DatetimeParts = { month: 5, @@ -642,30 +643,46 @@ export class Datetime implements ComponentInterface { this.setWorkingParts(validatedParts); if (multiple) { - const activePartsArray = Array.isArray(activeParts) ? activeParts : [activeParts]; + const activePartsArray = Array.isArray(activeParts) ? activeParts : ([activeParts] as DatetimeParts[]); if (removeDate) { this.activeParts = activePartsArray.filter((p) => !isSameDay(p, validatedParts)); } else { this.activeParts = [...activePartsArray, validatedParts]; } } else if (range) { - if (!Array.isArray(this.activeParts)) { - // If the activeParts is not an array, that means they have not made a selection yet - // and that the current activeParts is just the default value. - // We can set the start and end range to the same value. - this.activeParts = [validatedParts, validatedParts]; - } else { - // If the active parts is an array, then we need to determine which part of the range - // we are setting. We can do this by comparing if the validatedParts is before or after - // the first active part. Users can select the same day as the start or end of the range. + if (Array.isArray(this.activeParts)) { + /** + * If the active parts is an array, then we need to determine which part of the range + * we are setting. We can do this by comparing if the validatedParts is before or after + * the first active part. Users can select the same day as the start or end of the range. + */ const [start, end] = this.activeParts; if (start !== undefined && isBefore(validatedParts, start)) { - this.activeParts = [validatedParts, end]; + this.activeParts = { + start: validatedParts, + end, + }; } else if (end !== undefined && isAfter(validatedParts, end)) { - this.activeParts = [start, validatedParts]; + this.activeParts = { + start, + end: validatedParts, + }; } else { - this.activeParts = [validatedParts, validatedParts]; + this.activeParts = { + start: validatedParts, + end: validatedParts, + }; } + } else { + /** + * If the activeParts is not an array, that means they have not made a selection yet + * and that the current activeParts is just the default value. + * We can set the start and end range to the same value. + */ + this.activeParts = { + start: validatedParts, + end: validatedParts, + }; } } else { this.activeParts = { @@ -1600,7 +1617,7 @@ export class Datetime implements ComponentInterface { private renderCombinedDatePickerColumn() { const { defaultParts, disabled, workingParts, locale, minParts, maxParts, todayParts, isDateEnabled } = this; - const activePart = this.getActivePartsWithFallback(); + const activePart = this.getActivePartsWithFallback() as DatetimeParts; /** * By default, generate a range of 3 months: @@ -1801,7 +1818,7 @@ export class Datetime implements ComponentInterface { const { disabled, workingParts } = this; - const activePart = this.getActivePartsWithFallback(); + const activePart = this.getActivePartsWithFallback() as DatetimeParts; const pickerColumnValue = (workingParts.day !== null ? workingParts.day : this.defaultParts.day) ?? undefined; return ( @@ -1857,7 +1874,7 @@ export class Datetime implements ComponentInterface { const { disabled, workingParts } = this; - const activePart = this.getActivePartsWithFallback(); + const activePart = this.getActivePartsWithFallback() as DatetimeParts; return ( {this.renderTimeLabel()}, @@ -2527,7 +2544,7 @@ export class Datetime implements ComponentInterface { } } else { // for exactly 1 day selected (multiple set or not), show a formatted version of that - headerText = getMonthAndDay(this.locale, this.getActivePartsWithFallback()); + headerText = getMonthAndDay(this.locale, this.getActivePartsWithFallback() as DatetimeParts); // TODO verify if this can show with range enabled } return headerText; diff --git a/core/src/components/datetime/test/range/index.html b/core/src/components/datetime/test/range/index.html index a6c6d95f51..74b7221e88 100644 --- a/core/src/components/datetime/test/range/index.html +++ b/core/src/components/datetime/test/range/index.html @@ -267,6 +267,11 @@ datetime.addEventListener('ionChange', (event) => { console.log('Listen ionChange', event.detail); }); + + datetime.value = { + start: '2024-01-05', + end: '2024-02-10', + }; diff --git a/core/src/components/datetime/utils/format.ts b/core/src/components/datetime/utils/format.ts index be8195c151..66203c2c63 100644 --- a/core/src/components/datetime/utils/format.ts +++ b/core/src/components/datetime/utils/format.ts @@ -1,4 +1,4 @@ -import type { DatetimeParts, DatetimeHourCycle, DatetimeValue } from '../datetime-interface'; +import type { DatetimeParts, DatetimeHourCycle, DatetimeValue, DatetimeRangeParts } from '../datetime-interface'; import { is24Hour } from './helpers'; import { convertDataToISO } from './manipulation'; @@ -353,3 +353,19 @@ export const formatValue = (value?: DatetimeValue) => { } return value; }; + +/** + * Takes an incoming active parts object and formats it into an array + * of active parts. + * @param activeParts The active parts object to format. + * @returns Active parts as an array. + */ +export const activePartsToArray = (activeParts: DatetimeParts | DatetimeRangeParts | DatetimeParts[]) => { + if (Array.isArray(activeParts)) { + return activeParts; + } + if ('start' in activeParts) { + return [activeParts.start, activeParts.end]; + } + return [activeParts]; +} diff --git a/core/src/components/datetime/utils/manipulation.ts b/core/src/components/datetime/utils/manipulation.ts index 152f2f62fe..fbb521c9b6 100644 --- a/core/src/components/datetime/utils/manipulation.ts +++ b/core/src/components/datetime/utils/manipulation.ts @@ -1,4 +1,4 @@ -import type { DatetimeParts } from '../datetime-interface'; +import type { DatetimeMultipleValue, DatetimeParts, DatetimeRangeParts, DatetimeRangeValue } from '../datetime-interface'; import { isAfter, isBefore, isSameDay } from './comparison'; import { getNumDaysInMonth } from './helpers'; @@ -13,13 +13,22 @@ const fourDigit = (val: number | undefined): string => { }; export function convertDataToISO(data: DatetimeParts): string; -export function convertDataToISO(data: DatetimeParts[]): string[]; -export function convertDataToISO(data: DatetimeParts | DatetimeParts[]): string | string[]; -export function convertDataToISO(data: DatetimeParts | DatetimeParts[]): string | string[] { +export function convertDataToISO(data: DatetimeRangeParts): DatetimeRangeValue; +export function convertDataToISO(data: DatetimeParts[]): DatetimeMultipleValue; +export function convertDataToISO(data: DatetimeParts | DatetimeRangeParts | DatetimeParts[]): string | DatetimeMultipleValue; +export function convertDataToISO(data: DatetimeParts | DatetimeRangeParts | DatetimeParts[]): string | DatetimeMultipleValue | DatetimeRangeValue { + // Multiple selection if (Array.isArray(data)) { return data.map((parts) => convertDataToISO(parts)); } - + // Range selection + if ('start' in data) { + return { + start: convertDataToISO(data.start), + end: convertDataToISO(data.end), + }; + } + // Single selection // https://www.w3.org/TR/NOTE-datetime let rtn = ''; if (data.year !== undefined) { diff --git a/core/src/components/datetime/utils/state.ts b/core/src/components/datetime/utils/state.ts index 16026b2a62..bb93bfafe6 100644 --- a/core/src/components/datetime/utils/state.ts +++ b/core/src/components/datetime/utils/state.ts @@ -5,10 +5,11 @@ import type { DatetimeHighlightCallback, DatetimeHighlightStyle, DatetimeParts, + DatetimeRangeParts, } from '../datetime-interface'; import { isAfter, isBefore, isSameDay } from './comparison'; -import { generateDayAriaLabel, getDay } from './format'; +import { activePartsToArray, generateDayAriaLabel, getDay } from './format'; import { getNextMonth, getPreviousMonth } from './manipulation'; export const isYearDisabled = (refYear: number, minParts?: DatetimeParts, maxParts?: DatetimeParts) => { @@ -96,26 +97,17 @@ export const isDayDisabled = ( export const getCalendarDayState = ( locale: string, refParts: DatetimeParts, - activeParts: DatetimeParts | DatetimeParts[], + activeParts: DatetimeParts | DatetimeRangeParts | DatetimeParts[], todayParts: DatetimeParts, minParts?: DatetimeParts, maxParts?: DatetimeParts, dayValues?: number[] ) => { - /** - * activeParts signals what day(s) are currently selected in the datetime. - * If multiple="true", this will be an array, but the logic in this util - * is the same whether we have one selected day or many because we're only - * calculating the state for one button. So, we treat a single activeParts value - * the same as an array of length one. - */ - const activePartsArray = Array.isArray(activeParts) ? activeParts : [activeParts]; - /** * The day button is active if it is selected, or in other words, if refParts * matches at least one selected date. */ - const isActive = activePartsArray.find((parts) => isSameDay(refParts, parts)) !== undefined; + const isActive = activePartsToArray(activeParts).find((parts) => isSameDay(refParts, parts)) !== undefined; const isToday = isSameDay(refParts, todayParts); const disabled = isDayDisabled(refParts, minParts, maxParts, dayValues); @@ -228,7 +220,7 @@ export const getHighlightStyles = ( return undefined; }; -export const isDateRangeStart = (referenceParts: DatetimeParts, activeParts: DatetimeParts | DatetimeParts[]) => { +export const isDateRangeStart = (referenceParts: DatetimeParts, activeParts: DatetimeRangeParts) => { if (activeParts !== undefined && Array.isArray(activeParts)) { const startDate = activeParts[0]; return startDate !== undefined && isSameDay(referenceParts, startDate); @@ -236,7 +228,7 @@ export const isDateRangeStart = (referenceParts: DatetimeParts, activeParts: Dat return false; } -export const isDateRangeEnd = (referenceParts: DatetimeParts, activeParts: DatetimeParts | DatetimeParts[]) => { +export const isDateRangeEnd = (referenceParts: DatetimeParts, activeParts: DatetimeRangeParts) => { if (activeParts !== undefined && Array.isArray(activeParts)) { const endDate = activeParts[1]; return endDate !== undefined && isSameDay(referenceParts, endDate); @@ -244,7 +236,7 @@ export const isDateRangeEnd = (referenceParts: DatetimeParts, activeParts: Datet return false; } -export const isDateInRange = (referenceParts: DatetimeParts, activeParts: DatetimeParts | DatetimeParts[]) => { +export const isDateInRange = (referenceParts: DatetimeParts, activeParts: DatetimeRangeParts) => { if (activeParts !== undefined && Array.isArray(activeParts)) { const startDate = activeParts[0]; const endDate = activeParts[1]; diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index 20d69a036f..3b65797fd3 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -635,7 +635,7 @@ Set `scrollEvents` to `true` to enable. @ProxyCmp({ - inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'highlightedDates', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'multiple', 'name', 'preferWheel', 'presentation', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'titleSelectedDatesFormatter', 'value', 'yearValues'], + inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'highlightedDates', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'multiple', 'name', 'preferWheel', 'presentation', 'range', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'titleSelectedDatesFormatter', 'value', 'yearValues'], methods: ['confirm', 'reset', 'cancel'] }) @Component({ @@ -643,7 +643,7 @@ Set `scrollEvents` to `true` to enable. changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'highlightedDates', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'multiple', 'name', 'preferWheel', 'presentation', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'titleSelectedDatesFormatter', 'value', 'yearValues'], + inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'highlightedDates', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'multiple', 'name', 'preferWheel', 'presentation', 'range', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'titleSelectedDatesFormatter', 'value', 'yearValues'], }) export class IonDatetime { protected el: HTMLElement; diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 3359b1684e..cf8e062476 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -294,6 +294,7 @@ export const IonDatetime = /*@__PURE__*/ defineContainer