diff --git a/angular/src/directives/proxies-list.txt b/angular/src/directives/proxies-list.txt index 1ec13c5c85..06d60e7c6f 100644 --- a/angular/src/directives/proxies-list.txt +++ b/angular/src/directives/proxies-list.txt @@ -23,6 +23,7 @@ export const DIRECTIVES = [ d.IonCol, d.IonContent, d.IonDatetime, + d.IonDatetimeButton, d.IonFab, d.IonFabButton, d.IonFabList, diff --git a/angular/src/directives/proxies.ts b/angular/src/directives/proxies.ts index 65b70f4ced..f600ad7fde 100644 --- a/angular/src/directives/proxies.ts +++ b/angular/src/directives/proxies.ts @@ -543,6 +543,27 @@ export class IonDatetime { } +export declare interface IonDatetimeButton extends Components.IonDatetimeButton {} + +@ProxyCmp({ + defineCustomElementFn: undefined, + inputs: ['color', 'datetime', 'disabled', 'mode'] +}) +@Component({ + selector: 'ion-datetime-button', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + inputs: ['color', 'datetime', 'disabled', 'mode'] +}) +export class IonDatetimeButton { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + export declare interface IonFab extends Components.IonFab {} @ProxyCmp({ diff --git a/core/api.txt b/core/api.txt index faec6fe1f7..8a7ed74c34 100644 --- a/core/api.txt +++ b/core/api.txt @@ -410,6 +410,13 @@ ion-datetime,css-prop,--background ion-datetime,css-prop,--background-rgb ion-datetime,css-prop,--title-color +ion-datetime-button,shadow +ion-datetime-button,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record | undefined,'primary',false,true +ion-datetime-button,prop,datetime,string | undefined,undefined,false,false +ion-datetime-button,prop,disabled,boolean,false,false,true +ion-datetime-button,prop,mode,"ios" | "md",undefined,false,false +ion-datetime-button,part,native + ion-fab,shadow ion-fab,prop,activated,boolean,false,false,false ion-fab,prop,edge,boolean,false,false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 06f972783d..f2b1fbe72e 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -5,7 +5,7 @@ * It contains typing information for all components that exist in this project. */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; -import { AccordionGroupChangeEventDetail, ActionSheetAttributes, ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, BreadcrumbCollapsedClickEventDetail, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DomRenderFn, FooterHeightFn, FrameworkDelegate, HeaderFn, HeaderHeightFn, InputChangeEventDetail, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, LoadingAttributes, MenuChangeEventDetail, ModalAttributes, ModalBreakpointChangeEventDetail, ModalHandleBehavior, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerAttributes, PickerButton, PickerColumn, PopoverAttributes, PopoverSize, PositionAlign, PositionReference, PositionSide, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, TriggerAction, ViewController } from "./interface"; +import { AccordionGroupChangeEventDetail, ActionSheetAttributes, ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, BreadcrumbCollapsedClickEventDetail, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DatetimePresentation, DomRenderFn, FooterHeightFn, FrameworkDelegate, HeaderFn, HeaderHeightFn, InputChangeEventDetail, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, LoadingAttributes, MenuChangeEventDetail, ModalAttributes, ModalBreakpointChangeEventDetail, ModalHandleBehavior, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerAttributes, PickerButton, PickerColumn, PopoverAttributes, PopoverSize, PositionAlign, PositionReference, PositionSide, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, TriggerAction, ViewController } from "./interface"; import { IonicSafeString } from "./utils/sanitization"; import { AlertAttributes } from "./components/alert/alert-interface"; import { CounterFormatter } from "./components/item/item-interface"; @@ -800,7 +800,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' | 'month' | 'year' | 'month-year'; + "presentation": DatetimePresentation; /** * If `true`, the datetime appears normal but is not interactive. */ @@ -838,6 +838,24 @@ export namespace Components { */ "yearValues"?: number[] | number | string; } + interface IonDatetimeButton { + /** + * The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). + */ + "color"?: Color; + /** + * The ID of the `ion-datetime` instance associated with the datetime button. + */ + "datetime"?: string; + /** + * If `true`, the user cannot interact with the button. + */ + "disabled": boolean; + /** + * The mode determines which platform styles to use. + */ + "mode"?: "ios" | "md"; + } interface IonFab { /** * If `true`, both the `ion-fab-button` and all `ion-fab-list` inside `ion-fab` will become active. That means `ion-fab-button` will become a `close` icon and `ion-fab-list` will become visible. @@ -3413,6 +3431,12 @@ declare global { prototype: HTMLIonDatetimeElement; new (): HTMLIonDatetimeElement; }; + interface HTMLIonDatetimeButtonElement extends Components.IonDatetimeButton, HTMLStencilElement { + } + var HTMLIonDatetimeButtonElement: { + prototype: HTMLIonDatetimeButtonElement; + new (): HTMLIonDatetimeButtonElement; + }; interface HTMLIonFabElement extends Components.IonFab, HTMLStencilElement { } var HTMLIonFabElement: { @@ -3857,6 +3881,7 @@ declare global { "ion-col": HTMLIonColElement; "ion-content": HTMLIonContentElement; "ion-datetime": HTMLIonDatetimeElement; + "ion-datetime-button": HTMLIonDatetimeButtonElement; "ion-fab": HTMLIonFabElement; "ion-fab-button": HTMLIonFabButtonElement; "ion-fab-list": HTMLIonFabListElement; @@ -4740,6 +4765,10 @@ declare namespace LocalJSX { * Emitted when the datetime has focus. */ "onIonFocus"?: (event: IonDatetimeCustomEvent) => void; + /** + * Emitted when componentDidRender is fired. + */ + "onIonRender"?: (event: IonDatetimeCustomEvent) => void; /** * Emitted when the styles change. */ @@ -4751,7 +4780,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' | 'month' | 'year' | 'month-year'; + "presentation"?: DatetimePresentation; /** * If `true`, the datetime appears normal but is not interactive. */ @@ -4785,6 +4814,24 @@ declare namespace LocalJSX { */ "yearValues"?: number[] | number | string; } + interface IonDatetimeButton { + /** + * The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). + */ + "color"?: Color; + /** + * The ID of the `ion-datetime` instance associated with the datetime button. + */ + "datetime"?: string; + /** + * If `true`, the user cannot interact with the button. + */ + "disabled"?: boolean; + /** + * The mode determines which platform styles to use. + */ + "mode"?: "ios" | "md"; + } interface IonFab { /** * If `true`, both the `ion-fab-button` and all `ion-fab-list` inside `ion-fab` will become active. That means `ion-fab-button` will become a `close` icon and `ion-fab-list` will become visible. @@ -7077,6 +7124,7 @@ declare namespace LocalJSX { "ion-col": IonCol; "ion-content": IonContent; "ion-datetime": IonDatetime; + "ion-datetime-button": IonDatetimeButton; "ion-fab": IonFab; "ion-fab-button": IonFabButton; "ion-fab-list": IonFabList; @@ -7176,6 +7224,7 @@ declare module "@stencil/core" { "ion-col": LocalJSX.IonCol & JSXBase.HTMLAttributes; "ion-content": LocalJSX.IonContent & JSXBase.HTMLAttributes; "ion-datetime": LocalJSX.IonDatetime & JSXBase.HTMLAttributes; + "ion-datetime-button": LocalJSX.IonDatetimeButton & JSXBase.HTMLAttributes; "ion-fab": LocalJSX.IonFab & JSXBase.HTMLAttributes; "ion-fab-button": LocalJSX.IonFabButton & JSXBase.HTMLAttributes; "ion-fab-list": LocalJSX.IonFabList & JSXBase.HTMLAttributes; diff --git a/core/src/components/accordion/test/standalone/accordion.e2e.ts b/core/src/components/accordion/test/standalone/accordion.e2e.ts index 46470182e8..48bb7327ba 100644 --- a/core/src/components/accordion/test/standalone/accordion.e2e.ts +++ b/core/src/components/accordion/test/standalone/accordion.e2e.ts @@ -11,7 +11,8 @@ test.describe('accordion: standalone', () => { expect(results.violations).toEqual([]); }); - test('should not have visual regressions', async ({ page }) => { + // TODO(FW-1842) Re-enable when flakiness has been addressed. + test.skip('should not have visual regressions', async ({ page }) => { await page.goto(`/src/components/accordion/test/standalone`); expect(await page.screenshot({ fullPage: true })).toMatchSnapshot( diff --git a/core/src/components/accordion/test/standalone/accordion.e2e.ts-snapshots/accordion-standalone-ios-ltr-Mobile-Safari-linux.png b/core/src/components/accordion/test/standalone/accordion.e2e.ts-snapshots/accordion-standalone-ios-ltr-Mobile-Safari-linux.png index 497cfb1a56..1c985494d6 100644 Binary files a/core/src/components/accordion/test/standalone/accordion.e2e.ts-snapshots/accordion-standalone-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/accordion/test/standalone/accordion.e2e.ts-snapshots/accordion-standalone-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/accordion/test/standalone/accordion.e2e.ts-snapshots/accordion-standalone-ios-rtl-Mobile-Safari-linux.png b/core/src/components/accordion/test/standalone/accordion.e2e.ts-snapshots/accordion-standalone-ios-rtl-Mobile-Safari-linux.png index 91980dc95c..fe09385354 100644 Binary files a/core/src/components/accordion/test/standalone/accordion.e2e.ts-snapshots/accordion-standalone-ios-rtl-Mobile-Safari-linux.png and b/core/src/components/accordion/test/standalone/accordion.e2e.ts-snapshots/accordion-standalone-ios-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/datetime-button/datetime-button.scss b/core/src/components/datetime-button/datetime-button.scss new file mode 100644 index 0000000000..3a87b88128 --- /dev/null +++ b/core/src/components/datetime-button/datetime-button.scss @@ -0,0 +1,50 @@ +@import "../../themes/ionic.globals"; + +// Datetime Button +// -------------------------------------------------- + +:host { + display: flex; + + align-items: center; + + justify-content: center; +} + +:host button { + @include border-radius(8px); + @include padding(6px, 12px, 6px, 12px); + @include margin(0px, 2px, 0px, 2px); + + position: relative; + + transition: 150ms color ease-in-out; + + border: none; + + background: var(--ion-color-step-300, #edeef0); + + color: $text-color; + + font-family: inherit; + font-size: inherit; + + cursor: pointer; + + appearance: none; + + overflow: hidden; +} + +:host(.time-active) #time-button, +:host(.date-active) #date-button { + color: current-color(base); +} + +:host(.datetime-button-disabled) { + pointer-events: none; +} + +:host(.datetime-button-disabled) button { + opacity: 0.4; +} diff --git a/core/src/components/datetime-button/datetime-button.tsx b/core/src/components/datetime-button/datetime-button.tsx new file mode 100644 index 0000000000..a5536f4738 --- /dev/null +++ b/core/src/components/datetime-button/datetime-button.tsx @@ -0,0 +1,414 @@ +import type { ComponentInterface } from '@stencil/core'; +import { Component, Element, Host, Prop, State, h } from '@stencil/core'; + +import { getIonMode } from '../../global/ionic-global'; +import type { Color, DatetimePresentation, DatetimeParts } from '../../interface'; +import { componentOnReady, addEventListener } from '../../utils/helpers'; +import { printIonError, printIonWarning } from '../../utils/logging'; +import { createColorClasses } from '../../utils/theme'; +import { getToday } from '../datetime/utils/data'; +import { getMonthAndYear, getMonthDayAndYear, getLocalizedDateTime, getLocalizedTime } from '../datetime/utils/format'; +import { is24Hour } from '../datetime/utils/helpers'; +import { parseDate } from '../datetime/utils/parse'; +/** + * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use. + * + * @slot date-target - Content displayed inside of the date button. + * @slot time-target - Content displayed inside of the time button. + * + * @part native - The native HTML button that wraps the slotted text. + */ +@Component({ + tag: 'ion-datetime-button', + styleUrls: { + ios: 'datetime-button.scss', + md: 'datetime-button.scss', + }, + shadow: true, +}) +export class DatetimeButton implements ComponentInterface { + private datetimeEl: HTMLIonDatetimeElement | null = null; + private overlayEl: HTMLIonModalElement | HTMLIonPopoverElement | null = null; + private dateTargetEl: HTMLElement | undefined; + private timeTargetEl: HTMLElement | undefined; + + @Element() el!: HTMLIonDatetimeButtonElement; + + @State() datetimePresentation?: DatetimePresentation = 'date-time'; + @State() dateText?: string; + @State() timeText?: string; + @State() datetimeActive = false; + @State() selectedButton?: 'date' | 'time'; + + /** + * The color to use from your application's color palette. + * Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. + * For more information on colors, see [theming](/docs/theming/basics). + */ + @Prop({ reflect: true }) color?: Color = 'primary'; + + /** + * If `true`, the user cannot interact with the button. + */ + @Prop({ reflect: true }) disabled = false; + + /** + * The ID of the `ion-datetime` instance + * associated with the datetime button. + */ + @Prop() datetime?: string; + + async componentWillLoad() { + const { datetime } = this; + if (!datetime) { + printIonError( + 'An ID associated with an ion-datetime instance is required for ion-datetime-button to function properly.', + this.el + ); + return; + } + + const datetimeEl = (this.datetimeEl = document.getElementById(datetime) as HTMLIonDatetimeElement | null); + if (!datetimeEl) { + printIonError(`No ion-datetime instance found for ID '${datetime}'.`, this.el); + return; + } + + /** + * Since the datetime can be used in any context (overlays, accordion, etc) + * we track when it is visible to determine when it is active. + * This informs which button is highlighted as well as the + * aria-expanded state. + */ + const io = new IntersectionObserver( + (entries: IntersectionObserverEntry[]) => { + const ev = entries[0]; + this.datetimeActive = ev.isIntersecting; + }, + { + threshold: 0.01, + } + ); + + io.observe(datetimeEl); + + /** + * Get a reference to any modal/popover + * the datetime is being used in so we can + * correctly size it when it is presented. + */ + const overlayEl = (this.overlayEl = datetimeEl.closest('ion-modal, ion-popover')); + + /** + * The .ion-datetime-button-overlay class contains + * styles that allow any modal/popover to be + * sized according to the dimensions of the datetime. + * If developers want a smaller/larger overlay all they need + * to do is change the width/height of the datetime. + * Additionally, this lets us avoid having to set + * explicit widths on each variant of datetime. + */ + if (overlayEl) { + overlayEl.classList.add('ion-datetime-button-overlay'); + } + + componentOnReady(datetimeEl, () => { + const datetimePresentation = (this.datetimePresentation = datetimeEl.presentation || 'date-time'); + + /** + * Set the initial display + * in the rendered buttons. + * + * From there, we need to listen + * for ionChange to be emitted + * from datetime so we know when + * to re-render the displayed + * text in the buttons. + */ + this.setDateTimeText(); + addEventListener(datetimeEl, 'ionChange', this.setDateTimeText); + + /** + * Configure the initial selected button + * in the event that the datetime is displayed + * without clicking one of the datetime buttons. + * For example, a datetime could be expanded + * in an accordion. In this case users only + * need to click the accordion header to show + * the datetime. + */ + switch (datetimePresentation) { + case 'date-time': + case 'date': + case 'month-year': + case 'month': + case 'year': + this.selectedButton = 'date'; + break; + case 'time-date': + case 'time': + this.selectedButton = 'time'; + break; + } + }); + } + + /** + * Check the value property on the linked + * ion-datetime and then format it according + * to the locale specified on ion-datetime. + */ + private setDateTimeText = () => { + const { datetimeEl, datetimePresentation } = this; + + if (!datetimeEl) { + return; + } + + const { value, locale, hourCycle, preferWheel, multiple } = datetimeEl; + + if (multiple) { + printIonWarning( + `Multi-date selection cannot be used with ion-datetime-button. + +Please upvote https://github.com/ionic-team/ionic-framework/issues/25668 if you are interested in seeing this functionality added. + `, + this.el + ); + return; + } + + /** + * Both ion-datetime and ion-datetime-button default + * to today's date and time if no value is set. + */ + const parsedDatetime = parseDate(value || getToday()) as DatetimeParts; + const use24Hour = is24Hour(locale, hourCycle); + + // TODO(FW-1865) - Remove once FW-1831 is fixed. + parsedDatetime.tzOffset = undefined; + + this.dateText = this.timeText = undefined; + + switch (datetimePresentation) { + case 'date-time': + case 'time-date': + const dateText = getMonthDayAndYear(locale, parsedDatetime); + const timeText = getLocalizedTime(locale, parsedDatetime, use24Hour); + if (preferWheel) { + this.dateText = `${dateText} ${timeText}`; + } else { + this.dateText = dateText; + this.timeText = timeText; + } + break; + case 'date': + this.dateText = getMonthDayAndYear(locale, parsedDatetime); + break; + case 'time': + this.timeText = getLocalizedTime(locale, parsedDatetime, use24Hour); + break; + case 'month-year': + this.dateText = getMonthAndYear(locale, parsedDatetime); + break; + case 'month': + this.dateText = getLocalizedDateTime(locale, parsedDatetime, { month: 'long' }); + break; + case 'year': + this.dateText = getLocalizedDateTime(locale, parsedDatetime, { year: 'numeric' }); + break; + } + }; + + /** + * Waits for the ion-datetime to re-render. + * This is needed in order to correctly position + * a popover relative to the trigger element. + */ + private waitForDatetimeChanges = async () => { + const { datetimeEl } = this; + if (!datetimeEl) { + return Promise.resolve(); + } + + return new Promise((resolve) => { + addEventListener(datetimeEl, 'ionRender', resolve, { once: true }); + }); + }; + + private handleDateClick = async (ev: Event) => { + const { datetimeEl, datetimePresentation } = this; + + if (!datetimeEl) { + return; + } + + let needsPresentationChange = false; + + /** + * When clicking the date button, + * we need to make sure that only a date + * picker is displayed. For presentation styles + * that display content other than a date picker, + * we need to update the presentation style. + */ + switch (datetimePresentation) { + case 'date-time': + case 'time-date': + const needsChange = datetimeEl.presentation !== 'date'; + /** + * The date+time wheel picker + * shows date and time together, + * so do not adjust the presentation + * in that case. + */ + if (!datetimeEl.preferWheel && needsChange) { + datetimeEl.presentation = 'date'; + needsPresentationChange = true; + } + break; + } + + /** + * Track which button was clicked + * so that it can have the correct + * activated styles applied when + * the modal/popover containing + * the datetime is opened. + */ + this.selectedButton = 'date'; + + this.presentOverlay(ev, needsPresentationChange, this.dateTargetEl); + }; + + private handleTimeClick = (ev: Event) => { + const { datetimeEl, datetimePresentation } = this; + + if (!datetimeEl) { + return; + } + + let needsPresentationChange = false; + + /** + * When clicking the time button, + * we need to make sure that only a time + * picker is displayed. For presentation styles + * that display content other than a time picker, + * we need to update the presentation style. + */ + switch (datetimePresentation) { + case 'date-time': + case 'time-date': + const needsChange = datetimeEl.presentation !== 'time'; + if (needsChange) { + datetimeEl.presentation = 'time'; + needsPresentationChange = true; + } + break; + } + + /** + * Track which button was clicked + * so that it can have the correct + * activated styles applied when + * the modal/popover containing + * the datetime is opened. + */ + this.selectedButton = 'time'; + + this.presentOverlay(ev, needsPresentationChange, this.timeTargetEl); + }; + + /** + * If the datetime is presented in an + * overlay, the datetime and overlay + * should be appropriately sized. + * These classes provide default sizing values + * that developers can customize. + * The goal is to provide an overlay that is + * reasonably sized with a datetime that + * fills the entire container. + */ + private presentOverlay = async (ev: Event, needsPresentationChange: boolean, triggerEl?: HTMLElement) => { + const { overlayEl } = this; + + if (!overlayEl) { + return; + } + + if (overlayEl.tagName === 'ION-POPOVER') { + /** + * When the presentation on datetime changes, + * we need to wait for the component to re-render + * otherwise the computed width/height of the + * popover content will be wrong, causing + * the popover to not align with the trigger element. + */ + + if (needsPresentationChange) { + await this.waitForDatetimeChanges(); + } + + /** + * We pass the trigger button element + * so that the popover aligns with the individual + * button that was clicked, not the component container. + */ + (overlayEl as HTMLIonPopoverElement).present({ + ...ev, + detail: { + ionShadowTarget: triggerEl, + }, + } as CustomEvent); + } else { + overlayEl.present(); + } + }; + + render() { + const { color, dateText, timeText, selectedButton, datetimeActive, disabled } = this; + + const mode = getIonMode(this); + + return ( + + {dateText && ( + + )} + + {timeText && ( + + )} + + ); + } +} diff --git a/core/src/components/datetime-button/test/basic/datetime-button.e2e.ts b/core/src/components/datetime-button/test/basic/datetime-button.e2e.ts new file mode 100644 index 0000000000..6654ddf87b --- /dev/null +++ b/core/src/components/datetime-button/test/basic/datetime-button.e2e.ts @@ -0,0 +1,186 @@ +import { expect } from '@playwright/test'; +import { test } from '@utils/test/playwright'; + +test.describe('datetime-button: switching to correct view', () => { + test.beforeEach(async ({ page }, testInfo) => { + test.skip(testInfo.project.metadata.rtl === 'rtl', 'No layout tests'); + test.skip(testInfo.project.metadata.mode === 'ios', 'No mode-specific logic'); + + await page.setContent(` + + + `); + await page.waitForSelector('.datetime-ready'); + }); + test('should switch to a date-only view when the date button is clicked', async ({ page }) => { + const datetime = page.locator('ion-datetime'); + expect(datetime).toHaveJSProperty('presentation', 'date-time'); + + await page.locator('#date-button').click(); + + expect(datetime).toHaveJSProperty('presentation', 'date'); + }); + test('should switch to a time-only view when the time button is clicked', async ({ page }) => { + const datetime = page.locator('ion-datetime'); + expect(datetime).toHaveJSProperty('presentation', 'date-time'); + + await page.locator('#time-button').click(); + + expect(datetime).toHaveJSProperty('presentation', 'time'); + }); +}); + +test.describe('datetime-button: labels', () => { + // eslint-disable-next-line no-empty-pattern + test.beforeEach(({}, testInfo) => { + test.skip(testInfo.project.metadata.rtl === 'rtl', 'No layout tests'); + test.skip(testInfo.project.metadata.mode === 'ios', 'No mode-specific logic'); + }); + test('should set date and time labels in separate buttons', async ({ page }) => { + await page.setContent(` + + + `); + await page.waitForSelector('.datetime-ready'); + + await expect(page.locator('#date-button')).toContainText('Jan 1, 2022'); + await expect(page.locator('#time-button')).toContainText('6:30 AM'); + }); + test('should set only month and year', async ({ page }) => { + await page.setContent(` + + + `); + await page.waitForSelector('.datetime-ready'); + + await expect(page.locator('#date-button')).toContainText('January 2022'); + await expect(page.locator('#time-button')).toBeHidden(); + }); + test('should set only year', async ({ page }) => { + await page.setContent(` + + + `); + await page.waitForSelector('.datetime-ready'); + + await expect(page.locator('#date-button')).toContainText('2022'); + await expect(page.locator('#time-button')).toBeHidden(); + }); + test('should set only month', async ({ page }) => { + await page.setContent(` + + + `); + await page.waitForSelector('.datetime-ready'); + + await expect(page.locator('#date-button')).toContainText('January'); + await expect(page.locator('#time-button')).toBeHidden(); + }); + test('should set only time', async ({ page }) => { + await page.setContent(` + + + `); + await page.waitForSelector('.datetime-ready'); + + await expect(page.locator('#time-button')).toContainText('6:30 AM'); + await expect(page.locator('#date-button')).toBeHidden(); + }); + test('should update the label when the value of the datetime changes', async ({ page }) => { + await page.setContent(` + + + `); + await page.waitForSelector('.datetime-ready'); + + const datetime = page.locator('ion-datetime'); + const dateTarget = page.locator('#date-button'); + + await expect(dateTarget).toContainText('Jan 1, 2022'); + + await datetime.evaluate((el: HTMLIonDatetimeElement) => (el.value = '2023-05-10')); + await page.waitForChanges(); + + await expect(dateTarget).toContainText('May 10, 2023'); + }); +}); + +test.describe('datetime-button: locale', () => { + // eslint-disable-next-line no-empty-pattern + test.beforeEach(({}, testInfo) => { + test.skip(testInfo.project.metadata.rtl === 'rtl', 'No layout tests'); + test.skip(testInfo.project.metadata.mode === 'ios', 'No mode-specific logic'); + }); + test('should use the same locale as datetime', async ({ page }) => { + await page.setContent(` + + + `); + await page.waitForSelector('.datetime-ready'); + + /** + * The entire text reads 1 ene 2022, but some browsers will add + * a period after "ene". Just checking ene allows us to verify the + * behavior while avoiding these cross browser differences. + */ + await expect(page.locator('#date-button')).toContainText(/ene/); + await expect(page.locator('#time-button')).toContainText('6:30'); + }); + test('should respect hour cycle even if different from locale default', async ({ page }) => { + await page.setContent(` + + + `); + await page.waitForSelector('.datetime-ready'); + + await expect(page.locator('#time-button')).toContainText('16:30'); + }); + test('should ignore the timezone when selecting a date', async ({ page }) => { + await page.setContent(` + + + `); + await page.waitForSelector('.datetime-ready'); + + const timeTarget = page.locator('#time-button'); + await expect(timeTarget).toContainText('6:30'); + + const firstOfMonth = page.locator('ion-datetime .calendar-day[data-month="1"][data-day="1"]'); + await firstOfMonth.click(); + await page.waitForChanges(); + + await expect(timeTarget).toContainText('6:30'); + }); +}); + +test.describe('datetime-button: wheel', () => { + // eslint-disable-next-line no-empty-pattern + test.beforeEach(({}, testInfo) => { + test.skip(testInfo.project.metadata.rtl === 'rtl', 'No layout tests'); + test.skip(testInfo.project.metadata.mode === 'ios', 'No mode-specific logic'); + }); + test('should only show a single date button when presentation="date-time" and prefer-wheel="true"', async ({ + page, + }) => { + await page.setContent(` + + + `); + await page.waitForSelector('.datetime-ready'); + + await expect(page.locator('#date-button')).toContainText('Jan 1, 2022 6:30 AM'); + await expect(page.locator('#time-button')).not.toBeVisible(); + }); + test('should only show a single date button when presentation="time-date" and prefer-wheel="true"', async ({ + page, + }) => { + await page.setContent(` + + + `); + await page.waitForSelector('.datetime-ready'); + + await expect(page.locator('#date-button')).toContainText('Jan 1, 2022 6:30 AM'); + await expect(page.locator('#time-button')).not.toBeVisible(); + }); +}); diff --git a/core/src/components/datetime-button/test/basic/index.html b/core/src/components/datetime-button/test/basic/index.html new file mode 100644 index 0000000000..c446544a0a --- /dev/null +++ b/core/src/components/datetime-button/test/basic/index.html @@ -0,0 +1,222 @@ + + + + + Datetime Button - Basic + + + + + + + + + + + + Datetime Button - Basic + + + +
+
+

Date/Time

+ + + Start Date + + + + + + +
+ +
+

Date Only

+ + + Start Date + + + + + + +
+ +
+

Time Only

+ + + Start Time + + + + + + +
+
+

Time Only (24 hour)

+ + + Start Time + + + + + + +
+
+

Time/Date

+ + + Start Date + + + + + + +
+
+

Month

+ + + Start Date + + + + + + +
+
+

Month/Year

+ + + Start Date + + + + + + +
+
+

Year

+ + + Start Date + + + + + + +
+
+

preferWheel / date

+ + + Start Date + + + + + + +
+ +
+

preferWheel / date-time

+ + + Start Date + + + + + + +
+
+
+
+ + diff --git a/core/src/components/datetime-button/test/buttons/index.html b/core/src/components/datetime-button/test/buttons/index.html new file mode 100644 index 0000000000..2b6f1f7bcc --- /dev/null +++ b/core/src/components/datetime-button/test/buttons/index.html @@ -0,0 +1,49 @@ + + + + + Datetime Button - Custom Buttons + + + + + + + + + + + Datetime Button - Custom Buttons + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts new file mode 100644 index 0000000000..c9e23913a6 --- /dev/null +++ b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts @@ -0,0 +1,30 @@ +import { expect } from '@playwright/test'; +import { test } from '@utils/test/playwright'; + +test.describe('datetime-button: disabled buttons', () => { + test('buttons should not be enabled when component is disabled', async ({ page }, testInfo) => { + test.skip(testInfo.project.metadata.rtl === 'rtl', 'No layout tests'); + test.skip(testInfo.project.metadata.mode === 'ios', 'No mode-specific logic'); + + await page.setContent(` + + + `); + await page.waitForSelector('.datetime-ready'); + + await expect(page.locator('#date-button')).toBeDisabled(); + await expect(page.locator('#time-button')).toBeDisabled(); + }); + test('buttons should visually be disabled', async ({ page }) => { + await page.setContent(` + + + `); + await page.waitForSelector('.datetime-ready'); + + const datetimeButton = page.locator('ion-datetime-button'); + expect(await datetimeButton.screenshot()).toMatchSnapshot( + `datetime-button-disabled-${page.getSnapshotSettings()}.png` + ); + }); +}); diff --git a/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..ea6feb18f4 Binary files /dev/null and b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..4d16e9d498 Binary files /dev/null and b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-ios-ltr-Mobile-Safari-linux.png b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 0000000000..38a885fa46 Binary files /dev/null and b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-ios-rtl-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..a86e4b54a1 Binary files /dev/null and b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-ios-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-ios-rtl-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..acbb4da0db Binary files /dev/null and b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-ios-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-ios-rtl-Mobile-Safari-linux.png b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-ios-rtl-Mobile-Safari-linux.png new file mode 100644 index 0000000000..bfc90a5912 Binary files /dev/null and b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-ios-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-md-ltr-Mobile-Chrome-linux.png b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..0063e48b2f Binary files /dev/null and b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-md-ltr-Mobile-Firefox-linux.png b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..d73f47ad46 Binary files /dev/null and b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-md-ltr-Mobile-Safari-linux.png b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 0000000000..a8ebc64e22 Binary files /dev/null and b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-md-rtl-Mobile-Chrome-linux.png b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-md-rtl-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..b10eff5574 Binary files /dev/null and b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-md-rtl-Mobile-Firefox-linux.png b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-md-rtl-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..8f9e8b1931 Binary files /dev/null and b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-md-rtl-Mobile-Safari-linux.png b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-md-rtl-Mobile-Safari-linux.png new file mode 100644 index 0000000000..f70372d833 Binary files /dev/null and b/core/src/components/datetime-button/test/disabled/datetime-button.e2e.ts-snapshots/datetime-button-disabled-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/datetime-button/test/disabled/index.html b/core/src/components/datetime-button/test/disabled/index.html new file mode 100644 index 0000000000..a6bd1dc7ea --- /dev/null +++ b/core/src/components/datetime-button/test/disabled/index.html @@ -0,0 +1,74 @@ + + + + + Datetime Button - Disabled + + + + + + + + + + + + Datetime Button - Disabled + + + +
+
+

Datetime Button Disabled

+ + + Start Date + + + + + + +
+
+

Custom Button Disabled

+ + + Start Date + + + + + + + + + +
+
+
+
+ + diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts new file mode 100644 index 0000000000..49d18c5637 --- /dev/null +++ b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts @@ -0,0 +1,162 @@ +import { expect } from '@playwright/test'; +import type { Locator } from '@playwright/test'; +import { test } from '@utils/test/playwright'; +import type { EventSpy } from '@utils/test/playwright'; + +test.describe('datetime-button: rendering', () => { + test('should size the modal correctly', async ({ page }) => { + await page.setContent(` + + + + + `); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + const dateButton = page.locator('ion-datetime-button #date-button'); + await dateButton.click(); + await ionModalDidPresent.next(); + + expect(await page.screenshot()).toMatchSnapshot(`datetime-overlay-modal-${page.getSnapshotSettings()}.png`); + }); + + test('should size the popover correctly', async ({ page }) => { + await page.setContent(` + + + + + `); + + const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent'); + const dateButton = page.locator('ion-datetime-button #date-button'); + await dateButton.click(); + await ionPopoverDidPresent.next(); + + expect(await page.screenshot()).toMatchSnapshot(`datetime-overlay-popover-${page.getSnapshotSettings()}.png`); + }); +}); + +test.describe('datetime-button: popover', () => { + let datetime: Locator; + let popover: Locator; + let ionPopoverDidPresent: EventSpy; + let ionPopoverDidDismiss: EventSpy; + test.beforeEach(async ({ page }, testInfo) => { + test.skip(testInfo.project.metadata.rtl === 'rtl', 'No layout tests'); + test.skip(testInfo.project.metadata.mode === 'ios', 'No mode-specific logic'); + + await page.setContent(` + + + + + + `); + + datetime = page.locator('ion-datetime'); + popover = page.locator('ion-popover'); + ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent'); + ionPopoverDidDismiss = await page.spyOnEvent('ionPopoverDidDismiss'); + }); + test('should open the date popover', async ({ page }) => { + await page.locator('#date-button').click(); + + await ionPopoverDidPresent.next(); + + expect(datetime).toBeVisible(); + }); + test('should open the time popover', async ({ page }) => { + await page.locator('#time-button').click(); + + await ionPopoverDidPresent.next(); + + expect(datetime).toBeVisible(); + }); + test('should open the date popover then the time popover', async ({ page }) => { + await page.locator('#date-button').click(); + await ionPopoverDidPresent.next(); + expect(datetime).toBeVisible(); + + await popover.evaluate((el: HTMLIonPopoverElement) => el.dismiss()); + await ionPopoverDidDismiss.next(); + + await page.locator('#time-button').click(); + await ionPopoverDidPresent.next(); + expect(datetime).toBeVisible(); + }); + test('should open the time popover then the date popover', async ({ page }) => { + await page.locator('#time-button').click(); + await ionPopoverDidPresent.next(); + expect(datetime).toBeVisible(); + + await popover.evaluate((el: HTMLIonPopoverElement) => el.dismiss()); + await ionPopoverDidDismiss.next(); + + await page.locator('#date-button').click(); + await ionPopoverDidPresent.next(); + expect(datetime).toBeVisible(); + }); +}); + +test.describe('datetime-button: modal', () => { + let datetime: Locator; + let modal: Locator; + let ionModalDidPresent: EventSpy; + let ionModalDidDismiss: EventSpy; + test.beforeEach(async ({ page }, testInfo) => { + test.skip(testInfo.project.metadata.rtl === 'rtl', 'No layout tests'); + test.skip(testInfo.project.metadata.mode === 'ios', 'No mode-specific logic'); + + await page.setContent(` + + + + + + `); + + datetime = page.locator('ion-datetime'); + modal = page.locator('ion-modal'); + ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss'); + }); + test('should open the date modal', async ({ page }) => { + await page.locator('#date-button').click(); + + await ionModalDidPresent.next(); + + expect(datetime).toBeVisible(); + }); + test('should open the time modal', async ({ page }) => { + await page.locator('#time-button').click(); + + await ionModalDidPresent.next(); + + expect(datetime).toBeVisible(); + }); + test('should open the date modal then the time modal', async ({ page }) => { + await page.locator('#date-button').click(); + await ionModalDidPresent.next(); + expect(datetime).toBeVisible(); + + await modal.evaluate((el: HTMLIonModalElement) => el.dismiss()); + await ionModalDidDismiss.next(); + + await page.locator('#time-button').click(); + await ionModalDidPresent.next(); + expect(datetime).toBeVisible(); + }); + test('should open the time modal then the date modal', async ({ page }) => { + await page.locator('#time-button').click(); + await ionModalDidPresent.next(); + expect(datetime).toBeVisible(); + + await modal.evaluate((el: HTMLIonModalElement) => el.dismiss()); + await ionModalDidDismiss.next(); + + await page.locator('#date-button').click(); + await ionModalDidPresent.next(); + expect(datetime).toBeVisible(); + }); +}); diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..a7e751103a Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..17ae661d72 Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-ios-ltr-Mobile-Safari-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 0000000000..9fbd2ed454 Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-ios-rtl-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..e226527ecb Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-ios-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-ios-rtl-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..a9920562b4 Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-ios-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-ios-rtl-Mobile-Safari-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-ios-rtl-Mobile-Safari-linux.png new file mode 100644 index 0000000000..ba7b0872ed Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-ios-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-md-ltr-Mobile-Chrome-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..7e9514c23b Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-md-ltr-Mobile-Firefox-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..e730ef1879 Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-md-ltr-Mobile-Safari-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 0000000000..93e1117e64 Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-md-rtl-Mobile-Chrome-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-md-rtl-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..b7e5ce0647 Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-md-rtl-Mobile-Firefox-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-md-rtl-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..7a986b79bc Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-md-rtl-Mobile-Safari-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-md-rtl-Mobile-Safari-linux.png new file mode 100644 index 0000000000..8281b2fd15 Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-modal-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..85159b4fcf Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..b14b873670 Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-ios-ltr-Mobile-Safari-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 0000000000..cab65a42bc Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-ios-rtl-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..366bcc821d Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-ios-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-ios-rtl-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..14de14bda4 Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-ios-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-ios-rtl-Mobile-Safari-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-ios-rtl-Mobile-Safari-linux.png new file mode 100644 index 0000000000..17f42dadb2 Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-ios-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-md-ltr-Mobile-Chrome-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..38ff7b13da Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-md-ltr-Mobile-Firefox-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..3138117aa3 Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-md-ltr-Mobile-Safari-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 0000000000..5d2e271a42 Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-md-rtl-Mobile-Chrome-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-md-rtl-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..4c473322fb Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-md-rtl-Mobile-Firefox-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-md-rtl-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..85edfe5507 Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-md-rtl-Mobile-Safari-linux.png b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-md-rtl-Mobile-Safari-linux.png new file mode 100644 index 0000000000..13afce1f84 Binary files /dev/null and b/core/src/components/datetime-button/test/overlays/datetime-button.e2e.ts-snapshots/datetime-overlay-popover-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/datetime-button/test/overlays/index.html b/core/src/components/datetime-button/test/overlays/index.html new file mode 100644 index 0000000000..78722979f2 --- /dev/null +++ b/core/src/components/datetime-button/test/overlays/index.html @@ -0,0 +1,118 @@ + + + + + Datetime Button - Overlays + + + + + + + + + + + + Datetime Button - Overlays + + + +
+
+

Popover - Default

+ + + + + + + + +
+
+

Modal - Default

+ + + + + + + + +
+
+

Popover - Custom

+ + + + + + + +
Custom select a Date
+
+
+
+
+

Modal - Custom

+ + + + + + + +
Custom select a Date
+
+
+
+
+
+
+ + diff --git a/core/src/components/datetime-button/test/style/index.html b/core/src/components/datetime-button/test/style/index.html new file mode 100644 index 0000000000..f4bb659a85 --- /dev/null +++ b/core/src/components/datetime-button/test/style/index.html @@ -0,0 +1,40 @@ + + + + + Datetime Button - Custom Style + + + + + + + + + + + Datetime Button - Custom Style + + + + + + + + + + + + + + diff --git a/core/src/components/datetime/datetime-interface.ts b/core/src/components/datetime/datetime-interface.ts index 72fc4f3b1d..8c2ce79da8 100644 --- a/core/src/components/datetime/datetime-interface.ts +++ b/core/src/components/datetime/datetime-interface.ts @@ -21,3 +21,5 @@ export interface DatetimeParts { ampm?: 'am' | 'pm'; tzOffset?: number; } + +export type DatetimePresentation = 'date-time' | 'time-date' | 'date' | 'time' | 'month' | 'year' | 'month-year'; diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index ec4732a437..5ed5967bc9 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -3,7 +3,14 @@ import { Component, Element, Event, Host, Method, Prop, State, Watch, h, writeTa import { caretDownSharp, caretUpSharp, chevronBack, chevronDown, chevronForward } from 'ionicons/icons'; import { getIonMode } from '../../global/ionic-global'; -import type { Color, DatetimeChangeEventDetail, DatetimeParts, Mode, StyleEventDetail } from '../../interface'; +import type { + Color, + DatetimePresentation, + DatetimeChangeEventDetail, + DatetimeParts, + Mode, + StyleEventDetail, +} from '../../interface'; import { startFocusVisible } from '../../utils/focus-visible'; import { getElementRoot, raf, renderHiddenInput } from '../../utils/helpers'; import { printIonError, printIonWarning } from '../../utils/logging'; @@ -204,7 +211,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' | 'month' | 'year' | 'month-year' = 'date-time'; + @Prop() presentation: DatetimePresentation = 'date-time'; /** * The text to display on the picker's cancel button. @@ -469,6 +476,12 @@ export class Datetime implements ComponentInterface { */ @Event() ionStyle!: EventEmitter; + /** + * Emitted when componentDidRender is fired. + * @internal + */ + @Event() ionRender!: EventEmitter; + /** * Confirms the selected datetime value, updates the * `value` property, and optionally closes the popover @@ -1114,6 +1127,10 @@ export class Datetime implements ComponentInterface { this.destroyInteractionListeners(); this.initializeListeners(); + + raf(() => { + this.ionRender.emit(); + }); } private processValue = (value?: string | string[] | null) => { @@ -2130,8 +2147,9 @@ export class Datetime implements ComponentInterface { presentation === 'year' || presentation === 'month' || presentation === 'month-year'; const shouldShowMonthAndYear = showMonthAndYear || isMonthAndYearPresentation; const monthYearPickerOpen = showMonthAndYear && !isMonthAndYearPresentation; - const hasWheelVariant = - (presentation === 'date' || presentation === 'date-time' || presentation === 'time-date') && preferWheel; + const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date'; + const hasWheelVariant = hasDatePresentation && preferWheel; + const hasGrid = hasDatePresentation && !preferWheel; renderHiddenInput(true, el, name, formatValue(value), disabled); @@ -2151,6 +2169,7 @@ export class Datetime implements ComponentInterface { [`datetime-presentation-${presentation}`]: true, [`datetime-size-${size}`]: true, [`datetime-prefer-wheel`]: hasWheelVariant, + [`datetime-grid`]: hasGrid, }), }} > diff --git a/core/src/components/datetime/utils/format.ts b/core/src/components/datetime/utils/format.ts index 031a85aec3..64747f770e 100644 --- a/core/src/components/datetime/utils/format.ts +++ b/core/src/components/datetime/utils/format.ts @@ -100,6 +100,32 @@ export const getMonthAndYear = (locale: string, refParts: DatetimeParts) => { return new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric', timeZone: 'UTC' }).format(date); }; +/** + * Given a locale and a date object, + * return a formatted string that includes + * the short month, numeric day, and full year. + * Example: Apr 22, 2021 + */ +export const getMonthDayAndYear = (locale: string, refParts: DatetimeParts) => { + return getLocalizedDateTime(locale, refParts, { month: 'short', day: 'numeric', year: 'numeric' }); +}; + +/** + * Wrapper function for Intl.DateTimeFormat. + * Allows developers to apply an allowed format to DatetimeParts. + * This function also has built in safeguards for older browser bugs + * with Intl.DateTimeFormat. + */ +export const getLocalizedDateTime = ( + locale: string, + refParts: DatetimeParts, + options: Intl.DateTimeFormatOptions +): string => { + const timeString = !!refParts.hour && !!refParts.minute ? ` ${refParts.hour}:${refParts.minute}` : ''; + const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year}${timeString} GMT+0000`); + return new Intl.DateTimeFormat(locale, { ...options, timeZone: 'UTC' }).format(date); +}; + /** * Gets a localized version of "Today" * Falls back to "Today" in English for diff --git a/core/src/css/core.scss b/core/src/css/core.scss index 2ba1779c8b..9291dba0d6 100644 --- a/core/src/css/core.scss +++ b/core/src/css/core.scss @@ -339,3 +339,26 @@ ion-accordion-group.accordion-group-expand-inset.md > ion-accordion.accordion-ex ion-input input::-webkit-date-and-time-value { text-align: start; } + +/** + * The .ion-datetime-button-overlay class contains + * styles that allow any modal/popover to be + * sized according to the dimensions of the datetime + * when used with ion-datetime-button. + */ +.ion-datetime-button-overlay { + --width: fit-content; + --height: fit-content; +} + +/** + * The grid variant can scale down when inline. + * When used in a `fit-content` overlay, this causes + * the overlay to shrink when the month/year picker is open. + * Explicitly setting the dimensions lets us have a consistently + * sized grid interface. + */ +.ion-datetime-button-overlay ion-datetime.datetime-grid { + width: 320px; + min-height: 320px; +} diff --git a/packages/react/src/components/proxies.ts b/packages/react/src/components/proxies.ts index ec9e044fe9..dd568efaa6 100644 --- a/packages/react/src/components/proxies.ts +++ b/packages/react/src/components/proxies.ts @@ -21,6 +21,7 @@ import { defineCustomElement as defineIonChip } from '@ionic/core/components/ion import { defineCustomElement as defineIonCol } from '@ionic/core/components/ion-col.js'; import { defineCustomElement as defineIonContent } from '@ionic/core/components/ion-content.js'; import { defineCustomElement as defineIonDatetime } from '@ionic/core/components/ion-datetime.js'; +import { defineCustomElement as defineIonDatetimeButton } from '@ionic/core/components/ion-datetime-button.js'; import { defineCustomElement as defineIonFab } from '@ionic/core/components/ion-fab.js'; import { defineCustomElement as defineIonFabList } from '@ionic/core/components/ion-fab-list.js'; import { defineCustomElement as defineIonFooter } from '@ionic/core/components/ion-footer.js'; @@ -88,6 +89,7 @@ export const IonChip = /*@__PURE__*/createReactComponent('ion-col', undefined, undefined, defineIonCol); export const IonContent = /*@__PURE__*/createReactComponent('ion-content', undefined, undefined, defineIonContent); export const IonDatetime = /*@__PURE__*/createReactComponent('ion-datetime', undefined, undefined, defineIonDatetime); +export const IonDatetimeButton = /*@__PURE__*/createReactComponent('ion-datetime-button', undefined, undefined, defineIonDatetimeButton); export const IonFab = /*@__PURE__*/createReactComponent('ion-fab', undefined, undefined, defineIonFab); export const IonFabList = /*@__PURE__*/createReactComponent('ion-fab-list', undefined, undefined, defineIonFabList); export const IonFooter = /*@__PURE__*/createReactComponent('ion-footer', undefined, undefined, defineIonFooter); diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 44cd498924..db99f1e905 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -24,6 +24,7 @@ import { defineCustomElement as defineIonChip } from '@ionic/core/components/ion import { defineCustomElement as defineIonCol } from '@ionic/core/components/ion-col.js'; import { defineCustomElement as defineIonContent } from '@ionic/core/components/ion-content.js'; import { defineCustomElement as defineIonDatetime } from '@ionic/core/components/ion-datetime.js'; +import { defineCustomElement as defineIonDatetimeButton } from '@ionic/core/components/ion-datetime-button.js'; import { defineCustomElement as defineIonFab } from '@ionic/core/components/ion-fab.js'; import { defineCustomElement as defineIonFabButton } from '@ionic/core/components/ion-fab-button.js'; import { defineCustomElement as defineIonFabList } from '@ionic/core/components/ion-fab-list.js'; @@ -298,11 +299,19 @@ export const IonDatetime = /*@__PURE__*/ defineContainer('ion-d 'ionChange', 'ionFocus', 'ionBlur', - 'ionStyle' + 'ionStyle', + 'ionRender' ], 'value', 'v-ion-change', 'ionChange'); +export const IonDatetimeButton = /*@__PURE__*/ defineContainer('ion-datetime-button', defineIonDatetimeButton, [ + 'color', + 'disabled', + 'datetime' +]); + + export const IonFab = /*@__PURE__*/ defineContainer('ion-fab', defineIonFab, [ 'horizontal', 'vertical',