chore(): merge feature-6.2 into main

chore(): merge feature-6.2 into main
This commit is contained in:
Liam DeBeasi
2022-07-27 11:12:22 -04:00
committed by GitHub
394 changed files with 5594 additions and 618 deletions

View File

@ -54,6 +54,7 @@ export declare interface IonModal extends Components.IonModal {
@ProxyCmp({
inputs: [
'animated',
'keepContentsMounted',
'backdropBreakpoint',
'backdropDismiss',
'breakpoints',
@ -62,6 +63,7 @@ export declare interface IonModal extends Components.IonModal {
'enterAnimation',
'event',
'handle',
'handleBehavior',
'initialBreakpoint',
'isOpen',
'keyboardClose',
@ -78,9 +80,12 @@ export declare interface IonModal extends Components.IonModal {
@Component({
selector: 'ion-modal',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<div class="ion-page" *ngIf="isCmpOpen"><ng-container [ngTemplateOutlet]="template"></ng-container></div>`,
template: `<div class="ion-page" *ngIf="isCmpOpen || keepContentsMounted">
<ng-container [ngTemplateOutlet]="template"></ng-container>
</div>`,
inputs: [
'animated',
'keepContentsMounted',
'backdropBreakpoint',
'backdropDismiss',
'breakpoints',
@ -89,6 +94,7 @@ export declare interface IonModal extends Components.IonModal {
'enterAnimation',
'event',
'handle',
'handleBehavior',
'initialBreakpoint',
'isOpen',
'keyboardClose',

View File

@ -51,6 +51,7 @@ export declare interface IonPopover extends Components.IonPopover {
'alignment',
'animated',
'arrow',
'keepContentsMounted',
'backdropDismiss',
'cssClass',
'dismissOnSelect',
@ -73,11 +74,12 @@ export declare interface IonPopover extends Components.IonPopover {
@Component({
selector: 'ion-popover',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<ng-container [ngTemplateOutlet]="template" *ngIf="isCmpOpen"></ng-container>`,
template: `<ng-container [ngTemplateOutlet]="template" *ngIf="isCmpOpen || keepContentsMounted"></ng-container>`,
inputs: [
'alignment',
'animated',
'arrow',
'keepContentsMounted',
'backdropDismiss',
'cssClass',
'dismissOnSelect',

View File

@ -23,6 +23,7 @@ export const DIRECTIVES = [
d.IonCol,
d.IonContent,
d.IonDatetime,
d.IonDatetimeButton,
d.IonFab,
d.IonFabButton,
d.IonFabList,

View File

@ -524,14 +524,14 @@ export declare interface IonDatetime extends Components.IonDatetime {
@ProxyCmp({
defineCustomElementFn: undefined,
inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'name', 'presentation', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'value', 'yearValues'],
inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'multiple', 'name', 'preferWheel', 'presentation', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'value', 'yearValues'],
methods: ['confirm', 'reset', 'cancel']
})
@Component({
selector: 'ion-datetime',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'name', 'presentation', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'value', 'yearValues']
inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'multiple', 'name', 'preferWheel', 'presentation', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'value', 'yearValues']
})
export class IonDatetime {
protected el: HTMLElement;
@ -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: '<ng-content></ng-content>',
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({
@ -1304,13 +1325,13 @@ mouse drag, touch gesture, or keyboard interaction.
@ProxyCmp({
defineCustomElementFn: undefined,
inputs: ['color', 'debounce', 'disabled', 'dualKnobs', 'max', 'min', 'mode', 'name', 'pin', 'pinFormatter', 'snaps', 'step', 'ticks', 'value']
inputs: ['activeBarStart', 'color', 'debounce', 'disabled', 'dualKnobs', 'max', 'min', 'mode', 'name', 'pin', 'pinFormatter', 'snaps', 'step', 'ticks', 'value']
})
@Component({
selector: 'ion-range',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
inputs: ['color', 'debounce', 'disabled', 'dualKnobs', 'max', 'min', 'mode', 'name', 'pin', 'pinFormatter', 'snaps', 'step', 'ticks', 'value']
inputs: ['activeBarStart', 'color', 'debounce', 'disabled', 'dualKnobs', 'max', 'min', 'mode', 'name', 'pin', 'pinFormatter', 'snaps', 'step', 'ticks', 'value']
})
export class IonRange {
protected el: HTMLElement;
@ -1979,13 +2000,13 @@ export declare interface IonToggle extends Components.IonToggle {
@ProxyCmp({
defineCustomElementFn: undefined,
inputs: ['checked', 'color', 'disabled', 'mode', 'name', 'value']
inputs: ['checked', 'color', 'disabled', 'enableOnOffLabels', 'mode', 'name', 'value']
})
@Component({
selector: 'ion-toggle',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
inputs: ['checked', 'color', 'disabled', 'mode', 'name', 'value']
inputs: ['checked', 'color', 'disabled', 'enableOnOffLabels', 'mode', 'name', 'value']
})
export class IonToggle {
protected el: HTMLElement;

View File

@ -0,0 +1,60 @@
describe('overlays - keepContentsMounted', () => {
describe('modal', () => {
it('should not mount component if false', () => {
cy.visit('/modal-inline');
cy.get('ion-modal ion-content').should('not.exist');
});
it('should mount component if true', () => {
cy.visit('/keep-contents-mounted');
cy.get('ion-modal ion-content').should('exist');
});
it('should keep component mounted after dismissing if true', () => {
cy.visit('/keep-contents-mounted');
cy.get('#open-modal').click();
cy.get('ion-modal ion-content').should('exist');
cy.get('ion-modal ion-button').click();
cy.get('ion-modal')
.should('not.be.visible')
.should('have.class', 'overlay-hidden');
cy.get('ion-modal ion-content').should('exist');
});
})
describe('popover', () => {
it('should not mount component if false', () => {
cy.visit('/popover-inline');
cy.get('ion-popover ion-content').should('not.exist');
});
it('should mount component if true', () => {
cy.visit('/keep-contents-mounted');
cy.get('ion-popover ion-content').should('exist');
});
it('should keep component mounted after dismissing if true', () => {
cy.visit('/keep-contents-mounted');
cy.get('#open-popover').click();
cy.get('ion-popover ion-content').should('exist');
cy.get('ion-popover ion-button').click();
cy.get('ion-popover')
.should('not.be.visible')
.should('have.class', 'overlay-hidden');
cy.get('ion-popover ion-content').should('exist');
});
});
});

View File

@ -4,10 +4,17 @@ describe('Popovers: Inline', () => {
});
it('should initially have no items', () => {
cy.get('ion-button').click();
cy.get('ion-popover').should('be.visible');
cy.get('ion-list ion-item').should('not.exist');
});
it('should have items after 1500ms', () => {
cy.get('ion-button').click();
cy.get('ion-popover').should('be.visible');
cy.wait(1500);
cy.get('ion-list ion-item:nth-child(1)').should('have.text', 'A');

View File

@ -31,6 +31,7 @@ const routes: Routes = [
{ path: 'modals', component: ModalComponent },
{ path: 'modal-inline', loadChildren: () => import('./modal-inline').then(m => m.ModalInlineModule) },
{ path: 'view-child', component: ViewChildComponent },
{ path: 'keep-contents-mounted', loadChildren: () => import('./keep-contents-mounted').then(m => m.OverlayAutoMountModule) },
{ path: 'popover-inline', loadChildren: () => import('./popover-inline').then(m => m.PopoverInlineModule) },
{ path: 'providers', component: ProvidersComponent },
{ path: 'router-link', component: RouterLinkComponent },

View File

@ -0,0 +1,2 @@
export * from './keep-contents-mounted.component';
export * from './keep-contents-mounted.module';

View File

@ -0,0 +1,16 @@
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";
import { OverlayKeepContentsMounted } from ".";
@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
component: OverlayKeepContentsMounted
}
])
],
exports: [RouterModule]
})
export class OverlayKeepContentsMountedRoutingModule { }

View File

@ -0,0 +1,22 @@
<ion-content>
<ion-button id="open-modal" (click)="modal.present()">Open Modal</ion-button>
<ion-button id="open-popover" (click)="popover.present()">Open Popover</ion-button>
<ion-modal [keepContentsMounted]="true" #modal>
<ng-template>
<ion-content>
<ion-button (click)="modal.dismiss()">Dismiss</ion-button>
Modal Content
</ion-content>
</ng-template>
</ion-modal>
<ion-popover [keepContentsMounted]="true" #popover>
<ng-template>
<ion-content>
<ion-button (click)="popover.dismiss()">Dismiss</ion-button>
Popover Content
</ion-content>
</ng-template>
</ion-popover>
</ion-content>

View File

@ -0,0 +1,13 @@
import { Component } from "@angular/core";
/**
* Validates that inline modals correctly mount
* inner components when keepContentsMounted is
* enabled.
*/
@Component({
selector: 'app-keep-contents-mounted',
templateUrl: 'keep-contents-mounted.component.html'
})
export class OverlayKeepContentsMounted {
}

View File

@ -0,0 +1,12 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { IonicModule } from "@ionic/angular";
import { OverlayKeepContentsMountedRoutingModule } from "./keep-contents-mounted-routing.module";
import { OverlayKeepContentsMounted } from "./keep-contents-mounted.component";
@NgModule({
imports: [CommonModule, IonicModule, OverlayKeepContentsMountedRoutingModule],
declarations: [OverlayKeepContentsMounted],
exports: [OverlayKeepContentsMounted]
})
export class OverlayAutoMountModule { }

View File

@ -1,4 +1,6 @@
<ion-popover [isOpen]="true">
<ion-button (click)="openPopover(popover)">Open Popover</ion-button>
<ion-popover #popover>
<ng-template>
<ion-content>
<ion-list>

View File

@ -1,4 +1,4 @@
import { AfterViewInit, Component } from "@angular/core";
import { Component } from "@angular/core";
/**
* Validates that inline popovers will correctly display
@ -9,11 +9,13 @@ import { AfterViewInit, Component } from "@angular/core";
selector: 'app-popover-inline',
templateUrl: 'popover-inline.component.html'
})
export class PopoverInlineComponent implements AfterViewInit {
export class PopoverInlineComponent {
items: string[] = [];
ngAfterViewInit(): void {
openPopover(popover: HTMLIonPopoverElement) {
popover.present();
setTimeout(() => {
this.items = ['A', 'B', 'C', 'D'];
}, 1000);

View File

@ -387,7 +387,9 @@ ion-datetime,prop,min,string | undefined,undefined,false,false
ion-datetime,prop,minuteValues,number | number[] | string | undefined,undefined,false,false
ion-datetime,prop,mode,"ios" | "md",undefined,false,false
ion-datetime,prop,monthValues,number | number[] | string | undefined,undefined,false,false
ion-datetime,prop,multiple,boolean,false,false,false
ion-datetime,prop,name,string,this.inputId,false,false
ion-datetime,prop,preferWheel,boolean,false,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,showClearButton,boolean,false,false,false
@ -395,7 +397,7 @@ ion-datetime,prop,showDefaultButtons,boolean,false,false,false
ion-datetime,prop,showDefaultTimeLabel,boolean,true,false,false
ion-datetime,prop,showDefaultTitle,boolean,false,false,false
ion-datetime,prop,size,"cover" | "fixed",'fixed',false,false
ion-datetime,prop,value,null | string | undefined,undefined,false,false
ion-datetime,prop,value,null | string | string[] | undefined,undefined,false,false
ion-datetime,prop,yearValues,number | number[] | string | undefined,undefined,false,false
ion-datetime,method,cancel,cancel(closeOverlay?: boolean) => Promise<void>
ion-datetime,method,confirm,confirm(closeOverlay?: boolean) => Promise<void>
@ -408,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<never, never> | 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
@ -772,9 +781,11 @@ ion-modal,prop,breakpoints,number[] | undefined,undefined,false,false
ion-modal,prop,canDismiss,(() => Promise<boolean>) | boolean | undefined,undefined,false,false
ion-modal,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
ion-modal,prop,handle,boolean | undefined,undefined,false,false
ion-modal,prop,handleBehavior,"cycle" | "none" | undefined,'none',false,false
ion-modal,prop,htmlAttributes,undefined | { [key: string]: any; },undefined,false,false
ion-modal,prop,initialBreakpoint,number | undefined,undefined,false,false
ion-modal,prop,isOpen,boolean,false,false,false
ion-modal,prop,keepContentsMounted,boolean,false,false,false
ion-modal,prop,keyboardClose,boolean,true,false,false
ion-modal,prop,leaveAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
ion-modal,prop,mode,"ios" | "md",undefined,false,false
@ -894,6 +905,7 @@ ion-popover,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undef
ion-popover,prop,event,any,undefined,false,false
ion-popover,prop,htmlAttributes,undefined | { [key: string]: any; },undefined,false,false
ion-popover,prop,isOpen,boolean,false,false,false
ion-popover,prop,keepContentsMounted,boolean,false,false,false
ion-popover,prop,keyboardClose,boolean,true,false,false
ion-popover,prop,leaveAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
ion-popover,prop,mode,"ios" | "md",undefined,false,false
@ -967,6 +979,7 @@ ion-radio-group,prop,value,any,undefined,false,false
ion-radio-group,event,ionChange,RadioGroupChangeEventDetail<any>,true
ion-range,shadow
ion-range,prop,activeBarStart,number | undefined,undefined,false,false
ion-range,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
ion-range,prop,debounce,number,0,false,false
ion-range,prop,disabled,boolean,false,false,false
@ -1413,6 +1426,7 @@ ion-toggle,shadow
ion-toggle,prop,checked,boolean,false,false,false
ion-toggle,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
ion-toggle,prop,disabled,boolean,false,false,false
ion-toggle,prop,enableOnOffLabels,boolean | undefined,undefined,false,false
ion-toggle,prop,mode,"ios" | "md",undefined,false,false
ion-toggle,prop,name,string,this.inputId,false,false
ion-toggle,prop,value,null | string | undefined,'on',false,false

View File

@ -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, 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";
@ -785,14 +785,22 @@ export namespace Components {
* Values used to create the list of selectable months. By default the month values range from `1` to `12`. However, to control exactly which months to display, the `monthValues` input can take a number, an array of numbers, or a string of comma separated numbers. For example, if only summer months should be shown, then this input value would be `monthValues="6,7,8"`. Note that month numbers do *not* have a zero-based index, meaning January's value is `1`, and December's is `12`.
*/
"monthValues"?: number[] | number | string;
/**
* If `true`, multiple dates can be selected at once. Only applies to `presentation="date"` and `preferWheel="false"`.
*/
"multiple": boolean;
/**
* The name of the control, which is submitted with the form data.
*/
"name": string;
/**
* If `true`, a wheel picker will be rendered instead of a calendar grid where possible. If `false`, a calendar grid will be rendered instead of a wheel picker where possible. A wheel picker can be rendered instead of a grid when `presentation` is one of the following values: `'date'`, `'date-time'`, or `'time-date'`. A wheel picker will always be rendered regardless of the `preferWheel` value when `presentation` is one of the following values: `'time'`, `'month'`, `'month-year'`, or `'year'`.
*/
"preferWheel": boolean;
/**
* 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.
*/
@ -822,14 +830,32 @@ export namespace Components {
*/
"size": 'cover' | 'fixed';
/**
* The value of the datetime as a valid ISO 8601 datetime string.
* The value of the datetime as a valid ISO 8601 datetime string. Should be an array of strings if `multiple="true"`.
*/
"value"?: string | null;
"value"?: string | string[] | null;
/**
* Values used to create the list of selectable years. By default the year values range between the `min` and `max` datetime inputs. However, to control exactly which years to display, the `yearValues` input can take a number, an array of numbers, or string of comma separated numbers. For example, to show upcoming and recent leap years, then this input's value would be `yearValues="2024,2020,2016,2012,2008"`.
*/
"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.
@ -1554,6 +1580,10 @@ export namespace Components {
* The horizontal line that displays at the top of a sheet modal. It is `true` by default when setting the `breakpoints` and `initialBreakpoint` properties.
*/
"handle"?: boolean;
/**
* The interaction behavior for the sheet modal when the handle is pressed. Defaults to `"none"`, which means the modal will not change size or position when the handle is pressed. Set to `"cycle"` to let the modal cycle between available breakpoints when pressed. Handle behavior is unavailable when the `handle` property is set to `false` or when the `breakpoints` property is not set (using a fullscreen or card modal).
*/
"handleBehavior"?: ModalHandleBehavior;
"hasController": boolean;
/**
* Additional attributes to pass to the modal.
@ -1567,6 +1597,10 @@ export namespace Components {
* If `true`, the modal will open. If `false`, the modal will close. Use this if you need finer grained control over presentation, otherwise just use the modalController or the `trigger` property. Note: `isOpen` will not automatically be set back to `false` when the modal dismisses. You will need to do that in your code.
*/
"isOpen": boolean;
/**
* If `true`, the component passed into `ion-modal` will automatically be mounted when the modal is created. The component will remain mounted even when the modal is dismissed. However, the component will be destroyed when the modal is destroyed. This property is not reactive and should only be used when initially creating a modal. Note: This feature only applies to inline modals in JavaScript frameworks such as Angular, React, and Vue.
*/
"keepContentsMounted": boolean;
/**
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
*/
@ -1936,6 +1970,10 @@ export namespace Components {
* If `true`, the popover will open. If `false`, the popover will close. Use this if you need finer grained control over presentation, otherwise just use the popoverController or the `trigger` property. Note: `isOpen` will not automatically be set back to `false` when the popover dismisses. You will need to do that in your code.
*/
"isOpen": boolean;
/**
* If `true`, the component passed into `ion-popover` will automatically be mounted when the popover is created. The component will remain mounted even when the popover is dismissed. However, the component will be destroyed when the popover is destroyed. This property is not reactive and should only be used when initially creating a popover. Note: This feature only applies to inline popovers in JavaScript frameworks such as Angular, React, and Vue.
*/
"keepContentsMounted": boolean;
/**
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
*/
@ -2060,6 +2098,10 @@ export namespace Components {
"value"?: any | null;
}
interface IonRange {
/**
* The start position of the range active bar. This feature is only available with a single knob (dualKnobs="false"). Valid values are greater than or equal to the min value and less than or equal to the max value.
*/
"activeBarStart"?: number;
/**
* 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).
*/
@ -2967,6 +3009,10 @@ export namespace Components {
* If `true`, the user cannot interact with the toggle.
*/
"disabled": boolean;
/**
* Enables the on/off accessibility switch labels within the toggle.
*/
"enableOnOffLabels": boolean | undefined;
/**
* The mode determines which platform styles to use.
*/
@ -3385,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: {
@ -3829,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;
@ -4688,6 +4741,10 @@ declare namespace LocalJSX {
* Values used to create the list of selectable months. By default the month values range from `1` to `12`. However, to control exactly which months to display, the `monthValues` input can take a number, an array of numbers, or a string of comma separated numbers. For example, if only summer months should be shown, then this input value would be `monthValues="6,7,8"`. Note that month numbers do *not* have a zero-based index, meaning January's value is `1`, and December's is `12`.
*/
"monthValues"?: number[] | number | string;
/**
* If `true`, multiple dates can be selected at once. Only applies to `presentation="date"` and `preferWheel="false"`.
*/
"multiple"?: boolean;
/**
* The name of the control, which is submitted with the form data.
*/
@ -4708,14 +4765,22 @@ declare namespace LocalJSX {
* Emitted when the datetime has focus.
*/
"onIonFocus"?: (event: IonDatetimeCustomEvent<void>) => void;
/**
* Emitted when componentDidRender is fired.
*/
"onIonRender"?: (event: IonDatetimeCustomEvent<void>) => void;
/**
* Emitted when the styles change.
*/
"onIonStyle"?: (event: IonDatetimeCustomEvent<StyleEventDetail>) => void;
/**
* If `true`, a wheel picker will be rendered instead of a calendar grid where possible. If `false`, a calendar grid will be rendered instead of a wheel picker where possible. A wheel picker can be rendered instead of a grid when `presentation` is one of the following values: `'date'`, `'date-time'`, or `'time-date'`. A wheel picker will always be rendered regardless of the `preferWheel` value when `presentation` is one of the following values: `'time'`, `'month'`, `'month-year'`, or `'year'`.
*/
"preferWheel"?: boolean;
/**
* 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.
*/
@ -4741,14 +4806,32 @@ declare namespace LocalJSX {
*/
"size"?: 'cover' | 'fixed';
/**
* The value of the datetime as a valid ISO 8601 datetime string.
* The value of the datetime as a valid ISO 8601 datetime string. Should be an array of strings if `multiple="true"`.
*/
"value"?: string | null;
"value"?: string | string[] | null;
/**
* Values used to create the list of selectable years. By default the year values range between the `min` and `max` datetime inputs. However, to control exactly which years to display, the `yearValues` input can take a number, an array of numbers, or string of comma separated numbers. For example, to show upcoming and recent leap years, then this input's value would be `yearValues="2024,2020,2016,2012,2008"`.
*/
"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.
@ -5467,6 +5550,10 @@ declare namespace LocalJSX {
* The horizontal line that displays at the top of a sheet modal. It is `true` by default when setting the `breakpoints` and `initialBreakpoint` properties.
*/
"handle"?: boolean;
/**
* The interaction behavior for the sheet modal when the handle is pressed. Defaults to `"none"`, which means the modal will not change size or position when the handle is pressed. Set to `"cycle"` to let the modal cycle between available breakpoints when pressed. Handle behavior is unavailable when the `handle` property is set to `false` or when the `breakpoints` property is not set (using a fullscreen or card modal).
*/
"handleBehavior"?: ModalHandleBehavior;
"hasController"?: boolean;
/**
* Additional attributes to pass to the modal.
@ -5480,6 +5567,10 @@ declare namespace LocalJSX {
* If `true`, the modal will open. If `false`, the modal will close. Use this if you need finer grained control over presentation, otherwise just use the modalController or the `trigger` property. Note: `isOpen` will not automatically be set back to `false` when the modal dismisses. You will need to do that in your code.
*/
"isOpen"?: boolean;
/**
* If `true`, the component passed into `ion-modal` will automatically be mounted when the modal is created. The component will remain mounted even when the modal is dismissed. However, the component will be destroyed when the modal is destroyed. This property is not reactive and should only be used when initially creating a modal. Note: This feature only applies to inline modals in JavaScript frameworks such as Angular, React, and Vue.
*/
"keepContentsMounted"?: boolean;
/**
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
*/
@ -5771,6 +5862,10 @@ declare namespace LocalJSX {
* If `true`, the popover will open. If `false`, the popover will close. Use this if you need finer grained control over presentation, otherwise just use the popoverController or the `trigger` property. Note: `isOpen` will not automatically be set back to `false` when the popover dismisses. You will need to do that in your code.
*/
"isOpen"?: boolean;
/**
* If `true`, the component passed into `ion-popover` will automatically be mounted when the popover is created. The component will remain mounted even when the popover is dismissed. However, the component will be destroyed when the popover is destroyed. This property is not reactive and should only be used when initially creating a popover. Note: This feature only applies to inline popovers in JavaScript frameworks such as Angular, React, and Vue.
*/
"keepContentsMounted"?: boolean;
/**
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
*/
@ -5925,6 +6020,10 @@ declare namespace LocalJSX {
"value"?: any | null;
}
interface IonRange {
/**
* The start position of the range active bar. This feature is only available with a single knob (dualKnobs="false"). Valid values are greater than or equal to the min value and less than or equal to the max value.
*/
"activeBarStart"?: number;
/**
* 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).
*/
@ -6903,6 +7002,10 @@ declare namespace LocalJSX {
* If `true`, the user cannot interact with the toggle.
*/
"disabled"?: boolean;
/**
* Enables the on/off accessibility switch labels within the toggle.
*/
"enableOnOffLabels"?: boolean | undefined;
/**
* The mode determines which platform styles to use.
*/
@ -7021,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;
@ -7120,6 +7224,7 @@ declare module "@stencil/core" {
"ion-col": LocalJSX.IonCol & JSXBase.HTMLAttributes<HTMLIonColElement>;
"ion-content": LocalJSX.IonContent & JSXBase.HTMLAttributes<HTMLIonContentElement>;
"ion-datetime": LocalJSX.IonDatetime & JSXBase.HTMLAttributes<HTMLIonDatetimeElement>;
"ion-datetime-button": LocalJSX.IonDatetimeButton & JSXBase.HTMLAttributes<HTMLIonDatetimeButtonElement>;
"ion-fab": LocalJSX.IonFab & JSXBase.HTMLAttributes<HTMLIonFabElement>;
"ion-fab-button": LocalJSX.IonFabButton & JSXBase.HTMLAttributes<HTMLIonFabButtonElement>;
"ion-fab-list": LocalJSX.IonFabList & JSXBase.HTMLAttributes<HTMLIonFabListElement>;

View File

@ -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(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View File

@ -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;
}

View File

@ -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 (
<Host
class={createColorClasses(color, {
[mode]: true,
[`${selectedButton}-active`]: datetimeActive,
['datetime-button-disabled']: disabled,
})}
>
{dateText && (
<button
class="ion-activatable"
id="date-button"
aria-expanded={datetimeActive ? 'true' : 'false'}
onClick={this.handleDateClick}
disabled={disabled}
part="native"
ref={(el) => (this.dateTargetEl = el)}
>
<slot name="date-target">{dateText}</slot>
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
</button>
)}
{timeText && (
<button
class="ion-activatable"
id="time-button"
aria-expanded={datetimeActive ? 'true' : 'false'}
onClick={this.handleTimeClick}
disabled={disabled}
part="native"
ref={(el) => (this.timeTargetEl = el)}
>
<slot name="time-target">{timeText}</slot>
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
</button>
)}
</Host>
);
}
}

View File

@ -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(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" presentation="date-time"></ion-datetime>
`);
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(`
<ion-datetime-button locale="en-US" datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" value="2022-01-01T06:30:00" presentation="date-time"></ion-datetime>
`);
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(`
<ion-datetime-button locale="en-US" datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" value="2022-01-01T06:30:00" presentation="month-year"></ion-datetime>
`);
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(`
<ion-datetime-button locale="en-US" datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" value="2022-01-01T06:30:00" presentation="year"></ion-datetime>
`);
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(`
<ion-datetime-button locale="en-US" datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" value="2022-01-01T06:30:00" presentation="month"></ion-datetime>
`);
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(`
<ion-datetime-button locale="en-US" datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" value="2022-01-01T06:30:00" presentation="time"></ion-datetime>
`);
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(`
<ion-datetime-button locale="en-US" datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" value="2022-01-01T06:30:00" presentation="date"></ion-datetime>
`);
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(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-datetime locale="es-ES" id="datetime" value="2022-01-01T06:30:00" presentation="date-time"></ion-datetime>
`);
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(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-datetime hour-cycle="h23" locale="en-US" id="datetime" value="2022-01-01T16:30:00" presentation="date-time"></ion-datetime>
`);
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(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-datetime locale="en-US" id="datetime" value="2022-01-02T06:30:00" presentation="date-time"></ion-datetime>
`);
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(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-datetime locale="en-US" id="datetime" value="2022-01-01T06:30:00" presentation="date-time" prefer-wheel="true"></ion-datetime>
`);
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(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-datetime locale="en-US" id="datetime" value="2022-01-01T06:30:00" presentation="time-date" prefer-wheel="true"></ion-datetime>
`);
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();
});
});

View File

@ -0,0 +1,222 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Datetime Button - Basic</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(325px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: #6f7378;
margin-top: 10px;
margin-left: 5px;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Datetime Button - Basic</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="grid">
<div class="grid-item">
<h2>Date/Time</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button slot="end" datetime="default-datetime"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
presentation="date-time"
id="default-datetime"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Date Only</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button slot="end" datetime="date-datetime"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
presentation="date"
id="date-datetime"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Time Only</h2>
<ion-item>
<ion-label>Start Time</ion-label>
<ion-datetime-button slot="end" datetime="time-datetime"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
presentation="time"
id="time-datetime"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Time Only (24 hour)</h2>
<ion-item>
<ion-label>Start Time</ion-label>
<ion-datetime-button slot="end" datetime="time-h23-datetime"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
presentation="time"
id="time-h23-datetime"
value="2022-03-15T00:43:00"
hour-cycle="h23"
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Time/Date</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button slot="end" datetime="time-date-datetime"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
presentation="time-date"
id="time-date-datetime"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Month</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button slot="end" datetime="month-datetime"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
presentation="month"
id="month-datetime"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Month/Year</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button slot="end" datetime="month-year-datetime"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
presentation="month-year"
id="month-year-datetime"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Year</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button slot="end" datetime="year-datetime"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
presentation="year"
id="year-datetime"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>preferWheel / date</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button slot="end" datetime="prefer-wheel-date"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
prefer-wheel="true"
presentation="date"
id="prefer-wheel-date"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>preferWheel / date-time</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button
slot="end"
id="prefer-wheel-date-time-button"
datetime="prefer-wheel-date-time"
></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
prefer-wheel="true"
presentation="date-time"
id="prefer-wheel-date-time"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Datetime Button - Custom Buttons</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Datetime Button - Custom Buttons</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item>
<ion-input value="March 15, 2022 at 12:43 AM"></ion-input>
<ion-datetime-button slot="end" id="default-button" datetime="default-datetime">
<ion-icon color="primary" id="custom-date-button" slot="date-target" name="calendar"></ion-icon>
<ion-icon color="primary" id="custom-time-button" slot="time-target" name="time"></ion-icon>
</ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
presentation="date-time"
id="default-datetime"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</ion-content>
<script>
const datetime = document.querySelector('ion-datetime');
const input = document.querySelector('ion-input');
datetime.addEventListener('ionChange', (ev) => {
input.value = new Intl.DateTimeFormat('en-US', { dateStyle: 'long', timeStyle: 'short' }).format(
new Date(ev.detail.value)
);
});
</script>
</ion-app>
</body>
</html>

View File

@ -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(`
<ion-datetime-button datetime="datetime" disabled="true"></ion-datetime-button>
<ion-datetime id="datetime" presentation="date-time"></ion-datetime>
`);
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(`
<ion-datetime-button datetime="datetime" disabled="true"></ion-datetime-button>
<ion-datetime id="datetime" presentation="date-time" value="2022-01-01T16:30:00"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
const datetimeButton = page.locator('ion-datetime-button');
expect(await datetimeButton.screenshot()).toMatchSnapshot(
`datetime-button-disabled-${page.getSnapshotSettings()}.png`
);
});
});

View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Datetime Button - Disabled</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(325px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: #6f7378;
margin-top: 10px;
margin-left: 5px;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Datetime Button - Disabled</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="grid">
<div class="grid-item">
<h2>Datetime Button Disabled</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button
disabled="true"
slot="end"
id="year-button"
datetime="year-datetime"
></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime locale="en-US" id="year-datetime" value="2022-03-15T00:43:00"></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Custom Button Disabled</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button disabled="true" slot="end" id="custom-button" datetime="custom-datetime">
<ion-icon color="primary" id="custom-date-button" slot="date-target" name="calendar"></ion-icon>
<ion-icon color="primary" id="custom-time-button" slot="time-target" name="time"></ion-icon>
</ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime locale="en-US" id="custom-datetime" value="2022-03-15T00:43:00"></ion-datetime>
</ion-popover>
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@ -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(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-modal>
<ion-datetime id="datetime" show-default-title="true" show-default-buttons="true" value="2022-01-01T16:30:00"></ion-datetime>
</ion-modal>
`);
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(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-popover>
<ion-datetime id="datetime" show-default-title="true" show-default-buttons="true" value="2022-01-01T16:30:00"></ion-datetime>
</ion-popover>
`);
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(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-popover>
<ion-datetime id="datetime" presentation="date-time"></ion-datetime>
</ion-popover>
`);
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(`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-modal>
<ion-datetime id="datetime" presentation="date-time"></ion-datetime>
</ion-modal>
`);
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();
});
});

View File

@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Datetime Button - Overlays</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: #6f7378;
margin-top: 10px;
margin-left: 5px;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Datetime Button - Overlays</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="grid">
<div class="grid-item">
<h2>Popover - Default</h2>
<ion-item>
<ion-datetime-button
slot="end"
id="popover-default-button"
datetime="popover-default-datetime"
></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime locale="en-US" id="popover-default-datetime" value="2022-03-15T00:43:00"></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Modal - Default</h2>
<ion-item>
<ion-datetime-button
slot="end"
id="modal-default-button"
datetime="modal-default-datetime"
></ion-datetime-button>
</ion-item>
<ion-modal>
<ion-datetime locale="en-US" id="modal-default-datetime" value="2022-03-15T00:43:00"></ion-datetime>
</ion-modal>
</div>
<div class="grid-item">
<h2>Popover - Custom</h2>
<ion-item>
<ion-datetime-button
slot="end"
id="popover-custom-button"
datetime="popover-custom-datetime"
></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
locale="en-US"
id="popover-custom-datetime"
value="2022-03-15T00:43:00"
show-default-title="true"
show-default-buttons="true"
>
<div slot="title">Custom select a Date</div>
</ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>Modal - Custom</h2>
<ion-item>
<ion-datetime-button
slot="end"
id="modal-custom-button"
datetime="modal-custom-datetime"
></ion-datetime-button>
</ion-item>
<ion-modal>
<ion-datetime
locale="en-US"
id="modal-custom-datetime"
value="2022-03-15T00:43:00"
show-default-title="true"
show-default-buttons="true"
>
<div slot="title">Custom select a Date</div>
</ion-datetime>
</ion-modal>
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Datetime Button - Custom Style</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Datetime Button - Custom Style</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item>
<ion-datetime-button
color="danger"
slot="end"
id="default-button"
datetime="default-datetime"
></ion-datetime-button>
</ion-item>
<ion-popover arrow="false" trigger="default-button">
<ion-datetime
locale="en-US"
presentation="date-time"
id="default-datetime"
value="2022-03-15T00:43:00"
></ion-datetime>
</ion-popover>
</ion-content>
</ion-app>
</body>
</html>

View File

@ -3,7 +3,7 @@ export interface DatetimeOptions {
}
export interface DatetimeChangeEventDetail {
value?: string | null;
value?: string | string[] | null;
}
export interface DatetimeCustomEvent extends CustomEvent {
@ -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';

View File

@ -8,9 +8,9 @@
--title-color: #{$text-color-step-400};
}
:host(.datetime-presentation-date-time),
:host(.datetime-presentation-time-date),
:host(.datetime-presentation-date) {
:host(.datetime-presentation-date-time:not(.datetime-prefer-wheel)),
:host(.datetime-presentation-time-date:not(.datetime-prefer-wheel)),
:host(.datetime-presentation-date:not(.datetime-prefer-wheel)) {
min-height: 350px;
}

View File

@ -19,12 +19,37 @@
overflow: hidden;
}
/**
* When using the wheel picker to switch
* between months, sometimes the allowed
* dates may be filtered. As a result, it
* is possible to get a layout shift as
* the picker column will shrink to fit the
* widest item in the column. Setting a minimum
* width avoids this layout shifting.
*/
ion-picker-column-internal {
min-width: 26px;
}
:host(.datetime-size-fixed) {
width: auto;
max-width: 350px;
height: auto;
}
:host(.datetime-size-fixed:not(.datetime-prefer-wheel)) {
max-width: 350px;
}
/**
* This ensures that the picker is apppropriately
* sized and never truncates the text.
*/
:host(.datetime-size-fixed.datetime-prefer-wheel) {
min-width: 350px;
max-width: max-content;
}
:host(.datetime-size-cover) {
width: 100%;
}
@ -58,21 +83,19 @@
* the order we need to manually switch
* the text alignment too.
*/
:host .datetime-year .order-month-first .month-column {
order: 1;
:host .wheel-order-year-first .day-column {
order: 3;
text-align: end;
}
:host .datetime-year .order-month-first .year-column {
order: 2;
}
:host .datetime-year .order-year-first .month-column {
:host .wheel-order-year-first .month-column {
order: 2;
text-align: end;
}
:host .datetime-year .order-year-first .year-column {
:host .wheel-order-year-first .year-column {
order: 1;
text-align: start;

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ import {
getFormattedHour,
addTimePadding,
getMonthAndYear,
getLocalizedDayPeriod,
} from '../utils/format';
describe('generateDayAriaLabel()', () => {
@ -88,3 +89,13 @@ describe('getMonthAndYear()', () => {
expect(getMonthAndYear('es-ES', { month: 4, day: 1, year: 2006 })).toEqual('abril de 2006');
});
});
describe('getLocalizedDayPeriod', () => {
it('should return AM when the date is in the morning', () => {
expect(getLocalizedDayPeriod('en-US', 'am'));
});
it('should return PM when the date is in the afternoon', () => {
expect(getLocalizedDayPeriod('en-US', 'pm'));
});
});

View File

@ -0,0 +1,146 @@
import { expect } from '@playwright/test';
import type { E2EPage } from '@utils/test/playwright';
import { test } from '@utils/test/playwright';
test.describe('datetime: locale', () => {
let datetimeFixture: DatetimeLocaleFixture;
test.beforeEach(async ({ page }) => {
datetimeFixture = new DatetimeLocaleFixture(page);
await datetimeFixture.goto();
});
test.describe('en-US', () => {
test.beforeEach(async () => {
await datetimeFixture.setLocale('en-US');
});
test('should not have visual regressions', async () => {
await datetimeFixture.expectLocalizedDatePicker();
});
test('month/year picker should not have visual regressions', async () => {
await datetimeFixture.expectLocalizedMonthYearPicker();
});
test('time picker should not have visual regressions', async () => {
await datetimeFixture.expectLocalizedTimePicker();
});
});
test.describe('ta-IN', () => {
test.beforeEach(async () => {
await datetimeFixture.setLocale('ta-IN');
});
test('should not have visual regressions', async () => {
await datetimeFixture.expectLocalizedDatePicker();
});
test('month/year picker should not have visual regressions', async () => {
await datetimeFixture.expectLocalizedMonthYearPicker();
});
test('time picker should not have visual regressions', async () => {
await datetimeFixture.expectLocalizedTimePicker();
});
});
test.describe('ja-JP', () => {
test.beforeEach(async () => {
await datetimeFixture.setLocale('ja-JP');
});
test('should not have visual regressions', async () => {
await datetimeFixture.expectLocalizedDatePicker();
});
test('month/year picker should not have visual regressions', async () => {
await datetimeFixture.expectLocalizedMonthYearPicker();
});
test('time picker should not have visual regressions', async () => {
await datetimeFixture.expectLocalizedTimePicker();
});
});
test.describe('es-ES', () => {
test.beforeEach(async () => {
await datetimeFixture.setLocale('es-ES');
});
test('should not have visual regressions', async () => {
await datetimeFixture.expectLocalizedDatePicker();
});
test('month/year picker should not have visual regressions', async () => {
await datetimeFixture.expectLocalizedMonthYearPicker();
});
test('time picker should not have visual regressions', async () => {
await datetimeFixture.expectLocalizedTimePicker();
});
});
});
class DatetimeLocaleFixture {
readonly page: E2EPage;
locale = 'en-US';
constructor(page: E2EPage) {
this.page = page;
}
async goto() {
await this.page.goto(`/src/components/datetime/test/locale`);
}
async setLocale(locale: string) {
this.locale = locale;
await this.page.locator('select').selectOption(locale);
await this.page.waitForChanges();
}
async expectLocalizedDatePicker() {
await this.waitForDatetime();
await this.page.setIonViewport();
// Captures a screenshot of the datepicker with localized am/pm labels
expect(await this.page.screenshot()).toMatchSnapshot(
`datetime-locale-${this.locale}-diff-${this.page.getSnapshotSettings()}.png`
);
}
async expectLocalizedMonthYearPicker() {
await this.waitForDatetime();
await this.page.setIonViewport();
// Opens the month/year picker
const monthYearButton = this.page.locator('#am .calendar-month-year ion-item');
await monthYearButton.click();
await this.page.waitForChanges();
// Capture a screenshot of the month/year picker with localized month labels.
expect(await this.page.screenshot()).toMatchSnapshot(
`datetime-locale-${this.locale}-month-year-diff-${this.page.getSnapshotSettings()}.png`
);
}
async expectLocalizedTimePicker() {
await this.waitForDatetime();
await this.page.setIonViewport();
// Opens the timepicker
const timePickerButton = this.page.locator('#am .time-body');
const timePickerPopoverPresentSpy = await this.page.spyOnEvent('ionPopoverDidPresent');
await timePickerButton.click();
await timePickerPopoverPresentSpy.next();
// Capture a screenshot of the time picker with localized am/pm labels
expect(await this.page.screenshot()).toMatchSnapshot(
`datetime-locale-${this.locale}-time-diff-${this.page.getSnapshotSettings()}.png`
);
}
private async waitForDatetime() {
await this.page.locator('#am.datetime-ready').waitFor({ state: 'attached' });
await this.page.locator('#pm').scrollIntoViewIfNeeded();
await this.page.locator('#pm.datetime-ready').waitFor({ state: 'attached' });
}
}

Some files were not shown because too many files have changed in this diff Show More