feat(datetime): add wheel style picker for dates and times (#25468)

This commit is contained in:
Liam DeBeasi
2022-06-14 16:35:47 -04:00
committed by GitHub
parent 6bc0acc427
commit 3d19771185
63 changed files with 1173 additions and 314 deletions

View File

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

View File

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

View File

@ -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.
*/

View File

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

View File

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

View File

@ -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,
}),
}}
>

View 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日(木)']);
});
});
});

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,8 @@
"jsxFactory": "h",
"lib": [
"dom",
"es2017"
"es2017",
"es2020"
],
"module": "esnext",
"moduleResolution": "node",

View File

@ -292,6 +292,7 @@ export const IonDatetime = /*@__PURE__*/ defineContainer<JSX.IonDatetime>('ion-d
'showDefaultTimeLabel',
'hourCycle',
'size',
'preferWheel',
'ionCancel',
'ionChange',
'ionFocus',