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 (