diff --git a/core/api.txt b/core/api.txt index 4dae8d6d4f..7e39df77b0 100644 --- a/core/api.txt +++ b/core/api.txt @@ -384,7 +384,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,presentation,"date" | "date-time" | "time" | "time-date",'date-time',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,showDefaultButtons,boolean,false,false,false ion-datetime,prop,showDefaultTimeLabel,boolean,true,false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 2d066c9894..da5cdab5f4 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -771,7 +771,7 @@ export namespace Components { /** * 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. */ - "presentation": 'date-time' | 'time-date' | 'date' | 'time'; + "presentation": 'date-time' | 'time-date' | 'date' | 'time' | 'month' | 'year' | 'month-year'; /** * If `true`, the datetime appears normal but is not interactive. */ @@ -4379,7 +4379,7 @@ declare namespace LocalJSX { /** * 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. */ - "presentation"?: 'date-time' | 'time-date' | 'date' | 'time'; + "presentation"?: 'date-time' | 'time-date' | 'date' | 'time' | 'month' | 'year' | 'month-year'; /** * If `true`, the datetime appears normal but is not interactive. */ diff --git a/core/src/components/datetime/datetime.ios.scss b/core/src/components/datetime/datetime.ios.scss index a2240eea51..642d1261ea 100644 --- a/core/src/components/datetime/datetime.ios.scss +++ b/core/src/components/datetime/datetime.ios.scss @@ -8,7 +8,9 @@ --title-color: #{$text-color-step-400}; } -:host(:not(.datetime-presentation-time)) { +:host(.datetime-presentation-date-time), +:host(.datetime-presentation-time-date), +:host(.datetime-presentation-date) { min-height: 300px; } @@ -156,58 +158,29 @@ line-height: $datetime-ios-time-height; } -// Year Picker +// Month and Year Picker // ----------------------------------- -: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); @include padding(0, $datetime-ios-padding, 0, $datetime-ios-padding); } :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.8) 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.8) 100%); - - z-index: 10; - - pointer-events: none; } :host .datetime-picker-highlight { - @include position(50%, 0, 0, 0); @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%); @@ -217,78 +190,6 @@ z-index: -1; } -: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: 34px; - - line-height: 34px; - - 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; -} - // Footer // ----------------------------------- :host .datetime-buttons { diff --git a/core/src/components/datetime/datetime.md.scss b/core/src/components/datetime/datetime.md.scss index d86e5f8e19..0cc2e2cfe0 100644 --- a/core/src/components/datetime/datetime.md.scss +++ b/core/src/components/datetime/datetime.md.scss @@ -150,96 +150,33 @@ background: var(--background-checked); } -// Year Picker +// Month and Year // ----------------------------------- -:host(.show-month-and-year) .datetime-calendar { - flex: 0; +: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(.show-month-and-year) .datetime-year { - flex: 1; - min-height: 0; +:host .picker-col-item { + font-size: 18px; } -:host(.show-month-and-year) .datetime-footer { - border-top: 1px solid var(--ion-color-step-250, #dddddd); +: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-year-body { - @include padding(0, $datetime-md-padding, $datetime-md-padding, $datetime-md-padding); - - display: grid; - - grid-template-columns: repeat(auto-fit, minmax(74px, 1fr)); - grid-gap: 0px 6px; - - overflow-y: scroll; - -webkit-overflow-scrolling: touch; -} - -:host .datetime-year-item { - @include padding(0px, 0px, 0px, 0px); - @include margin(0px, 0px, 0px, 0px); - - position: relative; - - align-items: center; - justify-content: center; - - border: none; - - outline: none; - - background: none; - color: currentColor; - - cursor: pointer; - - appearance: none; - - z-index: 0; -} - -:host .datetime-year-item[disabled] { - pointer-events: none; - - opacity: 0.4; -} - -:host .datetime-year-item .datetime-year-inner { - @include border-radius(20px, 20px, 20px, 20px); - @include margin(10px, 10px, 10px, 10px); - - display: flex; - - align-items: center; - justify-content: center; - - height: 32px; - - border: 1px solid transparent; - - font-size: 16px; +: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%); } -:host .datetime-current-year .datetime-year-inner { - border: 1px solid current-color(base); - - color: current-color(base); -} - -:host .datetime-active-year .datetime-year-inner { - border: 1px solid current-color(base); - - background: current-color(base); - color: current-color(contrast); -} - -@media (any-hover: hover) { - :host .datetime-year-item:hover, - :host .datetime-year-item:focus { - background: current-color(base, 0.1); - } +/** + * Add some margin when only selecting month/year + * otherwise layout will too constricted. + */ +:host(.datetime-presentation-month) .datetime-year, +:host(.datetime-presentation-year) .datetime-year, +:host(.datetime-presentation-month-year) .datetime-year { + @include margin(20px, null, 20px, null); } // Footer @@ -257,3 +194,7 @@ :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 a735fa0d8d..f2c167c85a 100644 --- a/core/src/components/datetime/datetime.md.vars.scss +++ b/core/src/components/datetime/datetime.md.vars.scss @@ -24,3 +24,9 @@ $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 8096678066..5446f961c6 100644 --- a/core/src/components/datetime/datetime.scss +++ b/core/src/components/datetime/datetime.scss @@ -19,7 +19,9 @@ overflow: hidden; } -:host(:not(.datetime-presentation-time)) { +:host(.datetime-presentation-date-time), +:host(.datetime-presentation-time-date), +:host(.datetime-presentation-date) { height: 100%; } @@ -387,3 +389,125 @@ :host(.in-item) { position: static; } + +// Year Picker +// ----------------------------------- +: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 c2fe1cff74..c163afc3e2 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -53,7 +53,6 @@ import { } from './utils/parse'; import { getCalendarDayState, - getCalendarYearState, isDayDisabled } from './utils/state'; @@ -188,7 +187,7 @@ export class Datetime implements ComponentInterface { * 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. */ - @Prop() presentation: 'date-time' | 'time-date' | 'date' | 'time' = 'date-time'; + @Prop() presentation: 'date-time' | 'time-date' | 'date' | 'time' | 'month' | 'year' | 'month-year' = 'date-time'; /** * The text to display on the picker's cancel button. @@ -814,8 +813,6 @@ export class Datetime implements ComponentInterface { } componentDidLoad() { - const mode = getIonMode(this); - /** * If a scrollable element is hidden using `display: none`, * it will not have a scroll height meaning we cannot scroll elements @@ -832,10 +829,7 @@ export class Datetime implements ComponentInterface { this.initializeKeyboardListeners(); this.initializeTimeScrollListener(); this.initializeOverlayListener(); - - if (mode === 'ios') { - this.initializeMonthAndYearScrollListeners(); - } + this.initializeMonthAndYearScrollListeners(); /** * TODO: Datetime needs a frame to ensure that it @@ -891,24 +885,24 @@ export class Datetime implements ComponentInterface { } private initializeMonthAndYearScrollListeners = () => { - const { monthRef, yearRef } = this; - if (!yearRef || !monthRef) { return; } - - const { year, month } = this.workingParts; + 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. */ - const initialYear = yearRef.querySelector(`.picker-col-item[data-value="${year}"]`) as HTMLElement | null; - if (initialYear) { - yearRef.scrollTop = initialYear.offsetTop - (initialYear.clientHeight * 2); + 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); } - const initialMonth = monthRef.querySelector(`.picker-col-item[data-value="${month}"]`) as HTMLElement | null; - if (initialMonth) { - monthRef.scrollTop = initialMonth.offsetTop - (initialMonth.clientHeight * 2); + 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; @@ -920,18 +914,31 @@ export class Datetime implements ComponentInterface { } const activeCol = colType === 'month' ? monthRef : yearRef; - timeout = setTimeout(() => { + if (!activeCol) { return; } - const bbox = activeCol.getBoundingClientRect(); + 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); - /** - * 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); + } - const activeElement = this.el!.shadowRoot!.elementFromPoint(centerX, centerY)!; + 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'); /** @@ -944,16 +951,35 @@ export class Datetime implements ComponentInterface { } 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 + }); + } } /** @@ -963,14 +989,14 @@ export class Datetime implements ComponentInterface { */ 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}']`); + const monthEl = monthRef?.querySelector(`.picker-col-item[data-value='${workingMonth}']`); + const yearEl = yearRef?.querySelector(`.picker-col-item[data-value='${workingYear}']`); - if (monthEl) { + if (monthEl && monthRef) { this.centerPickerItemInView(monthEl as HTMLElement, monthRef, 'auto'); } - if (yearEl) { + if (yearEl && yearRef) { this.centerPickerItemInView(yearEl as HTMLElement, yearRef, 'auto'); } }); @@ -985,12 +1011,12 @@ export class Datetime implements ComponentInterface { raf(() => { const monthScroll = () => scrollCallback('month'); const yearScroll = () => scrollCallback('year'); - monthRef.addEventListener('scroll', monthScroll); - yearRef.addEventListener('scroll', yearScroll); + monthRef?.addEventListener('scroll', monthScroll); + yearRef?.addEventListener('scroll', yearScroll); this.destroyMonthAndYearScroll = () => { - monthRef.removeEventListener('scroll', monthScroll); - yearRef.removeEventListener('scroll', yearScroll); + monthRef?.removeEventListener('scroll', monthScroll); + yearRef?.removeEventListener('scroll', yearScroll); } }); } @@ -1198,34 +1224,6 @@ export class Datetime implements ComponentInterface { this.showMonthAndYear = !this.showMonthAndYear; } - private renderMDYearView(calendarYears: number[] = []) { - return calendarYears.map(year => { - - const { isCurrentYear, isActiveYear, ariaSelected } = getCalendarYearState(year, this.workingParts, this.todayParts); - return ( - - ) - }) - } - 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) @@ -1235,54 +1233,52 @@ export class Datetime implements ComponentInterface { }); } - private renderiOSYearView(calendarYears: number[] = []) { - return [ -
, - , - , -