feat(datetime): add wheel style picker for dates and times (#25468)
@ -524,14 +524,14 @@ export declare interface IonDatetime extends Components.IonDatetime {
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: undefined,
|
||||
inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'name', 'presentation', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'value', 'yearValues'],
|
||||
inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'name', 'preferWheel', 'presentation', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'value', 'yearValues'],
|
||||
methods: ['confirm', 'reset', 'cancel']
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-datetime',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'name', 'presentation', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'value', 'yearValues']
|
||||
inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'name', 'preferWheel', 'presentation', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'value', 'yearValues']
|
||||
})
|
||||
export class IonDatetime {
|
||||
protected el: HTMLElement;
|
||||
|
@ -388,6 +388,7 @@ ion-datetime,prop,minuteValues,number | number[] | string | undefined,undefined,
|
||||
ion-datetime,prop,mode,"ios" | "md",undefined,false,false
|
||||
ion-datetime,prop,monthValues,number | number[] | string | undefined,undefined,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,readonly,boolean,false,false,false
|
||||
ion-datetime,prop,showClearButton,boolean,false,false,false
|
||||
|
8
core/src/components.d.ts
vendored
@ -789,6 +789,10 @@ export namespace Components {
|
||||
* The name of the control, which is submitted with the form data.
|
||||
*/
|
||||
"name": string;
|
||||
/**
|
||||
* If `true`, a wheel picker will be rendered instead of a calendar grid where possible. If `false`, a calendar grid will be rendered instead of a wheel picker where possible. A wheel picker can be rendered instead of a grid when `presentation` is one of the following values: `'date'`, `'date-time'`, or `'time-date'`. A wheel picker will always be rendered regardless of the `preferWheel` value when `presentation` is one of the following values: `'time'`, `'month'`, `'month-year'`, or `'year'`.
|
||||
*/
|
||||
"preferWheel": boolean;
|
||||
/**
|
||||
* Which values you want to select. `'date'` will show a calendar picker to select the month, day, and year. `'time'` will show a time picker to select the hour, minute, and (optionally) AM/PM. `'date-time'` will show the date picker first and time picker second. `'time-date'` will show the time picker first and date picker second.
|
||||
*/
|
||||
@ -4707,6 +4711,10 @@ declare namespace LocalJSX {
|
||||
* Emitted when the styles change.
|
||||
*/
|
||||
"onIonStyle"?: (event: IonDatetimeCustomEvent<StyleEventDetail>) => void;
|
||||
/**
|
||||
* If `true`, a wheel picker will be rendered instead of a calendar grid where possible. If `false`, a calendar grid will be rendered instead of a wheel picker where possible. A wheel picker can be rendered instead of a grid when `presentation` is one of the following values: `'date'`, `'date-time'`, or `'time-date'`. A wheel picker will always be rendered regardless of the `preferWheel` value when `presentation` is one of the following values: `'time'`, `'month'`, `'month-year'`, or `'year'`.
|
||||
*/
|
||||
"preferWheel"?: boolean;
|
||||
/**
|
||||
* Which values you want to select. `'date'` will show a calendar picker to select the month, day, and year. `'time'` will show a time picker to select the hour, minute, and (optionally) AM/PM. `'date-time'` will show the date picker first and time picker second. `'time-date'` will show the time picker first and date picker second.
|
||||
*/
|
||||
|
@ -8,9 +8,9 @@
|
||||
--title-color: #{$text-color-step-400};
|
||||
}
|
||||
|
||||
:host(.datetime-presentation-date-time),
|
||||
:host(.datetime-presentation-time-date),
|
||||
:host(.datetime-presentation-date) {
|
||||
:host(.datetime-presentation-date-time:not(.datetime-prefer-wheel)),
|
||||
:host(.datetime-presentation-time-date:not(.datetime-prefer-wheel)),
|
||||
:host(.datetime-presentation-date:not(.datetime-prefer-wheel)) {
|
||||
min-height: 350px;
|
||||
}
|
||||
|
||||
|
@ -19,12 +19,37 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* When using the wheel picker to switch
|
||||
* between months, sometimes the allowed
|
||||
* dates may be filtered. As a result, it
|
||||
* is possible to get a layout shift as
|
||||
* the picker column will shrink to fit the
|
||||
* widest item in the column. Setting a minimum
|
||||
* width avoids this layout shifting.
|
||||
*/
|
||||
ion-picker-column-internal {
|
||||
min-width: 26px;
|
||||
}
|
||||
|
||||
:host(.datetime-size-fixed) {
|
||||
width: auto;
|
||||
max-width: 350px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
:host(.datetime-size-fixed:not(.datetime-prefer-wheel)) {
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
/**
|
||||
* This ensures that the picker is apppropriately
|
||||
* sized and never truncates the text.
|
||||
*/
|
||||
:host(.datetime-size-fixed.datetime-prefer-wheel) {
|
||||
min-width: 350px;
|
||||
max-width: max-content;
|
||||
}
|
||||
|
||||
:host(.datetime-size-cover) {
|
||||
width: 100%;
|
||||
}
|
||||
@ -58,21 +83,19 @@
|
||||
* the order we need to manually switch
|
||||
* the text alignment too.
|
||||
*/
|
||||
:host .datetime-year .order-month-first .month-column {
|
||||
order: 1;
|
||||
:host .wheel-order-year-first .day-column {
|
||||
order: 3;
|
||||
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
:host .datetime-year .order-month-first .year-column {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
:host .datetime-year .order-year-first .month-column {
|
||||
:host .wheel-order-year-first .month-column {
|
||||
order: 2;
|
||||
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
:host .datetime-year .order-year-first .year-column {
|
||||
:host .wheel-order-year-first .year-column {
|
||||
order: 1;
|
||||
|
||||
text-align: start;
|
||||
|
@ -13,27 +13,21 @@ import type { PickerColumnItem } from '../picker-column-internal/picker-column-i
|
||||
|
||||
import {
|
||||
generateMonths,
|
||||
generateTime,
|
||||
getCalendarYears,
|
||||
getDaysOfMonth,
|
||||
getDaysOfWeek,
|
||||
getPickerMonths,
|
||||
getToday,
|
||||
getMonthColumnData,
|
||||
getDayColumnData,
|
||||
getYearColumnData,
|
||||
getTimeColumnsData,
|
||||
getCombinedDateColumnData,
|
||||
} from './utils/data';
|
||||
import {
|
||||
addTimePadding,
|
||||
getFormattedHour,
|
||||
getLocalizedTime,
|
||||
getLocalizedDayPeriod,
|
||||
getMonthAndDay,
|
||||
getMonthAndYear,
|
||||
} from './utils/format';
|
||||
import { getLocalizedTime, getMonthAndDay, getMonthAndYear } from './utils/format';
|
||||
import { is24Hour, isLocaleDayPeriodRTL, isMonthFirstLocale } from './utils/helpers';
|
||||
import {
|
||||
calculateHourFromAMPM,
|
||||
convertDataToISO,
|
||||
getEndOfWeek,
|
||||
getInternalHourValue,
|
||||
getNextDay,
|
||||
getNextMonth,
|
||||
getNextWeek,
|
||||
@ -415,6 +409,20 @@ export class Datetime implements ComponentInterface {
|
||||
*/
|
||||
@Prop() size: 'cover' | 'fixed' = 'fixed';
|
||||
|
||||
/**
|
||||
* If `true`, a wheel picker will be rendered instead of a calendar grid
|
||||
* where possible. If `false`, a calendar grid will be rendered instead of
|
||||
* a wheel picker where possible.
|
||||
*
|
||||
* A wheel picker can be rendered instead of a grid when `presentation` is
|
||||
* one of the following values: `'date'`, `'date-time'`, or `'time-date'`.
|
||||
*
|
||||
* A wheel picker will always be rendered regardless of
|
||||
* the `preferWheel` value when `presentation` is one of the following values:
|
||||
* `'time'`, `'month'`, `'month-year'`, or `'year'`.
|
||||
*/
|
||||
@Prop() preferWheel = false;
|
||||
|
||||
/**
|
||||
* Emitted when the datetime selection was cancelled.
|
||||
*/
|
||||
@ -1185,6 +1193,16 @@ export class Datetime implements ComponentInterface {
|
||||
});
|
||||
};
|
||||
|
||||
private toggleMonthAndYearView = () => {
|
||||
this.showMonthAndYear = !this.showMonthAndYear;
|
||||
};
|
||||
|
||||
/**
|
||||
* Universal render methods
|
||||
* These are pieces of datetime that
|
||||
* are rendered independently of presentation.
|
||||
*/
|
||||
|
||||
private renderFooter() {
|
||||
const { showDefaultButtons, showClearButton } = this;
|
||||
const hasSlottedButtons = this.el.querySelector('[slot="buttons"]') !== null;
|
||||
@ -1240,35 +1258,237 @@ export class Datetime implements ComponentInterface {
|
||||
);
|
||||
}
|
||||
|
||||
private toggleMonthAndYearView = () => {
|
||||
this.showMonthAndYear = !this.showMonthAndYear;
|
||||
};
|
||||
/**
|
||||
* Wheel picker render methods
|
||||
*/
|
||||
|
||||
private renderYearView() {
|
||||
const { presentation, workingParts, locale } = this;
|
||||
const calendarYears = getCalendarYears(this.todayParts, this.minParts, this.maxParts, this.parsedYearValues);
|
||||
const showMonth = presentation !== 'year';
|
||||
const showYear = presentation !== 'month';
|
||||
private renderWheelPicker(forcePresentation: string = this.presentation) {
|
||||
/**
|
||||
* If presentation="time-date" we switch the
|
||||
* order of the render array here instead of
|
||||
* manually reordering each date/time picker
|
||||
* column with CSS. This allows for additional
|
||||
* flexibility if we need to render subsets
|
||||
* of the date/time data or do additional ordering
|
||||
* within the child render functions.
|
||||
*/
|
||||
const renderArray =
|
||||
forcePresentation === 'time-date'
|
||||
? [this.renderTimePickerColumns(forcePresentation), this.renderDatePickerColumns(forcePresentation)]
|
||||
: [this.renderDatePickerColumns(forcePresentation), this.renderTimePickerColumns(forcePresentation)];
|
||||
return <ion-picker-internal>{renderArray}</ion-picker-internal>;
|
||||
}
|
||||
|
||||
private renderDatePickerColumns(forcePresentation: string) {
|
||||
return forcePresentation === 'date-time' || forcePresentation === 'time-date'
|
||||
? this.renderCombinedDatePickerColumn()
|
||||
: this.renderIndividualDatePickerColumns(forcePresentation);
|
||||
}
|
||||
|
||||
private renderCombinedDatePickerColumn() {
|
||||
const { workingParts, locale, minParts, maxParts, todayParts, isDateEnabled } = this;
|
||||
|
||||
/**
|
||||
* By default, generate a range of 3 months:
|
||||
* Previous month, current month, and next month
|
||||
*/
|
||||
const monthsToRender = generateMonths(workingParts);
|
||||
|
||||
/**
|
||||
* generateMonths returns the day data as well,
|
||||
* but we do not want the day value to act as a max/min
|
||||
* on the data we are going to generate.
|
||||
*/
|
||||
for (let i = 0; i <= monthsToRender.length - 1; i++) {
|
||||
monthsToRender[i].day = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* If developers have provided their own
|
||||
* min/max values, use that instead. Otherwise,
|
||||
* fallback to the default range of 3 months.
|
||||
*/
|
||||
const min = minParts || monthsToRender[0];
|
||||
const max = maxParts || monthsToRender[monthsToRender.length - 1];
|
||||
|
||||
const result = getCombinedDateColumnData(
|
||||
locale,
|
||||
workingParts,
|
||||
todayParts,
|
||||
min,
|
||||
max,
|
||||
this.parsedDayValues,
|
||||
this.parsedMonthValues
|
||||
);
|
||||
let items = result.items;
|
||||
const parts = result.parts;
|
||||
|
||||
if (isDateEnabled) {
|
||||
items = items.map((itemObject, index) => {
|
||||
const referenceParts = parts[index];
|
||||
|
||||
let disabled;
|
||||
try {
|
||||
/**
|
||||
* The `isDateEnabled` implementation is try-catch wrapped
|
||||
* to prevent exceptions in the user's function from
|
||||
* interrupting the calendar rendering.
|
||||
*/
|
||||
disabled = !isDateEnabled(convertDataToISO(referenceParts));
|
||||
} catch (e) {
|
||||
printIonError(
|
||||
'Exception thrown from provided `isDateEnabled` function. Please check your function and try again.',
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
const months = getPickerMonths(locale, workingParts, this.minParts, this.maxParts, this.parsedMonthValues);
|
||||
const years = calendarYears.map((year) => {
|
||||
return {
|
||||
text: `${year}`,
|
||||
value: year,
|
||||
...itemObject,
|
||||
disabled,
|
||||
};
|
||||
});
|
||||
const showMonthFirst = isMonthFirstLocale(locale);
|
||||
const columnOrder = showMonthFirst ? 'month-first' : 'year-first';
|
||||
}
|
||||
|
||||
/**
|
||||
* If we have selected a day already, then default the column
|
||||
* to that value. Otherwise, default it to today.
|
||||
*/
|
||||
const todayString = workingParts.day
|
||||
? `${workingParts.year}-${workingParts.month}-${workingParts.day}`
|
||||
: `${todayParts.year}-${todayParts.month}-${todayParts.day}`;
|
||||
|
||||
return (
|
||||
<div class="datetime-year">
|
||||
<div
|
||||
class={{
|
||||
'datetime-year-body': true,
|
||||
[`order-${columnOrder}`]: true,
|
||||
<ion-picker-column-internal
|
||||
class="date-column"
|
||||
color={this.color}
|
||||
items={items}
|
||||
value={todayString}
|
||||
onIonChange={(ev: CustomEvent) => {
|
||||
// Due to a Safari 14 issue we need to destroy
|
||||
// the intersection observer before we update state
|
||||
// and trigger a re-render.
|
||||
if (this.destroyCalendarIO) {
|
||||
this.destroyCalendarIO();
|
||||
}
|
||||
|
||||
const { value } = ev.detail;
|
||||
const findPart = parts.find(({ month, day, year }) => value === `${year}-${month}-${day}`);
|
||||
|
||||
this.setWorkingParts({
|
||||
...this.workingParts,
|
||||
...findPart,
|
||||
});
|
||||
|
||||
this.setActiveParts({
|
||||
...this.activeParts,
|
||||
...findPart,
|
||||
});
|
||||
|
||||
// We can re-attach the intersection observer after
|
||||
// the working parts have been updated.
|
||||
this.initializeCalendarIOListeners();
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<ion-picker-internal>
|
||||
{showMonth && (
|
||||
></ion-picker-column-internal>
|
||||
);
|
||||
}
|
||||
|
||||
private renderIndividualDatePickerColumns(forcePresentation: string) {
|
||||
const { workingParts, isDateEnabled } = this;
|
||||
const shouldRenderMonths = forcePresentation !== 'year' && forcePresentation !== 'time';
|
||||
const months = shouldRenderMonths
|
||||
? getMonthColumnData(this.locale, workingParts, this.minParts, this.maxParts, this.parsedMonthValues)
|
||||
: [];
|
||||
|
||||
const shouldRenderDays = forcePresentation === 'date';
|
||||
let days = shouldRenderDays
|
||||
? getDayColumnData(this.locale, workingParts, this.minParts, this.maxParts, this.parsedDayValues)
|
||||
: [];
|
||||
|
||||
if (isDateEnabled) {
|
||||
days = days.map((dayObject) => {
|
||||
const referenceParts = { month: workingParts.month, day: dayObject.value, year: workingParts.year };
|
||||
|
||||
let disabled;
|
||||
try {
|
||||
/**
|
||||
* The `isDateEnabled` implementation is try-catch wrapped
|
||||
* to prevent exceptions in the user's function from
|
||||
* interrupting the calendar rendering.
|
||||
*/
|
||||
disabled = !isDateEnabled(convertDataToISO(referenceParts));
|
||||
} catch (e) {
|
||||
printIonError(
|
||||
'Exception thrown from provided `isDateEnabled` function. Please check your function and try again.',
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...dayObject,
|
||||
disabled,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const shouldRenderYears = forcePresentation !== 'month' && forcePresentation !== 'time';
|
||||
const years = shouldRenderYears
|
||||
? getYearColumnData(this.todayParts, this.minParts, this.maxParts, this.parsedYearValues)
|
||||
: [];
|
||||
|
||||
return [this.renderMonthPickerColumn(months), this.renderDayPickerColumn(days), this.renderYearPickerColumn(years)];
|
||||
}
|
||||
|
||||
private renderDayPickerColumn(days: PickerColumnItem[]) {
|
||||
if (days.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { workingParts } = this;
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
class="day-column"
|
||||
color={this.color}
|
||||
items={days}
|
||||
value={workingParts.day || this.todayParts.day}
|
||||
onIonChange={(ev: CustomEvent) => {
|
||||
// Due to a Safari 14 issue we need to destroy
|
||||
// the intersection observer before we update state
|
||||
// and trigger a re-render.
|
||||
if (this.destroyCalendarIO) {
|
||||
this.destroyCalendarIO();
|
||||
}
|
||||
|
||||
this.setWorkingParts({
|
||||
...this.workingParts,
|
||||
day: ev.detail.value,
|
||||
});
|
||||
|
||||
this.setActiveParts({
|
||||
...this.activeParts,
|
||||
day: ev.detail.value,
|
||||
});
|
||||
|
||||
// We can re-attach the intersection observer after
|
||||
// the working parts have been updated.
|
||||
this.initializeCalendarIOListeners();
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
);
|
||||
}
|
||||
|
||||
private renderMonthPickerColumn(months: PickerColumnItem[]) {
|
||||
if (months.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { workingParts } = this;
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
class="month-column"
|
||||
color={this.color}
|
||||
@ -1287,12 +1507,10 @@ export class Datetime implements ComponentInterface {
|
||||
month: ev.detail.value,
|
||||
});
|
||||
|
||||
if (presentation === 'month' || presentation === 'month-year') {
|
||||
this.setActiveParts({
|
||||
...this.activeParts,
|
||||
month: ev.detail.value,
|
||||
});
|
||||
}
|
||||
|
||||
// We can re-attach the intersection observer after
|
||||
// the working parts have been updated.
|
||||
@ -1301,8 +1519,16 @@ export class Datetime implements ComponentInterface {
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
)}
|
||||
{showYear && (
|
||||
);
|
||||
}
|
||||
private renderYearPickerColumn(years: PickerColumnItem[]) {
|
||||
if (years.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { workingParts } = this;
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
class="year-column"
|
||||
color={this.color}
|
||||
@ -1321,12 +1547,10 @@ export class Datetime implements ComponentInterface {
|
||||
year: ev.detail.value,
|
||||
});
|
||||
|
||||
if (presentation === 'year' || presentation === 'month-year') {
|
||||
this.setActiveParts({
|
||||
...this.activeParts,
|
||||
year: ev.detail.value,
|
||||
});
|
||||
}
|
||||
|
||||
// We can re-attach the intersection observer after
|
||||
// the working parts have been updated.
|
||||
@ -1335,13 +1559,134 @@ export class Datetime implements ComponentInterface {
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
)}
|
||||
</ion-picker-internal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderTimePickerColumns(forcePresentation: string) {
|
||||
if (['date', 'month', 'month-year', 'year'].includes(forcePresentation)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { hoursData, minutesData, dayPeriodData } = getTimeColumnsData(
|
||||
this.locale,
|
||||
this.workingParts,
|
||||
this.hourCycle,
|
||||
this.value ? this.minParts : undefined,
|
||||
this.value ? this.maxParts : undefined,
|
||||
this.parsedHourValues,
|
||||
this.parsedMinuteValues
|
||||
);
|
||||
|
||||
return [
|
||||
this.renderHourPickerColumn(hoursData),
|
||||
this.renderMinutePickerColumn(minutesData),
|
||||
this.renderDayPeriodPickerColumn(dayPeriodData),
|
||||
];
|
||||
}
|
||||
|
||||
private renderHourPickerColumn(hoursData: PickerColumnItem[]) {
|
||||
const { workingParts, activePartsClone } = this;
|
||||
if (hoursData.length === 0) return [];
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
color={this.color}
|
||||
value={activePartsClone.hour}
|
||||
items={hoursData}
|
||||
numericInput
|
||||
onIonChange={(ev: CustomEvent) => {
|
||||
this.setWorkingParts({
|
||||
...workingParts,
|
||||
hour: ev.detail.value,
|
||||
});
|
||||
this.setActiveParts({
|
||||
...activePartsClone,
|
||||
hour: ev.detail.value,
|
||||
});
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
);
|
||||
}
|
||||
private renderMinutePickerColumn(minutesData: PickerColumnItem[]) {
|
||||
const { workingParts, activePartsClone } = this;
|
||||
if (minutesData.length === 0) return [];
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
color={this.color}
|
||||
value={activePartsClone.minute}
|
||||
items={minutesData}
|
||||
numericInput
|
||||
onIonChange={(ev: CustomEvent) => {
|
||||
this.setWorkingParts({
|
||||
...workingParts,
|
||||
minute: ev.detail.value,
|
||||
});
|
||||
this.setActiveParts({
|
||||
...activePartsClone,
|
||||
minute: ev.detail.value,
|
||||
});
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
);
|
||||
}
|
||||
private renderDayPeriodPickerColumn(dayPeriodData: PickerColumnItem[]) {
|
||||
const { workingParts, activePartsClone } = this;
|
||||
if (dayPeriodData.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const isDayPeriodRTL = isLocaleDayPeriodRTL(this.locale);
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
style={isDayPeriodRTL ? { order: '-1' } : {}}
|
||||
color={this.color}
|
||||
value={activePartsClone.ampm}
|
||||
items={dayPeriodData}
|
||||
onIonChange={(ev: CustomEvent) => {
|
||||
const hour = calculateHourFromAMPM(workingParts, ev.detail.value);
|
||||
|
||||
this.setWorkingParts({
|
||||
...workingParts,
|
||||
ampm: ev.detail.value,
|
||||
hour,
|
||||
});
|
||||
|
||||
this.setActiveParts({
|
||||
...activePartsClone,
|
||||
ampm: ev.detail.value,
|
||||
hour,
|
||||
});
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
);
|
||||
}
|
||||
|
||||
private renderWheelView(forcePresentation?: string) {
|
||||
const { locale } = this;
|
||||
const showMonthFirst = isMonthFirstLocale(locale);
|
||||
const columnOrder = showMonthFirst ? 'month-first' : 'year-first';
|
||||
return (
|
||||
<div
|
||||
class={{
|
||||
[`wheel-order-${columnOrder}`]: true,
|
||||
}}
|
||||
>
|
||||
{this.renderWheelPicker(forcePresentation)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grid Render Methods
|
||||
*/
|
||||
|
||||
private renderCalendarHeader(mode: Mode) {
|
||||
const expandedIcon = mode === 'ios' ? chevronDown : caretUpSharp;
|
||||
const collapsedIcon = mode === 'ios' ? chevronForward : caretDownSharp;
|
||||
@ -1380,7 +1725,6 @@ export class Datetime implements ComponentInterface {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderMonth(month: number, year: number) {
|
||||
const { highlightActiveParts } = this;
|
||||
const yearAllowed = this.parsedYearValues === undefined || this.parsedYearValues.includes(year);
|
||||
@ -1499,7 +1843,6 @@ export class Datetime implements ComponentInterface {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderCalendarBody() {
|
||||
return (
|
||||
<div class="calendar-body ion-focusable" ref={(el) => (this.calendarBodyRef = el)} tabindex="0">
|
||||
@ -1509,7 +1852,6 @@ export class Datetime implements ComponentInterface {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderCalendar(mode: Mode) {
|
||||
return (
|
||||
<div class="datetime-calendar">
|
||||
@ -1518,7 +1860,6 @@ export class Datetime implements ComponentInterface {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderTimeLabel() {
|
||||
const hasSlottedTimeLabel = this.el.querySelector('[slot="time-label"]') !== null;
|
||||
if (!hasSlottedTimeLabel && !this.showDefaultTimeLabel) {
|
||||
@ -1528,114 +1869,8 @@ export class Datetime implements ComponentInterface {
|
||||
return <slot name="time-label">Time</slot>;
|
||||
}
|
||||
|
||||
private renderTimePicker(
|
||||
hoursItems: PickerColumnItem[],
|
||||
minutesItems: PickerColumnItem[],
|
||||
ampmItems: PickerColumnItem[],
|
||||
use24Hour: boolean
|
||||
) {
|
||||
return (
|
||||
<ion-picker-internal>
|
||||
{this.renderHourPickerColumn(hoursItems)}
|
||||
{this.renderMinutePickerColumn(minutesItems)}
|
||||
{!use24Hour && this.renderDayPeriodPickerColumn(ampmItems)}
|
||||
</ion-picker-internal>
|
||||
);
|
||||
}
|
||||
|
||||
private renderHourPickerColumn(hoursItems: PickerColumnItem[]) {
|
||||
if (hoursItems.length === 0) return [];
|
||||
|
||||
const { color, activePartsClone, workingParts } = this;
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
color={color}
|
||||
value={activePartsClone.hour}
|
||||
items={hoursItems}
|
||||
numericInput
|
||||
onIonChange={(ev: CustomEvent) => {
|
||||
this.setWorkingParts({
|
||||
...workingParts,
|
||||
hour: ev.detail.value,
|
||||
});
|
||||
this.setActiveParts({
|
||||
...activePartsClone,
|
||||
hour: ev.detail.value,
|
||||
});
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
);
|
||||
}
|
||||
|
||||
private renderMinutePickerColumn(minutesItems: PickerColumnItem[]) {
|
||||
if (minutesItems.length === 0) return [];
|
||||
|
||||
const { color, activePartsClone, workingParts } = this;
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
color={color}
|
||||
value={activePartsClone.minute}
|
||||
items={minutesItems}
|
||||
numericInput
|
||||
onIonChange={(ev: CustomEvent) => {
|
||||
this.setWorkingParts({
|
||||
...workingParts,
|
||||
minute: ev.detail.value,
|
||||
});
|
||||
this.setActiveParts({
|
||||
...activePartsClone,
|
||||
minute: ev.detail.value,
|
||||
});
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
);
|
||||
}
|
||||
|
||||
private renderDayPeriodPickerColumn(dayPeriodItems: PickerColumnItem[]) {
|
||||
if (dayPeriodItems.length === 0) return [];
|
||||
|
||||
const { color, activePartsClone, workingParts, locale } = this;
|
||||
const isDayPeriodRTL = isLocaleDayPeriodRTL(locale);
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
style={isDayPeriodRTL ? { order: '-1' } : {}}
|
||||
color={color}
|
||||
value={activePartsClone.ampm}
|
||||
items={dayPeriodItems}
|
||||
onIonChange={(ev: CustomEvent) => {
|
||||
const hour = calculateHourFromAMPM(workingParts, ev.detail.value);
|
||||
|
||||
this.setWorkingParts({
|
||||
...workingParts,
|
||||
ampm: ev.detail.value,
|
||||
hour,
|
||||
});
|
||||
|
||||
this.setActiveParts({
|
||||
...activePartsClone,
|
||||
ampm: ev.detail.value,
|
||||
hour,
|
||||
});
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
);
|
||||
}
|
||||
|
||||
private renderTimeOverlay(
|
||||
hoursItems: PickerColumnItem[],
|
||||
minutesItems: PickerColumnItem[],
|
||||
ampmItems: PickerColumnItem[],
|
||||
use24Hour: boolean
|
||||
) {
|
||||
private renderTimeOverlay() {
|
||||
const use24Hour = is24Hour(this.locale, this.hourCycle);
|
||||
return [
|
||||
<div class="time-header">{this.renderTimeLabel()}</div>,
|
||||
<button
|
||||
@ -1694,69 +1929,10 @@ export class Datetime implements ComponentInterface {
|
||||
keyboardEvents
|
||||
ref={(el) => (this.popoverRef = el)}
|
||||
>
|
||||
{this.renderTimePicker(hoursItems, minutesItems, ampmItems, use24Hour)}
|
||||
{this.renderWheelPicker('time')}
|
||||
</ion-popover>,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render time picker inside of datetime.
|
||||
* Do not pass color prop to segment on
|
||||
* iOS mode. MD segment has been customized and
|
||||
* should take on the color prop, but iOS
|
||||
* should just be the default segment.
|
||||
*/
|
||||
private renderTime() {
|
||||
const { workingParts, presentation, locale } = this;
|
||||
const timeOnlyPresentation = presentation === 'time';
|
||||
const use24Hour = is24Hour(this.locale, this.hourCycle);
|
||||
const { hours, minutes, am, pm } = generateTime(
|
||||
workingParts,
|
||||
use24Hour ? 'h23' : 'h12',
|
||||
this.value ? this.minParts : undefined,
|
||||
this.value ? this.maxParts : undefined,
|
||||
this.parsedHourValues,
|
||||
this.parsedMinuteValues
|
||||
);
|
||||
|
||||
const hoursItems = hours.map((hour) => {
|
||||
return {
|
||||
text: getFormattedHour(hour, use24Hour),
|
||||
value: getInternalHourValue(hour, use24Hour, workingParts.ampm),
|
||||
};
|
||||
});
|
||||
|
||||
const minutesItems = minutes.map((minute) => {
|
||||
return {
|
||||
text: addTimePadding(minute),
|
||||
value: minute,
|
||||
};
|
||||
});
|
||||
|
||||
const ampmItems = [];
|
||||
if (am) {
|
||||
ampmItems.push({
|
||||
text: getLocalizedDayPeriod(locale, 'am'),
|
||||
value: 'am',
|
||||
});
|
||||
}
|
||||
|
||||
if (pm) {
|
||||
ampmItems.push({
|
||||
text: getLocalizedDayPeriod(locale, 'pm'),
|
||||
value: 'pm',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="datetime-time">
|
||||
{timeOnlyPresentation
|
||||
? this.renderTimePicker(hoursItems, minutesItems, ampmItems, use24Hour)
|
||||
: this.renderTimeOverlay(hoursItems, minutesItems, ampmItems, use24Hour)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderCalendarViewHeader(mode: Mode) {
|
||||
const hasSlottedTitle = this.el.querySelector('[slot="title"]') !== null;
|
||||
if (!hasSlottedTitle && !this.showDefaultTitle) {
|
||||
@ -1773,14 +1949,57 @@ export class Datetime implements ComponentInterface {
|
||||
);
|
||||
}
|
||||
|
||||
private renderDatetime(mode: Mode) {
|
||||
/**
|
||||
* Render time picker inside of datetime.
|
||||
* Do not pass color prop to segment on
|
||||
* iOS mode. MD segment has been customized and
|
||||
* should take on the color prop, but iOS
|
||||
* should just be the default segment.
|
||||
*/
|
||||
private renderTime() {
|
||||
const { presentation } = this;
|
||||
const timeOnlyPresentation = presentation === 'time';
|
||||
|
||||
return (
|
||||
<div class="datetime-time">{timeOnlyPresentation ? this.renderWheelPicker() : this.renderTimeOverlay()}</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the month/year picker that is
|
||||
* displayed on the calendar grid.
|
||||
* The .datetime-year class has additional
|
||||
* styles that let us show/hide the
|
||||
* picker when the user clicks on the
|
||||
* toggle in the calendar header.
|
||||
*/
|
||||
private renderCalendarViewMonthYearPicker() {
|
||||
return <div class="datetime-year">{this.renderWheelView('month-year')}</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render entry point
|
||||
* All presentation types are rendered from here.
|
||||
*/
|
||||
|
||||
private renderDatetime(mode: Mode) {
|
||||
const { presentation, preferWheel } = this;
|
||||
|
||||
/**
|
||||
* Certain presentation types have separate grid and wheel displays.
|
||||
* If preferWheel is true then we should show a wheel picker instead.
|
||||
*/
|
||||
const hasWheelVariant = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
|
||||
if (preferWheel && hasWheelVariant) {
|
||||
return [this.renderWheelView(), this.renderFooter()];
|
||||
}
|
||||
|
||||
switch (presentation) {
|
||||
case 'date-time':
|
||||
return [
|
||||
this.renderCalendarViewHeader(mode),
|
||||
this.renderCalendar(mode),
|
||||
this.renderYearView(),
|
||||
this.renderCalendarViewMonthYearPicker(),
|
||||
this.renderTime(),
|
||||
this.renderFooter(),
|
||||
];
|
||||
@ -1789,7 +2008,7 @@ export class Datetime implements ComponentInterface {
|
||||
this.renderCalendarViewHeader(mode),
|
||||
this.renderTime(),
|
||||
this.renderCalendar(mode),
|
||||
this.renderYearView(),
|
||||
this.renderCalendarViewMonthYearPicker(),
|
||||
this.renderFooter(),
|
||||
];
|
||||
case 'time':
|
||||
@ -1797,24 +2016,38 @@ export class Datetime implements ComponentInterface {
|
||||
case 'month':
|
||||
case 'month-year':
|
||||
case 'year':
|
||||
return [this.renderYearView(), this.renderFooter()];
|
||||
return [this.renderWheelView(), this.renderFooter()];
|
||||
default:
|
||||
return [
|
||||
this.renderCalendarViewHeader(mode),
|
||||
this.renderCalendar(mode),
|
||||
this.renderYearView(),
|
||||
this.renderCalendarViewMonthYearPicker(),
|
||||
this.renderFooter(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { name, value, disabled, el, color, isPresented, readonly, showMonthAndYear, presentation, size } = this;
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
disabled,
|
||||
el,
|
||||
color,
|
||||
isPresented,
|
||||
readonly,
|
||||
showMonthAndYear,
|
||||
preferWheel,
|
||||
presentation,
|
||||
size,
|
||||
} = this;
|
||||
const mode = getIonMode(this);
|
||||
const isMonthAndYearPresentation =
|
||||
presentation === 'year' || presentation === 'month' || presentation === 'month-year';
|
||||
const shouldShowMonthAndYear = showMonthAndYear || isMonthAndYearPresentation;
|
||||
const monthYearPickerOpen = showMonthAndYear && !isMonthAndYearPresentation;
|
||||
const hasWheelVariant =
|
||||
(presentation === 'date' || presentation === 'date-time' || presentation === 'time-date') && preferWheel;
|
||||
|
||||
renderHiddenInput(true, el, name, value, disabled);
|
||||
|
||||
@ -1833,6 +2066,7 @@ export class Datetime implements ComponentInterface {
|
||||
'month-year-picker-open': monthYearPickerOpen,
|
||||
[`datetime-presentation-${presentation}`]: true,
|
||||
[`datetime-size-${size}`]: true,
|
||||
[`datetime-prefer-wheel`]: hasWheelVariant,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
|
290
core/src/components/datetime/test/prefer-wheel/datetime.e2e.ts
Normal file
@ -0,0 +1,290 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('datetime: prefer wheel', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.setViewportSize({
|
||||
width: 400,
|
||||
height: 200,
|
||||
});
|
||||
});
|
||||
/**
|
||||
* When taking screenshots, be sure to
|
||||
* set the datetime to size="cover". There
|
||||
* are rendering quirks on Linux
|
||||
* if the datetime is too small.
|
||||
*/
|
||||
test.describe('datetime: date wheel rendering', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime size="cover" presentation="date" prefer-wheel="true" value="2019-05-30"></ion-datetime>
|
||||
`);
|
||||
|
||||
expect(await page.screenshot()).toMatchSnapshot(`datetime-wheel-date-diff-${page.getSnapshotSettings()}.png`);
|
||||
});
|
||||
test('should respect the min bounds', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime presentation="date" prefer-wheel="true" min="2019-05-05" max="2023-10-01" value="2019-05-30"></ion-datetime>
|
||||
`);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const dayValues = page.locator('ion-datetime .day-column .picker-item[data-value]');
|
||||
expect(await dayValues.count()).toEqual(27);
|
||||
});
|
||||
test('should respect the max bounds', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime presentation="date" prefer-wheel="true" min="2019-05-05" max="2023-10-01" value="2023-10-01"></ion-datetime>
|
||||
`);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const dayValues = page.locator('ion-datetime .day-column .picker-item[data-value]');
|
||||
expect(await dayValues.count()).toEqual(1);
|
||||
});
|
||||
test('should respect isDateEnabled preference', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime presentation="date" prefer-wheel="true" value="2022-01-01"></ion-datetime>
|
||||
<script>
|
||||
const datetime = document.querySelector('ion-datetime');
|
||||
datetime.isDateEnabled = (dateIsoString) => {
|
||||
const date = new Date(dateIsoString);
|
||||
if (date.getUTCDate() % 2 === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
`);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const disabledMonths = page.locator('.month-column .picker-item[disabled]');
|
||||
const disabledYears = page.locator('.year-column .picker-item[disabled]');
|
||||
const disabledDays = page.locator('.day-column .picker-item[disabled]');
|
||||
|
||||
expect(await disabledMonths.count()).toBe(0);
|
||||
expect(await disabledYears.count()).toBe(0);
|
||||
expect(await disabledDays.count()).toBe(15);
|
||||
});
|
||||
test('should respect month, day, and year preferences', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime
|
||||
presentation="date"
|
||||
prefer-wheel="true"
|
||||
value="2022-01-01"
|
||||
month-values="4,6"
|
||||
day-values="1,2,3,4,5"
|
||||
year-values="2022,2020,2019"
|
||||
></ion-datetime>
|
||||
`);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const monthValues = page.locator('.month-column .picker-item:not(.picker-item-empty)');
|
||||
const yearValues = page.locator('.year-column .picker-item:not(.picker-item-empty)');
|
||||
const dayValues = page.locator('.day-column .picker-item:not(.picker-item-empty)');
|
||||
|
||||
expect(await monthValues.count()).toBe(2);
|
||||
expect(await yearValues.count()).toBe(3);
|
||||
expect(await dayValues.count()).toBe(5);
|
||||
});
|
||||
test('should correctly localize the date data', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime
|
||||
presentation="date"
|
||||
prefer-wheel="true"
|
||||
locale="ja-JP"
|
||||
min="2022-01-01"
|
||||
max="2022-03-01"
|
||||
day-values="1,2,3"
|
||||
value="2022-01-01"
|
||||
></ion-datetime>
|
||||
`);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const monthValues = page.locator('.month-column .picker-item:not(.picker-item-empty)');
|
||||
const dayValues = page.locator('.day-column .picker-item:not(.picker-item-empty)');
|
||||
|
||||
expect(monthValues).toHaveText(['1月', '2月', '3月']);
|
||||
expect(dayValues).toHaveText(['1日', '2日', '3日']);
|
||||
});
|
||||
});
|
||||
test.describe('datetime: date-time wheel rendering', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime size="cover" presentation="date-time" prefer-wheel="true" value="2019-05-30T16:30:00"></ion-datetime>
|
||||
`);
|
||||
|
||||
expect(await page.screenshot()).toMatchSnapshot(
|
||||
`datetime-wheel-date-time-diff-${page.getSnapshotSettings()}.png`
|
||||
);
|
||||
});
|
||||
test('should respect the min bounds', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime presentation="date-time" prefer-wheel="true" min="2019-05-05" value="2019-05-10T16:30:00"></ion-datetime>
|
||||
`);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const dayValues = page.locator('ion-datetime .date-column .picker-item[data-value]');
|
||||
expect(await dayValues.count()).toEqual(57);
|
||||
});
|
||||
test('should respect the max bounds', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime presentation="date-time" prefer-wheel="true" max="2023-06-10" value="2023-06-05T16:30:00"></ion-datetime>
|
||||
`);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const dayValues = page.locator('ion-datetime .date-column .picker-item[data-value]');
|
||||
expect(await dayValues.count()).toEqual(41);
|
||||
});
|
||||
test('should respect isDateEnabled preference', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime presentation="date-time" prefer-wheel="true" value="2022-02-01T16:30:00"></ion-datetime>
|
||||
<script>
|
||||
const datetime = document.querySelector('ion-datetime');
|
||||
datetime.isDateEnabled = (dateIsoString) => {
|
||||
const date = new Date(dateIsoString);
|
||||
if (date.getUTCDate() % 2 === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
`);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const disabledDates = page.locator('.date-column .picker-item[disabled]');
|
||||
|
||||
expect(await disabledDates.count()).toBe(44);
|
||||
});
|
||||
test('should respect month, day, and year preferences', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime
|
||||
presentation="date-time"
|
||||
prefer-wheel="true"
|
||||
value="2022-02-01"
|
||||
month-values="2"
|
||||
day-values="1,2,3,4,5"
|
||||
year-values="2022,2020,2019"
|
||||
></ion-datetime>
|
||||
`);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const dateValues = page.locator('.date-column .picker-item:not(.picker-item-empty)');
|
||||
|
||||
expect(await dateValues.count()).toBe(5);
|
||||
});
|
||||
test('should correctly localize the date data', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime
|
||||
presentation="date-time"
|
||||
prefer-wheel="true"
|
||||
locale="ja-JP"
|
||||
month-values="2"
|
||||
day-values="1,2,3"
|
||||
value="2022-02-01"
|
||||
></ion-datetime>
|
||||
`);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const dateValues = page.locator('.date-column .picker-item:not(.picker-item-empty)');
|
||||
|
||||
expect(dateValues).toHaveText(['2月1日(火)', '2月2日(水)', '2月3日(木)']);
|
||||
});
|
||||
});
|
||||
test.describe('datetime: time-date wheel rendering', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime size="cover" presentation="time-date" prefer-wheel="true" value="2019-05-30T16:30:00"></ion-datetime>
|
||||
`);
|
||||
|
||||
expect(await page.screenshot()).toMatchSnapshot(
|
||||
`datetime-wheel-time-date-diff-${page.getSnapshotSettings()}.png`
|
||||
);
|
||||
});
|
||||
test('should respect the min bounds', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime presentation="time-date" prefer-wheel="true" min="2019-05-05" value="2019-05-10T16:30:00"></ion-datetime>
|
||||
`);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const dayValues = page.locator('ion-datetime .date-column .picker-item[data-value]');
|
||||
expect(await dayValues.count()).toEqual(57);
|
||||
});
|
||||
test('should respect the max bounds', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime presentation="time-date" prefer-wheel="true" max="2023-06-10" value="2023-06-05T16:30:00"></ion-datetime>
|
||||
`);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const dayValues = page.locator('ion-datetime .date-column .picker-item[data-value]');
|
||||
expect(await dayValues.count()).toEqual(41);
|
||||
});
|
||||
test('should respect isDateEnabled preference', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime presentation="time-date" prefer-wheel="true" value="2022-02-01T16:30:00"></ion-datetime>
|
||||
<script>
|
||||
const datetime = document.querySelector('ion-datetime');
|
||||
datetime.isDateEnabled = (dateIsoString) => {
|
||||
const date = new Date(dateIsoString);
|
||||
if (date.getUTCDate() % 2 === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
`);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const disabledDates = page.locator('.date-column .picker-item[disabled]');
|
||||
|
||||
expect(await disabledDates.count()).toBe(44);
|
||||
});
|
||||
test('should respect month, day, and year preferences', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime
|
||||
presentation="time-date"
|
||||
prefer-wheel="true"
|
||||
value="2022-02-01"
|
||||
month-values="2"
|
||||
day-values="1,2,3,4,5"
|
||||
year-values="2022,2020,2019"
|
||||
></ion-datetime>
|
||||
`);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const dateValues = page.locator('.date-column .picker-item:not(.picker-item-empty)');
|
||||
|
||||
expect(await dateValues.count()).toBe(5);
|
||||
});
|
||||
test('should correctly localize the date data', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-datetime
|
||||
presentation="time-date"
|
||||
prefer-wheel="true"
|
||||
locale="ja-JP"
|
||||
month-values="2"
|
||||
day-values="1,2,3"
|
||||
value="2022-02-01"
|
||||
></ion-datetime>
|
||||
`);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const dateValues = page.locator('.date-column .picker-item:not(.picker-item-empty)');
|
||||
|
||||
expect(dateValues).toHaveText(['2月1日(火)', '2月2日(水)', '2月3日(木)']);
|
||||
});
|
||||
});
|
||||
});
|
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 47 KiB |
58
core/src/components/datetime/test/prefer-wheel/index.html
Normal file
@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Datetime - Prefer Wheel</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-content class="ion-padding">
|
||||
<label for="presentation-select">Presentation:</label>
|
||||
<select id="presentation-select">
|
||||
<option value="date">date</option>
|
||||
<option value="date-time">date-time</option>
|
||||
<option value="month">month</option>
|
||||
<option value="month-year">month-year</option>
|
||||
<option value="time">time</option>
|
||||
<option value="time-date">time-date</option>
|
||||
<option value="year">year</option>
|
||||
</select>
|
||||
<label for="locale-input">Locale:</label>
|
||||
<input type="text" id="locale-input" placeholder="en-US" />
|
||||
<ion-datetime
|
||||
min="2019-05-05"
|
||||
max="2023-10-01"
|
||||
value="2022-05-31T16:30:00"
|
||||
presentation="date"
|
||||
prefer-wheel="true"
|
||||
></ion-datetime>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
const datetime = document.querySelector('ion-datetime');
|
||||
const presentationSelect = document.querySelector('#presentation-select');
|
||||
const locaelInput = document.querySelector('#locale-input');
|
||||
|
||||
presentationSelect.onchange = (ev) => {
|
||||
datetime.presentation = ev.target.value;
|
||||
};
|
||||
locaelInput.onchange = (ev) => {
|
||||
datetime.locale = ev.target.value || 'en-US';
|
||||
};
|
||||
datetime.isDateEnabled = (dateIsoString) => {
|
||||
const date = new Date(dateIsoString);
|
||||
if (date.getUTCDate() % 2 === 0) {
|
||||
// Disables even dates
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,10 +1,11 @@
|
||||
import type { Mode } from '../../../interface';
|
||||
import type { PickerColumnItem } from '../../picker-column-internal/picker-column-internal-interfaces';
|
||||
import type { DatetimeParts } from '../datetime-interface';
|
||||
|
||||
import { isAfter, isBefore, isSameDay } from './comparison';
|
||||
import { removeDateTzOffset } from './format';
|
||||
import { getNumDaysInMonth } from './helpers';
|
||||
import { getNextMonth, getPreviousMonth } from './manipulation';
|
||||
import { getLocalizedDayPeriod, removeDateTzOffset, getFormattedHour, addTimePadding, getTodayLabel } from './format';
|
||||
import { getNumDaysInMonth, is24Hour } from './helpers';
|
||||
import { getNextMonth, getPreviousMonth, getInternalHourValue } from './manipulation';
|
||||
|
||||
/**
|
||||
* Returns the current date as
|
||||
@ -254,13 +255,16 @@ export const generateMonths = (refParts: DatetimeParts): DatetimeParts[] => {
|
||||
];
|
||||
};
|
||||
|
||||
export const getPickerMonths = (
|
||||
export const getMonthColumnData = (
|
||||
locale: string,
|
||||
refParts: DatetimeParts,
|
||||
minParts?: DatetimeParts,
|
||||
maxParts?: DatetimeParts,
|
||||
monthValues?: number[]
|
||||
) => {
|
||||
monthValues?: number[],
|
||||
formatOptions: Intl.DateTimeFormatOptions = {
|
||||
month: 'long',
|
||||
}
|
||||
): PickerColumnItem[] => {
|
||||
const { year } = refParts;
|
||||
const months = [];
|
||||
|
||||
@ -276,7 +280,7 @@ export const getPickerMonths = (
|
||||
processedMonths.forEach((processedMonth) => {
|
||||
const date = new Date(`${processedMonth}/1/${year} GMT+0000`);
|
||||
|
||||
const monthString = new Intl.DateTimeFormat(locale, { month: 'long', timeZone: 'UTC' }).format(date);
|
||||
const monthString = new Intl.DateTimeFormat(locale, { ...formatOptions, timeZone: 'UTC' }).format(date);
|
||||
months.push({ text: monthString, value: processedMonth });
|
||||
});
|
||||
} else {
|
||||
@ -310,7 +314,7 @@ export const getPickerMonths = (
|
||||
*/
|
||||
const date = new Date(`${i}/1/${year} GMT+0000`);
|
||||
|
||||
const monthString = new Intl.DateTimeFormat(locale, { month: 'long', timeZone: 'UTC' }).format(date);
|
||||
const monthString = new Intl.DateTimeFormat(locale, { ...formatOptions, timeZone: 'UTC' }).format(date);
|
||||
months.push({ text: monthString, value: i });
|
||||
}
|
||||
}
|
||||
@ -318,31 +322,222 @@ export const getPickerMonths = (
|
||||
return months;
|
||||
};
|
||||
|
||||
export const getCalendarYears = (
|
||||
/**
|
||||
* Returns information regarding
|
||||
* selectable dates (i.e 1st, 2nd, 3rd, etc)
|
||||
* within a reference month.
|
||||
* @param locale The locale to format the date with
|
||||
* @param refParts The reference month/year to generate dates for
|
||||
* @param minParts The minimum bound on the date that can be returned
|
||||
* @param maxParts The maximum bound on the date that can be returned
|
||||
* @param dayValues The allowed date values
|
||||
* @returns Date data to be used in ion-picker-column-internal
|
||||
*/
|
||||
export const getDayColumnData = (
|
||||
locale: string,
|
||||
refParts: DatetimeParts,
|
||||
minParts?: DatetimeParts,
|
||||
maxParts?: DatetimeParts,
|
||||
dayValues?: number[],
|
||||
formatOptions: Intl.DateTimeFormatOptions = {
|
||||
day: 'numeric',
|
||||
}
|
||||
): PickerColumnItem[] => {
|
||||
const { month, year } = refParts;
|
||||
const days = [];
|
||||
|
||||
/**
|
||||
* If we have max/min bounds that in the same
|
||||
* month/year as the refParts, we should
|
||||
* use the define day as the max/min day.
|
||||
* Otherwise, fallback to the max/min days in a month.
|
||||
*/
|
||||
const numDaysInMonth = getNumDaysInMonth(month, year);
|
||||
const maxDay = maxParts?.day && maxParts.year === year && maxParts.month === month ? maxParts.day : numDaysInMonth;
|
||||
const minDay = minParts?.day && minParts.year === year && minParts.month === month ? minParts.day : 1;
|
||||
|
||||
if (dayValues !== undefined) {
|
||||
let processedDays = dayValues;
|
||||
processedDays = processedDays.filter((day) => day >= minDay && day <= maxDay);
|
||||
processedDays.forEach((processedDay) => {
|
||||
const date = new Date(`${month}/${processedDay}/${year} GMT+0000`);
|
||||
|
||||
const dayString = new Intl.DateTimeFormat(locale, { ...formatOptions, timeZone: 'UTC' }).format(date);
|
||||
days.push({ text: dayString, value: processedDay });
|
||||
});
|
||||
} else {
|
||||
for (let i = minDay; i <= maxDay; i++) {
|
||||
const date = new Date(`${month}/${i}/${year} GMT+0000`);
|
||||
|
||||
const dayString = new Intl.DateTimeFormat(locale, { ...formatOptions, timeZone: 'UTC' }).format(date);
|
||||
days.push({ text: dayString, value: i });
|
||||
}
|
||||
}
|
||||
|
||||
return days;
|
||||
};
|
||||
|
||||
export const getYearColumnData = (
|
||||
refParts: DatetimeParts,
|
||||
minParts?: DatetimeParts,
|
||||
maxParts?: DatetimeParts,
|
||||
yearValues?: number[]
|
||||
) => {
|
||||
): PickerColumnItem[] => {
|
||||
let processedYears = [];
|
||||
if (yearValues !== undefined) {
|
||||
let processedYears = yearValues;
|
||||
processedYears = yearValues;
|
||||
if (maxParts?.year !== undefined) {
|
||||
processedYears = processedYears.filter((year) => year <= maxParts.year!);
|
||||
}
|
||||
if (minParts?.year !== undefined) {
|
||||
processedYears = processedYears.filter((year) => year >= minParts.year!);
|
||||
}
|
||||
return processedYears;
|
||||
} else {
|
||||
const { year } = refParts;
|
||||
const maxYear = maxParts?.year || year;
|
||||
const minYear = minParts?.year || year - 100;
|
||||
|
||||
const years = [];
|
||||
for (let i = maxYear; i >= minYear; i--) {
|
||||
years.push(i);
|
||||
processedYears.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
return years;
|
||||
}
|
||||
return processedYears.map((year) => ({
|
||||
text: `${year}`,
|
||||
value: year,
|
||||
}));
|
||||
};
|
||||
|
||||
interface CombinedDateColumnData {
|
||||
parts: DatetimeParts[];
|
||||
items: PickerColumnItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns picker items
|
||||
* that represent the days in a month.
|
||||
* Example: "Thu, Jun 2"
|
||||
*/
|
||||
export const getCombinedDateColumnData = (
|
||||
locale: string,
|
||||
refParts: DatetimeParts,
|
||||
todayParts: DatetimeParts,
|
||||
minParts?: DatetimeParts,
|
||||
maxParts?: DatetimeParts,
|
||||
dayValues?: number[],
|
||||
monthValues?: number[]
|
||||
): CombinedDateColumnData => {
|
||||
let items: PickerColumnItem[] = [];
|
||||
let parts: DatetimeParts[] = [];
|
||||
|
||||
// TODO(FW-1693) This does not work when the previous month is in the previous year.
|
||||
const months = getMonthColumnData(locale, refParts, minParts, maxParts, monthValues, { month: 'short' });
|
||||
|
||||
/**
|
||||
* Get all of the days in the month.
|
||||
* From there, generate an array where
|
||||
* each item has the month, date, and day
|
||||
* of work as the text.
|
||||
*/
|
||||
months.forEach((monthObject) => {
|
||||
const referenceMonth = { month: monthObject.value as number, day: null, year: refParts.year };
|
||||
const monthDays = getDayColumnData(locale, referenceMonth, minParts, maxParts, dayValues, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
weekday: 'short',
|
||||
});
|
||||
|
||||
const dateParts: DatetimeParts[] = [];
|
||||
const dateColumnItems: PickerColumnItem[] = [];
|
||||
|
||||
monthDays.forEach((dayObject) => {
|
||||
const isToday = isSameDay({ ...referenceMonth, day: dayObject.value as number }, todayParts);
|
||||
|
||||
/**
|
||||
* Today's date should read as "Today" (localized)
|
||||
* not the actual date string
|
||||
*/
|
||||
dateColumnItems.push({
|
||||
text: isToday ? getTodayLabel(locale) : dayObject.text,
|
||||
value: `${refParts.year}-${monthObject.value}-${dayObject.value}`,
|
||||
});
|
||||
|
||||
/**
|
||||
* When selecting a date in the wheel picker
|
||||
* we need access to the raw datetime parts data.
|
||||
* The picker column only accepts values of
|
||||
* type string or number, so we need to return
|
||||
* two sets of data: A data set to be passed
|
||||
* to the picker column, and a data set to
|
||||
* be used to reference the raw data when
|
||||
* updating the picker column value.
|
||||
*/
|
||||
dateParts.push({
|
||||
month: monthObject.value as number,
|
||||
year: refParts.year,
|
||||
day: dayObject.value as number,
|
||||
});
|
||||
});
|
||||
parts = [...parts, ...dateParts];
|
||||
items = [...items, ...dateColumnItems];
|
||||
});
|
||||
|
||||
return {
|
||||
parts,
|
||||
items,
|
||||
};
|
||||
};
|
||||
|
||||
export const getTimeColumnsData = (
|
||||
locale: string,
|
||||
refParts: DatetimeParts,
|
||||
hourCycle?: 'h23' | 'h12',
|
||||
minParts?: DatetimeParts,
|
||||
maxParts?: DatetimeParts,
|
||||
allowedHourValues?: number[],
|
||||
allowedMinuteVaues?: number[]
|
||||
): { [key: string]: PickerColumnItem[] } => {
|
||||
const use24Hour = is24Hour(locale, hourCycle);
|
||||
const { hours, minutes, am, pm } = generateTime(
|
||||
refParts,
|
||||
use24Hour ? 'h23' : 'h12',
|
||||
minParts,
|
||||
maxParts,
|
||||
allowedHourValues,
|
||||
allowedMinuteVaues
|
||||
);
|
||||
|
||||
const hoursItems = hours.map((hour) => {
|
||||
return {
|
||||
text: getFormattedHour(hour, use24Hour),
|
||||
value: getInternalHourValue(hour, use24Hour, refParts.ampm),
|
||||
};
|
||||
});
|
||||
const minutesItems = minutes.map((minute) => {
|
||||
return {
|
||||
text: addTimePadding(minute),
|
||||
value: minute,
|
||||
};
|
||||
});
|
||||
|
||||
const dayPeriodItems = [];
|
||||
if (am && !use24Hour) {
|
||||
dayPeriodItems.push({
|
||||
text: getLocalizedDayPeriod(locale, 'am'),
|
||||
value: 'am',
|
||||
});
|
||||
}
|
||||
|
||||
if (pm && !use24Hour) {
|
||||
dayPeriodItems.push({
|
||||
text: getLocalizedDayPeriod(locale, 'pm'),
|
||||
value: 'pm',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
minutesData: minutesItems,
|
||||
hoursData: hoursItems,
|
||||
dayPeriodData: dayPeriodItems,
|
||||
};
|
||||
};
|
||||
|
@ -100,6 +100,20 @@ export const getMonthAndYear = (locale: string, refParts: DatetimeParts) => {
|
||||
return new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric', timeZone: 'UTC' }).format(date);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a localized version of "Today"
|
||||
* Falls back to "Today" in English for
|
||||
* browsers that do not support RelativeTimeFormat.
|
||||
*/
|
||||
export const getTodayLabel = (locale: string) => {
|
||||
if ('RelativeTimeFormat' in Intl) {
|
||||
const label = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(0, 'day');
|
||||
return label.charAt(0).toUpperCase() + label.slice(1);
|
||||
} else {
|
||||
return 'Today';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* When calling toISOString(), the browser
|
||||
* will convert the date to UTC time by either adding
|
||||
|
@ -34,6 +34,9 @@
|
||||
}
|
||||
|
||||
:host .picker-item {
|
||||
@include padding(0);
|
||||
@include margin(0);
|
||||
|
||||
display: block;
|
||||
|
||||
width: 100%;
|
||||
@ -46,10 +49,14 @@
|
||||
|
||||
background: transparent;
|
||||
|
||||
color: inherit;
|
||||
|
||||
font-size: inherit;
|
||||
|
||||
line-height: 34px;
|
||||
|
||||
text-align: inherit;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
|
||||
white-space: nowrap;
|
||||
@ -68,6 +75,10 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:host .picker-item.picker-item-disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
:host(.picker-column-active) .picker-item.picker-item-active {
|
||||
color: current-color(base);
|
||||
}
|
||||
|
@ -2,6 +2,25 @@ import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('picker-column-internal: disabled', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal value="b"></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a', disabled: true },
|
||||
{ text: 'B', value: 'b' },
|
||||
{ text: 'C', value: 'c', disabled: true }
|
||||
]
|
||||
</script>
|
||||
`);
|
||||
|
||||
const picker = page.locator('ion-picker-internal');
|
||||
expect(await picker.screenshot()).toMatchSnapshot(`picker-internal-disabled-${page.getSnapshotSettings()}.png`);
|
||||
});
|
||||
test('all picker items should be enabled by default', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-picker-internal>
|
||||
|
After Width: | Height: | Size: 7.3 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 7.3 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 7.3 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 7.0 KiB |
After Width: | Height: | Size: 7.3 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 7.0 KiB |
@ -84,3 +84,7 @@
|
||||
:host ::slotted(ion-picker-column-internal:last-of-type) {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
:host ::slotted(ion-picker-column-internal:only-child) {
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -12,7 +12,8 @@
|
||||
"jsxFactory": "h",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2017"
|
||||
"es2017",
|
||||
"es2020"
|
||||
],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
|
@ -292,6 +292,7 @@ export const IonDatetime = /*@__PURE__*/ defineContainer<JSX.IonDatetime>('ion-d
|
||||
'showDefaultTimeLabel',
|
||||
'hourCycle',
|
||||
'size',
|
||||
'preferWheel',
|
||||
'ionCancel',
|
||||
'ionChange',
|
||||
'ionFocus',
|
||||
|