feat: support start/end object format for date range

This commit is contained in:
Sean Perkins
2024-01-19 20:43:41 -05:00
parent 804d51f3ef
commit a65bce2ce5
9 changed files with 96 additions and 50 deletions

View File

@@ -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<void>
ion-datetime,method,confirm,confirm(closeOverlay?: boolean) => Promise<void>

View File

@@ -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;

View File

@@ -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 (
<ion-picker-column
@@ -1911,7 +1928,7 @@ export class Datetime implements ComponentInterface {
const { disabled, workingParts } = this;
const activePart = this.getActivePartsWithFallback();
const activePart = this.getActivePartsWithFallback() as DatetimeParts;
return (
<ion-picker-column
@@ -1996,7 +2013,7 @@ export class Datetime implements ComponentInterface {
const { disabled, workingParts } = this;
if (hoursData.length === 0) return [];
const activePart = this.getActivePartsWithFallback();
const activePart = this.getActivePartsWithFallback() as DatetimeParts;
return (
<ion-picker-column
@@ -2035,7 +2052,7 @@ export class Datetime implements ComponentInterface {
const { disabled, workingParts } = this;
if (minutesData.length === 0) return [];
const activePart = this.getActivePartsWithFallback();
const activePart = this.getActivePartsWithFallback() as DatetimeParts;
return (
<ion-picker-column
@@ -2076,7 +2093,7 @@ export class Datetime implements ComponentInterface {
return [];
}
const activePart = this.getActivePartsWithFallback();
const activePart = this.getActivePartsWithFallback() as DatetimeParts;
const isDayPeriodRTL = isLocaleDayPeriodRTL(this.locale);
return (
@@ -2315,9 +2332,9 @@ export class Datetime implements ComponentInterface {
let dateParts = undefined;
const inRange = range && isDateInRange(referenceParts, activeParts);
const isRangeStart = range && isDateRangeStart(referenceParts, activeParts);
const isRangeEnd = range && isDateRangeEnd(referenceParts, activeParts);
const inRange = range && isDateInRange(referenceParts, activeParts as DatetimeRangeParts);
const isRangeStart = range && isDateRangeStart(referenceParts, activeParts as DatetimeRangeParts);
const isRangeEnd = range && isDateRangeEnd(referenceParts, activeParts as DatetimeRangeParts);
// "Filler days" at the beginning of the grid should not get the calendar day
// CSS parts added to them
@@ -2444,7 +2461,7 @@ export class Datetime implements ComponentInterface {
private renderTimeOverlay() {
const { disabled, hourCycle, isTimePopoverOpen, locale } = this;
const computedHourCycle = getHourCycle(locale, hourCycle);
const activePart = this.getActivePartsWithFallback();
const activePart = this.getActivePartsWithFallback() as DatetimeParts;
return [
<div class="time-header">{this.renderTimeLabel()}</div>,
@@ -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;

View File

@@ -267,6 +267,11 @@
datetime.addEventListener('ionChange', (event) => {
console.log('Listen ionChange', event.detail);
});
datetime.value = {
start: '2024-01-05',
end: '2024-02-10',
};
</script>
</ion-app>
</body>

View File

@@ -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];
}

View File

@@ -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) {

View File

@@ -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];

View File

@@ -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: '<ng-content></ng-content>',
// 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;

View File

@@ -294,6 +294,7 @@ export const IonDatetime = /*@__PURE__*/ defineContainer<JSX.IonDatetime, JSX.Io
'firstDayOfWeek',
'titleSelectedDatesFormatter',
'multiple',
'range',
'highlightedDates',
'value',
'showDefaultTitle',