feat(datetime-button): add button for displaying datetime in overlays (#25655)
resolves #24316
@@ -23,6 +23,7 @@ export const DIRECTIVES = [
|
||||
d.IonCol,
|
||||
d.IonContent,
|
||||
d.IonDatetime,
|
||||
d.IonDatetimeButton,
|
||||
d.IonFab,
|
||||
d.IonFabButton,
|
||||
d.IonFabList,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<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
|
||||
|
||||
55
core/src/components.d.ts
vendored
@@ -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>) => void;
|
||||
/**
|
||||
* Emitted when componentDidRender is fired.
|
||||
*/
|
||||
"onIonRender"?: (event: IonDatetimeCustomEvent<void>) => 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<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>;
|
||||
|
||||
@@ -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(
|
||||
|
||||
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 107 KiB |
50
core/src/components/datetime-button/datetime-button.scss
Normal 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;
|
||||
}
|
||||
414
core/src/components/datetime-button/datetime-button.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
222
core/src/components/datetime-button/test/basic/index.html
Normal 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>
|
||||
49
core/src/components/datetime-button/test/buttons/index.html
Normal 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>
|
||||
@@ -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`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
74
core/src/components/datetime-button/test/disabled/index.html
Normal 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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 74 KiB |
118
core/src/components/datetime-button/test/overlays/index.html
Normal 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>
|
||||
40
core/src/components/datetime-button/test/style/index.html
Normal 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>
|
||||
@@ -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';
|
||||
|
||||
@@ -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<StyleEventDetail>;
|
||||
|
||||
/**
|
||||
* Emitted when componentDidRender is fired.
|
||||
* @internal
|
||||
*/
|
||||
@Event() ionRender!: EventEmitter<void>;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<JSX.IonChip, HTMLIonChi
|
||||
export const IonCol = /*@__PURE__*/createReactComponent<JSX.IonCol, HTMLIonColElement>('ion-col', undefined, undefined, defineIonCol);
|
||||
export const IonContent = /*@__PURE__*/createReactComponent<JSX.IonContent, HTMLIonContentElement>('ion-content', undefined, undefined, defineIonContent);
|
||||
export const IonDatetime = /*@__PURE__*/createReactComponent<JSX.IonDatetime, HTMLIonDatetimeElement>('ion-datetime', undefined, undefined, defineIonDatetime);
|
||||
export const IonDatetimeButton = /*@__PURE__*/createReactComponent<JSX.IonDatetimeButton, HTMLIonDatetimeButtonElement>('ion-datetime-button', undefined, undefined, defineIonDatetimeButton);
|
||||
export const IonFab = /*@__PURE__*/createReactComponent<JSX.IonFab, HTMLIonFabElement>('ion-fab', undefined, undefined, defineIonFab);
|
||||
export const IonFabList = /*@__PURE__*/createReactComponent<JSX.IonFabList, HTMLIonFabListElement>('ion-fab-list', undefined, undefined, defineIonFabList);
|
||||
export const IonFooter = /*@__PURE__*/createReactComponent<JSX.IonFooter, HTMLIonFooterElement>('ion-footer', undefined, undefined, defineIonFooter);
|
||||
|
||||
@@ -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<JSX.IonDatetime>('ion-d
|
||||
'ionChange',
|
||||
'ionFocus',
|
||||
'ionBlur',
|
||||
'ionStyle'
|
||||
'ionStyle',
|
||||
'ionRender'
|
||||
],
|
||||
'value', 'v-ion-change', 'ionChange');
|
||||
|
||||
|
||||
export const IonDatetimeButton = /*@__PURE__*/ defineContainer<JSX.IonDatetimeButton>('ion-datetime-button', defineIonDatetimeButton, [
|
||||
'color',
|
||||
'disabled',
|
||||
'datetime'
|
||||
]);
|
||||
|
||||
|
||||
export const IonFab = /*@__PURE__*/ defineContainer<JSX.IonFab>('ion-fab', defineIonFab, [
|
||||
'horizontal',
|
||||
'vertical',
|
||||
|
||||