fix(datetime): time picker uses new iOS 15 style (#23996)

resolves #23768
This commit is contained in:
Liam DeBeasi
2021-10-05 09:14:52 -04:00
committed by GitHub
parent c20408369b
commit 0ab37b5061
14 changed files with 336 additions and 778 deletions

View File

@ -139,57 +139,12 @@
@include padding($datetime-ios-padding / 2, $datetime-ios-padding, $datetime-ios-padding, $datetime-ios-padding);
font-size: 16px;
}
:host .datetime-time .time-header {
font-weight: 600;
}
:host .time-base {
@include border-radius($datetime-ios-time-border-radius, $datetime-ios-time-border-radius, $datetime-ios-time-border-radius, $datetime-ios-time-border-radius);
@include margin(0, $datetime-ios-padding / 2, 0, 0);
width: $datetime-ios-time-width;
height: $datetime-ios-time-height;
}
:host .time-column {
@include border-radius($datetime-ios-time-border-radius, $datetime-ios-time-border-radius, $datetime-ios-time-border-radius, $datetime-ios-time-border-radius);
}
:host .time-item {
line-height: $datetime-ios-time-height;
}
// Month and Year Picker
// -----------------------------------
:host .datetime-year-body .datetime-picker-col {
@include padding(0, $datetime-ios-padding, 0, $datetime-ios-padding);
}
:host .datetime-picker-before {
background: linear-gradient(to bottom, var(--background, var(--ion-background-color, #fff)) 20%, rgba(var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255)), 0.8) 100%);
}
:host .datetime-picker-after {
background: linear-gradient(to top, var(--background, var(--ion-background-color, #fff)) 30%, rgba(var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255)), 0.8) 100%);
}
:host .datetime-picker-highlight {
@include border-radius($datetime-ios-time-border-radius, $datetime-ios-time-border-radius, $datetime-ios-time-border-radius, $datetime-ios-time-border-radius);
@include position(50%, 0, 0, 0);
@include margin(0, auto, 0, auto);
position: absolute;
width: calc(100% - #{$datetime-ios-padding * 2});
height: 34px;
transform: translateY(-50%);
background: var(--ion-color-step-150, #eeeeef);
z-index: -1;
}
// Footer
// -----------------------------------
:host .datetime-buttons {

View File

@ -117,57 +117,8 @@
color: #{$text-color-step-350};
}
:host .time-base {
@include border-radius($datetime-md-time-border-radius, $datetime-md-time-border-radius, $datetime-md-time-border-radius, $datetime-md-time-border-radius);
@include margin(0, $datetime-md-padding / 2, 0, 0);
width: $datetime-md-time-width;
height: $datetime-md-time-height;
}
:host .time-column {
@include border-radius($datetime-md-time-border-radius, $datetime-md-time-border-radius, $datetime-md-time-border-radius, $datetime-md-time-border-radius);
}
:host .time-item {
line-height: $datetime-md-time-height;
}
:host .time-ampm ion-segment {
@include border-radius($datetime-md-time-border-radius, $datetime-md-time-border-radius, $datetime-md-time-border-radius, $datetime-md-time-border-radius);
border: 1px solid rgba($text-color-rgb, 0.1);
}
:host .time-ampm ion-segment-button {
--indicator-height: 0px;
--background-checked: #{current-color(base, 0.1)};
min-height: $datetime-md-time-height + 2;
}
:host .time-ampm ion-segment-button.segment-button-checked {
background: var(--background-checked);
}
// Month and Year
// -----------------------------------
:host .datetime-picker-col {
@include border-radius($datetime-md-wheel-border-radius, $datetime-md-wheel-border-radius, $datetime-md-wheel-border-radius, $datetime-md-wheel-border-radius);
@include padding(null, $datetime-md-wheel-padding, null, $datetime-md-wheel-padding);
}
:host .picker-col-item {
font-size: 18px;
}
:host .datetime-picker-before {
background: linear-gradient(to bottom, var(--background, var(--ion-background-color, #fff)) 20%, rgba(var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255)), 0) 90%);
}
:host .datetime-picker-after {
background: linear-gradient(to top, var(--background, var(--ion-background-color, #fff)) 30%, rgba(var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255)), 0) 90%);
}
/**
* Add some margin when only selecting month/year
@ -194,7 +145,3 @@
:host .datetime-view-buttons ion-button {
color: $text-color-step-200;
}
:host .picker-col-item-active {
color: current-color(base);
}

View File

@ -15,18 +15,3 @@ $datetime-md-header-padding: 20px !default;
/// @prop - Padding for content
$datetime-md-padding: 16px !default;
/// @prop - Height of the time picker
$datetime-md-time-height: 28px !default;
/// @prop - Width of the time picker
$datetime-md-time-width: 68px !default;
/// @prop - Border radius of the time picker
$datetime-md-time-border-radius: 4px !default;
/// @prop - Border radius of the month and year wheel
$datetime-md-wheel-border-radius: 8px !default;
/// @prop - Padding of the month and year wheel
$datetime-md-wheel-padding: 8px !default;

View File

@ -35,7 +35,6 @@
}
:host .calendar-body,
:host .time-column,
:host .datetime-year {
opacity: 0;
}
@ -45,8 +44,7 @@
pointer-events: none;
}
:host(.datetime-ready) .calendar-body,
:host(.datetime-ready) .time-column {
:host(.datetime-ready) .calendar-body {
opacity: 1;
}
@ -289,93 +287,12 @@
justify-content: space-between;
}
:host .time-base {
display: flex;
align-items: center;
justify-content: center;
border: 2px solid transparent;
background: rgba($text-color-rgb, 0.065);
font-size: 22px;
font-weight: 400;
text-align: center;
overflow-y: hidden;
:host(.datetime-presentation-time) .datetime-time {
@include padding(0);
}
:host .time-base.time-base-active {
border: 2px solid current-color(base);
}
:host .time-wrapper {
display: flex;
align-items: center;
justify-content: flex-end;
height: 100%;
}
:host .time-column {
position: relative;
height: 100%;
outline: none;
scroll-snap-type: y mandatory;
overflow-y: scroll;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
@media (any-hover: hover) {
:host .time-column:focus {
outline: none;
background: current-color(base, 0.2);
}
}
:host .time-column.time-column-active {
background: transparent;
color: current-color(base);
}
:host .time-base.time-base-active .time-column:not(.time-column-active),
:host .time-base.time-base-active .time-separator {
pointer-events: none;
opacity: 0.4;
}
:host .time-column::-webkit-scrollbar {
display: none;
}
:host .time-column-hours .time-item {
text-align: end;
}
:host .time-column-minutes .time-item {
text-align: start;
}
:host .time-item {
scroll-snap-align: center;
height: 100%;
}
:host .time-separator {
height: 100%;
:host ion-popover {
--height: 200px;
}
:host .time-header {
@ -385,15 +302,27 @@
}
:host .time-body {
@include border-radius(8px);
@include padding(6px, 12px, 6px, 12px);
display: flex;
border: none;
background: var(--ion-color-step-300, #edeef0);
color: $text-color;
font-family: inherit;
font-size: inherit;
cursor: pointer;
appearance: none;
}
:host .time-ampm {
width: 100px;
}
:host .time-ampm ion-segment-button {
min-width: 50px;
:host .time-body-active {
color: current-color(base);
}
:host(.in-item) {
@ -405,119 +334,3 @@
:host(.show-month-and-year) .calendar-action-buttons ion-item {
--color: #{current-color(base)};
}
:host .datetime-year-body .datetime-picker-col {
@include margin(0, 10px, 0, 10px);
}
:host .datetime-picker-before {
@include position(0, null, null, 0);
position: absolute;
width: 100%;
height: 82px;
background: linear-gradient(to bottom, var(--background, var(--ion-background-color, #fff)) 20%, rgba(var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255)), 0.7) 100%);
z-index: 10;
pointer-events: none;
}
:host .datetime-picker-after {
@include position(116px, null, null, 0);
position: absolute;
width: 100%;
height: 115px;
background: linear-gradient(to top, var(--background, var(--ion-background-color, #fff)) 30%, rgba(var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255)), 0.7) 100%);
z-index: 10;
pointer-events: none;
}
:host .datetime-year-body {
display: flex;
position: relative;
align-items: center;
justify-content: center;
font-size: 22px;
/**
* This is required otherwise the
* highlight will appear behind
* the datetime.
*/
z-index: 0;
}
:host .datetime-picker-col {
scroll-snap-type: y mandatory;
/**
* Need to explicitly set overflow-x: hidden
* for older implementations of scroll snapping.
*/
overflow-x: hidden;
overflow-y: scroll;
// Hide scrollbars on Firefox
scrollbar-width: none;
height: 200px;
outline: none;
}
@media (any-hover: hover) {
:host .datetime-picker-col:focus {
background: current-color(base, 0.2);
}
}
/**
* Hide scrollbars on Chrome and Safari
*/
:host .datetime-picker-col::-webkit-scrollbar {
display: none;
}
:host .picker-col-item {
height: 38px;
line-height: 38px;
scroll-snap-align: center;
}
:host .picker-col-item-empty {
scroll-snap-align: none;
}
:host .datetime-year-body .datetime-picker-col:first-of-type {
text-align: left;
}
:host .datetime-year-body .datetime-picker-col:last-of-type {
text-align: right;
}
/**
* Adding :last-of-type is needed here so that
* we can achieve higher specificity than the
* previous selectors and avoid using !important.
*/
:host(.datetime-presentation-year) .datetime-picker-col:last-of-type,
:host(.datetime-presentation-month) .datetime-picker-col:last-of-type {
text-align: center;
}

View File

@ -10,8 +10,9 @@ import {
import { getIonMode } from '../../global/ionic-global';
import { Color, DatetimeChangeEventDetail, DatetimeParts, Mode, StyleEventDetail } from '../../interface';
import { startFocusVisible } from '../../utils/focus-visible';
import { getElementRoot, raf, renderHiddenInput } from '../../utils/helpers';
import { getElementRoot, renderHiddenInput } from '../../utils/helpers';
import { createColorClasses } from '../../utils/theme';
import { PickerColumnItem } from '../picker-column-internal/picker-column-internal-interfaces';
import {
generateMonths,
@ -25,6 +26,7 @@ import {
import {
addTimePadding,
getFormattedHour,
getFormattedTime,
getMonthAndDay,
getMonthAndYear
} from './utils/format';
@ -75,11 +77,7 @@ export class Datetime implements ComponentInterface {
private inputId = `ion-dt-${datetimeIds++}`;
private calendarBodyRef?: HTMLElement;
private timeBaseRef?: HTMLElement;
private timeHourRef?: HTMLElement;
private timeMinuteRef?: HTMLElement;
private monthRef?: HTMLElement;
private yearRef?: HTMLElement;
private popoverRef?: HTMLIonPopoverElement;
private clearFocusVisible?: () => void;
private overlayIsPresenting = false;
@ -91,8 +89,6 @@ export class Datetime implements ComponentInterface {
private destroyCalendarIO?: () => void;
private destroyKeyboardMO?: () => void;
private destroyTimeScroll?: () => void;
private destroyMonthAndYearScroll?: () => void;
private minParts?: any;
private maxParts?: any;
@ -122,6 +118,7 @@ export class Datetime implements ComponentInterface {
@Element() el!: HTMLIonDatetimeElement;
@State() isPresented = false;
@State() isTimePopoverOpen = false;
/**
* The color to use from your application's color palette.
@ -807,7 +804,7 @@ export class Datetime implements ComponentInterface {
* if the datetime has been hidden/presented by a modal or popover.
*/
private destroyListeners = () => {
const { destroyCalendarIO, destroyKeyboardMO, destroyTimeScroll, destroyMonthAndYearScroll } = this;
const { destroyCalendarIO, destroyKeyboardMO } = this;
if (destroyCalendarIO !== undefined) {
destroyCalendarIO();
@ -816,14 +813,6 @@ export class Datetime implements ComponentInterface {
if (destroyKeyboardMO !== undefined) {
destroyKeyboardMO();
}
if (destroyTimeScroll !== undefined) {
destroyTimeScroll();
}
if (destroyMonthAndYearScroll !== undefined) {
destroyMonthAndYearScroll();
}
}
componentDidLoad() {
@ -841,9 +830,7 @@ export class Datetime implements ComponentInterface {
this.initializeCalendarIOListeners();
this.initializeKeyboardListeners();
this.initializeTimeScrollListener();
this.initializeOverlayListener();
this.initializeMonthAndYearScrollListeners();
/**
* TODO: Datetime needs a frame to ensure that it
@ -911,253 +898,6 @@ export class Datetime implements ComponentInterface {
});
}
private initializeMonthAndYearScrollListeners = () => {
const { monthRef, yearRef, workingParts } = this;
const { year, month } = workingParts;
/**
* Scroll initial month and year into view.
* scrollIntoView() will scroll entire page
* if element is not in viewport. Use scrollTop instead.
*/
let activeYearEl = yearRef?.querySelector(`.picker-col-item[data-value="${year}"]`) as HTMLElement | null;
if (activeYearEl) {
yearRef!.scrollTop = activeYearEl.offsetTop - (activeYearEl.clientHeight * 2);
activeYearEl.classList.add(PICKER_COL_ACTIVE);
}
let activeMonthEl = monthRef?.querySelector(`.picker-col-item[data-value="${month}"]`) as HTMLElement | null;
if (activeMonthEl) {
monthRef!.scrollTop = activeMonthEl.offsetTop - (activeMonthEl.clientHeight * 2);
activeMonthEl.classList.add(PICKER_COL_ACTIVE)
}
let timeout: any;
const scrollCallback = (colType: string) => {
raf(() => {
if (timeout) {
clearTimeout(timeout);
timeout = undefined;
}
const activeCol = colType === 'month' ? monthRef : yearRef;
if (!activeCol) { return; }
const bbox = activeCol.getBoundingClientRect();
/**
* Select item in the center of the column
* which is the month/year that we want to select
*/
const centerX = bbox.x + (bbox.width / 2);
const centerY = bbox.y + (bbox.height / 2);
const activeElement = this.el!.shadowRoot!.elementFromPoint(centerX, centerY) as HTMLElement;
const prevActiveEl = colType === 'month' ? activeMonthEl : activeYearEl;
if (prevActiveEl !== null) {
prevActiveEl.classList.remove(PICKER_COL_ACTIVE);
}
if (colType === 'month') {
activeMonthEl = activeElement;
} else if (colType === 'year') {
activeYearEl = activeElement;
}
activeElement.classList.add(PICKER_COL_ACTIVE);
timeout = setTimeout(() => {
const dataValue = activeElement.getAttribute('data-value');
/**
* If no value it is
* possible we hit one of the
* empty padding columns.
*/
if (dataValue === null) {
return;
}
const value = parseInt(dataValue, 10);
const { presentation } = this;
if (colType === 'month') {
this.setWorkingParts({
...this.workingParts,
month: value
});
/**
* If developers are only selecting month/month-year
* then we need to call ionChange as they will
* not be selecting dates too.
*/
if (presentation === 'month' || presentation === 'month-year') {
this.setActiveParts({
...this.activeParts,
month: value
});
}
} else {
this.setWorkingParts({
...this.workingParts,
year: value
});
if (presentation === 'year' || presentation === 'month-year') {
this.setActiveParts({
...this.activeParts,
year: value
});
}
}
/**
* If the year changed, it is possible that
* the allowed month values have changed and the scroll
* position got reset
*/
raf(() => {
const { month: workingMonth, year: workingYear } = this.workingParts;
const monthEl = monthRef?.querySelector(`.picker-col-item[data-value='${workingMonth}']`);
const yearEl = yearRef?.querySelector(`.picker-col-item[data-value='${workingYear}']`);
if (monthEl && monthRef) {
this.centerPickerItemInView(monthEl as HTMLElement, monthRef, 'auto');
}
if (yearEl && yearRef) {
this.centerPickerItemInView(yearEl as HTMLElement, yearRef, 'auto');
}
});
}, 250);
})
}
/**
* Add scroll listeners to the month and year containers.
* Wrap this in an raf so that the scroll callback
* does not fire when we do our initial scrollIntoView above.
*/
raf(() => {
const monthScroll = () => scrollCallback('month');
const yearScroll = () => scrollCallback('year');
monthRef?.addEventListener('scroll', monthScroll);
yearRef?.addEventListener('scroll', yearScroll);
this.destroyMonthAndYearScroll = () => {
monthRef?.removeEventListener('scroll', monthScroll);
yearRef?.removeEventListener('scroll', yearScroll);
}
});
}
private initializeTimeScrollListener = () => {
const { timeBaseRef, timeHourRef, timeMinuteRef } = this;
if (!timeBaseRef || !timeHourRef || !timeMinuteRef) { return; }
const { hour, minute } = this.workingParts;
/**
* Scroll initial hour and minute into view.
* scrollIntoView() will scroll entire page
* if element is not in viewport. Use scrollTop instead.
*/
raf(() => {
const initialHour = timeHourRef.querySelector(`.time-item[data-value="${hour}"]`) as HTMLElement | null;
if (initialHour) {
timeHourRef.scrollTop = initialHour.offsetTop;
}
const initialMinute = timeMinuteRef.querySelector(`.time-item[data-value="${minute}"]`) as HTMLElement | null;
if (initialMinute) {
timeMinuteRef.scrollTop = initialMinute.offsetTop;
}
/**
* Highlight the container and
* appropriate column when scrolling.
*/
let timeout: any;
const scrollCallback = (colType: string) => {
raf(() => {
if (timeout) {
clearTimeout(timeout);
timeout = undefined;
}
const activeCol = colType === 'hour' ? timeHourRef : timeMinuteRef;
const otherCol = colType === 'hour' ? timeMinuteRef : timeHourRef;
timeBaseRef.classList.add('time-base-active');
activeCol.classList.add('time-column-active');
timeout = setTimeout(() => {
timeBaseRef.classList.remove('time-base-active');
activeCol.classList.remove('time-column-active');
otherCol.classList.remove('time-column-active');
const bbox = activeCol.getBoundingClientRect();
/**
* Do not use floating point
* here as some browsers may clamp
* or round down.
*/
const x = Math.ceil(bbox.x + 1);
const y = Math.ceil(bbox.y + 1);
const activeElement = this.el!.shadowRoot!.elementFromPoint(x, y)!;
const value = parseInt(activeElement.getAttribute('data-value')!, 10);
/**
* When scrolling to a month that is out of
* bounds, the hour/minute column values may
* be updated, triggering a scroll callback.
* Check to make sure there is a valid
* hour/minute element so we do not emit NaN.
*/
if (Number.isNaN(value)) {
return;
}
if (colType === 'hour') {
this.setWorkingParts({
...this.workingParts,
hour: value
});
this.setActiveParts({
...this.activeParts,
hour: value
});
} else {
this.setWorkingParts({
...this.workingParts,
minute: value
});
this.setActiveParts({
...this.activeParts,
minute: value
});
}
}, 250);
});
}
/**
* Add scroll listeners to the hour and minute containers.
* Wrap this in an raf so that the scroll callback
* does not fire when we do our initial scrollIntoView above.
*/
raf(() => {
const hourScroll = () => scrollCallback('hour');
const minuteScroll = () => scrollCallback('minute');
timeHourRef.addEventListener('scroll', hourScroll);
timeMinuteRef.addEventListener('scroll', minuteScroll);
this.destroyTimeScroll = () => {
timeHourRef.removeEventListener('scroll', hourScroll);
timeMinuteRef.removeEventListener('scroll', minuteScroll);
}
});
});
}
private processValue = (value?: string | null) => {
const valueToProcess = value || getToday();
const { month, day, year, hour, minute, tzOffset } = parseDate(valueToProcess);
@ -1281,61 +1021,70 @@ export class Datetime implements ComponentInterface {
this.showMonthAndYear = !this.showMonthAndYear;
}
private centerPickerItemInView(target: HTMLElement, container: HTMLElement, behavior: ScrollBehavior = 'smooth') {
container.scroll({
// (Vertical offset from parent) - (three empty picker rows) + (half the height of the target to ensure the scroll triggers)
top: target.offsetTop - (3 * target.clientHeight) + (target.clientHeight / 2),
left: 0,
behavior
});
}
private renderYearView() {
const { presentation } = this;
const { presentation, workingParts } = this;
const calendarYears = getCalendarYears(this.todayParts, this.minParts, this.maxParts, this.parsedYearValues);
const showMonth = presentation !== 'year';
const showYear = presentation !== 'month';
const months = getPickerMonths(this.locale, workingParts, this.minParts, this.maxParts, this.parsedMonthValues);
const years = calendarYears.map(year => {
return {
text: `${year}`,
value: year
}
})
return (
<div class="datetime-year">
<div class="datetime-year-body">
<div class="datetime-picker-before"></div>
<div class="datetime-picker-after"></div>
<div class="datetime-picker-highlight"></div>
{showMonth && <div class="datetime-picker-col month-col" ref={el => this.monthRef = el} tabindex="0">
<div class="picker-col-item picker-col-item-empty">&nbsp;</div>
<div class="picker-col-item picker-col-item-empty">&nbsp;</div>
<div class="picker-col-item picker-col-item-empty">&nbsp;</div>
{getPickerMonths(this.locale, this.workingParts, this.minParts, this.maxParts, this.parsedMonthValues).map(month => {
return (
<div
class="picker-col-item"
data-value={month.value}
onClick={(ev: Event) => this.centerPickerItemInView(ev.target as HTMLElement, this.monthRef as HTMLElement)}
>{month.text}</div>
)
})}
<div class="picker-col-item picker-col-item-empty">&nbsp;</div>
<div class="picker-col-item picker-col-item-empty">&nbsp;</div>
<div class="picker-col-item picker-col-item-empty">&nbsp;</div>
</div>}
{showYear && <div class="datetime-picker-col year-col" ref={el => this.yearRef = el} tabindex="0">
<div class="picker-col-item picker-col-item-empty">&nbsp;</div>
<div class="picker-col-item picker-col-item-empty">&nbsp;</div>
<div class="picker-col-item picker-col-item-empty">&nbsp;</div>
{calendarYears.map(year => {
return (
<div
class="picker-col-item"
data-value={year}
onClick={(ev: Event) => this.centerPickerItemInView(ev.target as HTMLElement, this.yearRef as HTMLElement)}
>{year}</div>
)
})}
<div class="picker-col-item picker-col-item-empty">&nbsp;</div>
<div class="picker-col-item picker-col-item-empty">&nbsp;</div>
<div class="picker-col-item picker-col-item-empty">&nbsp;</div>
</div>}
<ion-picker-internal>
{
showMonth &&
<ion-picker-column-internal
color={this.color}
items={months}
value={workingParts.month}
onIonChange={(ev: CustomEvent) => {
this.setWorkingParts({
...this.workingParts,
month: ev.detail.value
});
if (presentation === 'month' || presentation === 'month-year') {
this.setActiveParts({
...this.activeParts,
month: ev.detail.value
});
}
ev.stopPropagation();
}}
></ion-picker-column-internal>
}
{
showYear &&
<ion-picker-column-internal
color={this.color}
items={years}
value={workingParts.year}
onIonChange={(ev: CustomEvent) => {
this.setWorkingParts({
...this.workingParts,
year: ev.detail.value
});
if (presentation === 'year' || presentation === 'month-year') {
this.setActiveParts({
...this.activeParts,
year: ev.detail.value
});
}
ev.stopPropagation();
}}
></ion-picker-column-internal>
}
</ion-picker-internal>
</div>
</div>
);
@ -1457,6 +1206,125 @@ export class Datetime implements ComponentInterface {
);
}
private renderTimePicker(
hoursItems: PickerColumnItem[],
minutesItems: PickerColumnItem[],
ampmItems: PickerColumnItem[],
use24Hour: boolean
) {
const { color, workingParts } = this;
return (
<ion-picker-internal>
<ion-picker-column-internal
color={color}
value={workingParts.hour}
items={hoursItems}
numericInput
onIonChange={(ev: CustomEvent) => {
this.setWorkingParts({
...this.workingParts,
hour: ev.detail.value
});
this.setActiveParts({
...this.activeParts,
hour: ev.detail.value
});
ev.stopPropagation();
}}
></ion-picker-column-internal>
<ion-picker-column-internal
color={color}
value={workingParts.minute}
items={minutesItems}
numericInput
onIonChange={(ev: CustomEvent) => {
this.setWorkingParts({
...this.workingParts,
minute: ev.detail.value
});
this.setActiveParts({
...this.activeParts,
minute: ev.detail.value
});
ev.stopPropagation();
}}
></ion-picker-column-internal>
{ !use24Hour && <ion-picker-column-internal
color={color}
value={workingParts.ampm}
items={ampmItems}
onIonChange={(ev: CustomEvent) => {
const hour = calculateHourFromAMPM(this.workingParts, ev.detail.value);
this.setWorkingParts({
...this.workingParts,
ampm: ev.detail.value,
hour
});
this.setActiveParts({
...this.workingParts,
ampm: ev.detail.value,
hour
});
ev.stopPropagation();
}}
></ion-picker-column-internal> }
</ion-picker-internal>
)
}
private renderTimeOverlay(
hoursItems: PickerColumnItem[],
minutesItems: PickerColumnItem[],
ampmItems: PickerColumnItem[],
use24Hour: boolean
) {
return [
<div class="time-header">
{this.renderTimeLabel()}
</div>,
<button
class={{
'time-body': true,
'time-body-active': this.isTimePopoverOpen
}}
aria-expanded="false"
aria-haspopup="true"
onClick={async ev => {
const { popoverRef } = this;
if (popoverRef) {
this.isTimePopoverOpen = true;
popoverRef.present(ev);
await popoverRef.onWillDismiss();
this.isTimePopoverOpen = false;
}
}}
>
{getFormattedTime(this.workingParts, use24Hour)}
</button>,
<ion-popover
alignment="center"
translucent
overlayIndex={1}
arrow={false}
style={{
'--offset-y': '-10px'
}}
ref={el => this.popoverRef = el}
>
{this.renderTimePicker(hoursItems, minutesItems, ampmItems, use24Hour)}
</ion-popover>
]
}
/**
* Render time picker inside of datetime.
* Do not pass color prop to segment on
@ -1464,87 +1332,44 @@ export class Datetime implements ComponentInterface {
* should take on the color prop, but iOS
* should just be the default segment.
*/
private renderTime(mode: Mode) {
const { hourCycle } = this;
const use24Hour = is24Hour(this.locale, hourCycle);
const { ampm } = this.workingParts;
private renderTime() {
const { workingParts, presentation } = this;
const timeOnlyPresentation = presentation === 'time';
const use24Hour = is24Hour(this.locale, this.hourCycle);
const { hours, minutes, am, pm } = generateTime(this.workingParts, use24Hour ? 'h23' : 'h12', this.minParts, this.maxParts, this.parsedHourValues, this.parsedMinuteValues);
return (
<div class="datetime-time">
<div class="time-header">
{this.renderTimeLabel()}
</div>
<div class="time-body">
<div class="time-base" ref={el => this.timeBaseRef = el}>
<div class="time-wrapper">
<div
class="ion-focusable time-column time-column-hours"
aria-label="Hours"
role="slider"
ref={el => this.timeHourRef = el}
tabindex="0"
>
{ hours.map(hour => {
return (
<div
class="time-item"
data-value={getInternalHourValue(hour, use24Hour, ampm)}
>{getFormattedHour(hour, use24Hour)}</div>
)
})}
</div>
<div class="time-separator">:</div>
<div
class="ion-focusable time-column time-column-minutes"
aria-label="Minutes"
role="slider"
ref={el => this.timeMinuteRef = el}
tabindex="0"
>
{ minutes.map(minute => {
return (
<div
class="time-item"
data-value={minute}
>{addTimePadding(minute)}</div>
)
})}
</div>
</div>
</div>
{ !use24Hour && <div class="time-ampm">
<ion-segment
color={mode === 'md' ? this.color : undefined}
value={this.workingParts.ampm}
onIonChange={(ev: CustomEvent) => {
/**
* Since datetime uses 24-hour time internally
* we need to update the working hour here as well
* if the user is using a 12-hour time format.
*/
const { value } = ev.detail;
const hour = calculateHourFromAMPM(this.workingParts, value);
this.setWorkingParts({
...this.workingParts,
ampm: value,
hour
const hoursItems = hours.map(hour => {
return {
text: getFormattedHour(hour, use24Hour),
value: getInternalHourValue(hour, use24Hour, workingParts.ampm)
}
});
/**
* Do not let this event bubble up
* otherwise developers listening for ionChange
* on the datetime will see this event.
*/
ev.stopPropagation();
}}
>
<ion-segment-button disabled={!am} value="am">AM</ion-segment-button>
<ion-segment-button disabled={!pm} value="pm">PM</ion-segment-button>
</ion-segment>
</div> }
</div>
const minutesItems = minutes.map(minute => {
return {
text: addTimePadding(minute),
value: minute
}
});
const ampmItems = [];
if (am) {
ampmItems.push({
text: 'AM',
value: 'am'
})
}
if (pm) {
ampmItems.push({
text: 'PM',
value: 'pm'
})
}
return (
<div class="datetime-time">
{timeOnlyPresentation ? this.renderTimePicker(hoursItems, minutesItems, ampmItems, use24Hour) : this.renderTimeOverlay(hoursItems, minutesItems, ampmItems, use24Hour)}
</div>
)
}
@ -1573,20 +1398,20 @@ export class Datetime implements ComponentInterface {
this.renderCalendarViewHeader(mode),
this.renderCalendar(mode),
this.renderYearView(),
this.renderTime(mode),
this.renderTime(),
this.renderFooter()
]
case 'time-date':
return [
this.renderCalendarViewHeader(mode),
this.renderTime(mode),
this.renderTime(),
this.renderCalendar(mode),
this.renderYearView(),
this.renderFooter()
]
case 'time':
return [
this.renderTime(mode),
this.renderTime(),
this.renderFooter()
]
case 'month':
@ -1638,4 +1463,3 @@ export class Datetime implements ComponentInterface {
}
let datetimeIds = 0;
const PICKER_COL_ACTIVE = 'picker-col-item-active';

View File

@ -183,6 +183,17 @@ dates in JavaScript.
| `Shift` + `PageUp` | Changes the grid of dates to the previous year. |
| `Shift` + `PageDown` | Changes the grid of dates to the next year. |
#### Time, Month, and Year Wheels
When using the time wheel picker, you can use the number keys to select hour and minute values when the columns are focused.
| Key | Function |
| ------------------ | ------------------------------------------------------------ |
| `ArrowUp` | Scroll to the previous item. |
| `ArrowDown` | Scroll to the next item. |
| `Home` | Scroll to the first item. |
| `End` | Scroll to the last item. |
## Interfaces
### DatetimeChangeEventDetail
@ -776,27 +787,29 @@ Type: `Promise<void>`
- [ion-buttons](../buttons)
- [ion-button](../button)
- ion-picker-internal
- ion-picker-column-internal
- [ion-item](../item)
- [ion-label](../label)
- ion-icon
- [ion-segment](../segment)
- [ion-segment-button](../segment-button)
- [ion-popover](../popover)
### Graph
```mermaid
graph TD;
ion-datetime --> ion-buttons
ion-datetime --> ion-button
ion-datetime --> ion-picker-internal
ion-datetime --> ion-picker-column-internal
ion-datetime --> ion-item
ion-datetime --> ion-label
ion-datetime --> ion-icon
ion-datetime --> ion-segment
ion-datetime --> ion-segment-button
ion-datetime --> ion-popover
ion-button --> ion-ripple-effect
ion-item --> ion-icon
ion-item --> ion-ripple-effect
ion-item --> ion-note
ion-segment-button --> ion-ripple-effect
ion-popover --> ion-backdrop
style ion-datetime fill:#f9f,stroke:#333,stroke-width:4px
```

View File

@ -401,7 +401,9 @@
const modalElement = Object.assign(document.createElement('ion-modal'), {
component: element
});
document.body.appendChild(modalElement);
const app = document.querySelector('ion-app');
app.appendChild(modalElement);
return modalElement;
}
</script>

View File

@ -57,6 +57,15 @@
value="2021-06-20"
></ion-datetime>
</div>
<div class="grid-item">
<h2>AM/PM Min/Max</h2>
<ion-datetime
presentation="time"
min="09:30"
max="14:50"
value="10:30"
></ion-datetime>
</div>
</div>
</ion-content>

View File

@ -89,6 +89,7 @@
presentation="year"
></ion-datetime>
</div>
</div>
</ion-content>
</ion-app>
</body>

View File

@ -1,5 +1,28 @@
import { DatetimeParts } from '../datetime-interface';
const get12HourTime = (hour: number) => {
return hour % 12 || 12;
}
const getFormattedAMPM = (ampm?: string) => {
if (ampm === undefined) { return ''; }
return ampm.toUpperCase();
}
export const getFormattedTime = (refParts: DatetimeParts, use24Hour: boolean): string => {
if (refParts.hour === undefined || refParts.minute === undefined) { return 'Invalid Time'; }
const hour = use24Hour ? getFormattedHour(refParts.hour, use24Hour) : get12HourTime(refParts.hour);
const minute = addTimePadding(refParts.minute);
if (use24Hour) {
return `${hour}:${minute}`
}
return `${hour}:${minute} ${getFormattedAMPM(refParts.ampm)}`
}
/**
* Adds padding to a time value so
* that it is always 2 digits.

View File

@ -111,7 +111,6 @@
column.addEventListener('ionChange', (ev) => {
console.log('Column change', ev.detail);
});
const setPickerColumn = (selector, items, value) => {
const picker = document.querySelector(selector);

View File

@ -633,6 +633,10 @@ Type: `Promise<void>`
## Dependencies
### Used by
- [ion-datetime](../datetime)
### Depends on
- [ion-backdrop](../backdrop)
@ -641,6 +645,7 @@ Type: `Promise<void>`
```mermaid
graph TD;
ion-popover --> ion-backdrop
ion-datetime --> ion-popover
style ion-popover fill:#f9f,stroke:#333,stroke-width:4px
```

View File

@ -852,10 +852,6 @@ export default defineComponent({
## Dependencies
### Used by
- [ion-datetime](../datetime)
### Depends on
- [ion-ripple-effect](../ripple-effect)
@ -864,7 +860,6 @@ export default defineComponent({
```mermaid
graph TD;
ion-segment-button --> ion-ripple-effect
ion-datetime --> ion-segment-button
style ion-segment-button fill:#f9f,stroke:#333,stroke-width:4px
```

View File

@ -626,19 +626,6 @@ export default defineComponent({
| `--background` | Background of the segment button |
## Dependencies
### Used by
- [ion-datetime](../datetime)
### Graph
```mermaid
graph TD;
ion-datetime --> ion-segment
style ion-segment fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*