diff --git a/core/src/components/datetime/datetime.ios.scss b/core/src/components/datetime/datetime.ios.scss index 642d1261ea..7507681e0d 100644 --- a/core/src/components/datetime/datetime.ios.scss +++ b/core/src/components/datetime/datetime.ios.scss @@ -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 { diff --git a/core/src/components/datetime/datetime.md.scss b/core/src/components/datetime/datetime.md.scss index 0cc2e2cfe0..df57e4b025 100644 --- a/core/src/components/datetime/datetime.md.scss +++ b/core/src/components/datetime/datetime.md.scss @@ -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); -} diff --git a/core/src/components/datetime/datetime.md.vars.scss b/core/src/components/datetime/datetime.md.vars.scss index f2c167c85a..ee1b2bfe82 100644 --- a/core/src/components/datetime/datetime.md.vars.scss +++ b/core/src/components/datetime/datetime.md.vars.scss @@ -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; diff --git a/core/src/components/datetime/datetime.scss b/core/src/components/datetime/datetime.scss index 0414184bac..c5e5c6c26a 100644 --- a/core/src/components/datetime/datetime.scss +++ b/core/src/components/datetime/datetime.scss @@ -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; -} diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 9f4f4e4b3e..197fedb3f7 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -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 (
-
-
-
- {showMonth &&
this.monthRef = el} tabindex="0"> -
 
-
 
-
 
- {getPickerMonths(this.locale, this.workingParts, this.minParts, this.maxParts, this.parsedMonthValues).map(month => { - return ( -
this.centerPickerItemInView(ev.target as HTMLElement, this.monthRef as HTMLElement)} - >{month.text}
- ) - })} -
 
-
 
-
 
-
} - {showYear &&
this.yearRef = el} tabindex="0"> -
 
-
 
-
 
- {calendarYears.map(year => { - return ( -
this.centerPickerItemInView(ev.target as HTMLElement, this.yearRef as HTMLElement)} - >{year}
- ) - })} -
 
-
 
-
 
-
} + + { + showMonth && + { + this.setWorkingParts({ + ...this.workingParts, + month: ev.detail.value + }); + + if (presentation === 'month' || presentation === 'month-year') { + this.setActiveParts({ + ...this.activeParts, + month: ev.detail.value + }); + } + + ev.stopPropagation(); + }} + > + } + { + showYear && + { + this.setWorkingParts({ + ...this.workingParts, + year: ev.detail.value + }); + + if (presentation === 'year' || presentation === 'month-year') { + this.setActiveParts({ + ...this.activeParts, + year: ev.detail.value + }); + } + + ev.stopPropagation(); + }} + > + } +
); @@ -1457,6 +1206,125 @@ export class Datetime implements ComponentInterface { ); } + private renderTimePicker( + hoursItems: PickerColumnItem[], + minutesItems: PickerColumnItem[], + ampmItems: PickerColumnItem[], + use24Hour: boolean + ) { + const { color, workingParts } = this; + return ( + + { + this.setWorkingParts({ + ...this.workingParts, + hour: ev.detail.value + }); + this.setActiveParts({ + ...this.activeParts, + hour: ev.detail.value + }); + + ev.stopPropagation(); + }} + > + { + this.setWorkingParts({ + ...this.workingParts, + minute: ev.detail.value + }); + this.setActiveParts({ + ...this.activeParts, + minute: ev.detail.value + }); + + ev.stopPropagation(); + }} + > + { !use24Hour && { + 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(); + }} + > } + + ) + } + + private renderTimeOverlay( + hoursItems: PickerColumnItem[], + minutesItems: PickerColumnItem[], + ampmItems: PickerColumnItem[], + use24Hour: boolean + ) { + return [ +
+ {this.renderTimeLabel()} +
, + , + this.popoverRef = el} + > + {this.renderTimePicker(hoursItems, minutesItems, ampmItems, use24Hour)} + + ] + } + /** * 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); + + 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: 'AM', + value: 'am' + }) + } + + if (pm) { + ampmItems.push({ + text: 'PM', + value: 'pm' + }) + } + return (
-
- {this.renderTimeLabel()} -
-
-
this.timeBaseRef = el}> -
-
this.timeHourRef = el} - tabindex="0" - > - { hours.map(hour => { - return ( -
{getFormattedHour(hour, use24Hour)}
- ) - })} -
-
:
-
this.timeMinuteRef = el} - tabindex="0" - > - { minutes.map(minute => { - return ( -
{addTimePadding(minute)}
- ) - })} -
-
-
- { !use24Hour &&
- { - - /** - * 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 - }); - - /** - * Do not let this event bubble up - * otherwise developers listening for ionChange - * on the datetime will see this event. - */ - ev.stopPropagation(); - }} - > - AM - PM - -
} -
+ {timeOnlyPresentation ? this.renderTimePicker(hoursItems, minutesItems, ampmItems, use24Hour) : this.renderTimeOverlay(hoursItems, minutesItems, ampmItems, use24Hour)}
) } @@ -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'; diff --git a/core/src/components/datetime/readme.md b/core/src/components/datetime/readme.md index b043b89582..4aa0882152 100644 --- a/core/src/components/datetime/readme.md +++ b/core/src/components/datetime/readme.md @@ -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` - [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 ``` diff --git a/core/src/components/datetime/test/basic/index.html b/core/src/components/datetime/test/basic/index.html index 56cb12d665..b49c5b7721 100644 --- a/core/src/components/datetime/test/basic/index.html +++ b/core/src/components/datetime/test/basic/index.html @@ -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; } diff --git a/core/src/components/datetime/test/minmax/index.html b/core/src/components/datetime/test/minmax/index.html index ce88429ff1..a6f93975e6 100644 --- a/core/src/components/datetime/test/minmax/index.html +++ b/core/src/components/datetime/test/minmax/index.html @@ -57,6 +57,15 @@ value="2021-06-20" > +
+

AM/PM Min/Max

+ +
diff --git a/core/src/components/datetime/test/presentation/index.html b/core/src/components/datetime/test/presentation/index.html index e91defc7b3..93a494cb27 100644 --- a/core/src/components/datetime/test/presentation/index.html +++ b/core/src/components/datetime/test/presentation/index.html @@ -56,38 +56,39 @@

time-date

-
-
-

time

- -
-
-

date

- -
-
-

month-year

- -
-
-

month

- -
-
-

year

- + presentation="time-date" + > +
+
+

time

+ +
+
+

date

+ +
+
+

month-year

+ +
+
+

month

+ +
+
+

year

+ +
diff --git a/core/src/components/datetime/utils/format.ts b/core/src/components/datetime/utils/format.ts index f7571e65bf..f5a027b12b 100644 --- a/core/src/components/datetime/utils/format.ts +++ b/core/src/components/datetime/utils/format.ts @@ -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. diff --git a/core/src/components/picker-internal/test/basic/index.html b/core/src/components/picker-internal/test/basic/index.html index c0dce32bdd..d061db68a0 100644 --- a/core/src/components/picker-internal/test/basic/index.html +++ b/core/src/components/picker-internal/test/basic/index.html @@ -111,7 +111,6 @@ column.addEventListener('ionChange', (ev) => { console.log('Column change', ev.detail); }); - const setPickerColumn = (selector, items, value) => { const picker = document.querySelector(selector); diff --git a/core/src/components/popover/readme.md b/core/src/components/popover/readme.md index 72522f0cde..a1a398403b 100644 --- a/core/src/components/popover/readme.md +++ b/core/src/components/popover/readme.md @@ -633,6 +633,10 @@ Type: `Promise` ## Dependencies +### Used by + + - [ion-datetime](../datetime) + ### Depends on - [ion-backdrop](../backdrop) @@ -641,6 +645,7 @@ Type: `Promise` ```mermaid graph TD; ion-popover --> ion-backdrop + ion-datetime --> ion-popover style ion-popover fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/core/src/components/segment-button/readme.md b/core/src/components/segment-button/readme.md index 11c2bb7584..921a9d8b2f 100644 --- a/core/src/components/segment-button/readme.md +++ b/core/src/components/segment-button/readme.md @@ -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 ``` diff --git a/core/src/components/segment/readme.md b/core/src/components/segment/readme.md index daa67ed016..ba48ec3ec4 100644 --- a/core/src/components/segment/readme.md +++ b/core/src/components/segment/readme.md @@ -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/)*