mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Compare commits
6 Commits
fix/dateti
...
FW-6371-po
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
885b0edc97 | ||
|
|
62cf80c433 | ||
|
|
6a819d5eeb | ||
|
|
ec40cd5b3e | ||
|
|
4c598d5a34 | ||
|
|
1cfa915e8f |
@@ -100,12 +100,15 @@ ion-alert,scoped
|
||||
ion-alert,prop,animated,boolean,true,false,false
|
||||
ion-alert,prop,backdropDismiss,boolean,true,false,false
|
||||
ion-alert,prop,buttons,(string | AlertButton)[],[],false,false
|
||||
ion-alert,prop,component,Function | HTMLElement | null | string | undefined,undefined,false,false
|
||||
ion-alert,prop,componentProps,undefined | { [key: string]: any; },undefined,false,false
|
||||
ion-alert,prop,cssClass,string | string[] | undefined,undefined,false,false
|
||||
ion-alert,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
|
||||
ion-alert,prop,header,string | undefined,undefined,false,false
|
||||
ion-alert,prop,htmlAttributes,undefined | { [key: string]: any; },undefined,false,false
|
||||
ion-alert,prop,inputs,AlertInput[],[],false,false
|
||||
ion-alert,prop,isOpen,boolean,false,false,false
|
||||
ion-alert,prop,keepContentsMounted,boolean,false,false,false
|
||||
ion-alert,prop,keyboardClose,boolean,true,false,false
|
||||
ion-alert,prop,leaveAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
|
||||
ion-alert,prop,message,IonicSafeString | string | undefined,undefined,false,false
|
||||
|
||||
29
core/src/components.d.ts
vendored
29
core/src/components.d.ts
vendored
@@ -233,6 +233,14 @@ export namespace Components {
|
||||
* Array of buttons to be added to the alert.
|
||||
*/
|
||||
"buttons": (AlertButton | string)[];
|
||||
/**
|
||||
* The component to display inside of the alert.
|
||||
*/
|
||||
"component"?: ComponentRef;
|
||||
/**
|
||||
* The data to pass to the alert component. You only need to use this if you are not using a JavaScript framework. Otherwise, you can just set the props directly on your component.
|
||||
*/
|
||||
"componentProps"?: ComponentProps;
|
||||
/**
|
||||
* Additional classes to apply for custom CSS. If multiple classes are provided they should be separated by spaces.
|
||||
*/
|
||||
@@ -265,6 +273,10 @@ export namespace Components {
|
||||
* If `true`, the alert will open. If `false`, the alert will close. Use this if you need finer grained control over presentation, otherwise just use the alertController or the `trigger` property. Note: `isOpen` will not automatically be set back to `false` when the alert dismisses. You will need to do that in your code.
|
||||
*/
|
||||
"isOpen": boolean;
|
||||
/**
|
||||
* If `true`, the component passed into `ion-alert` will automatically be mounted when the alert is created. The component will remain mounted even when the alert is dismissed. However, the component will be destroyed when the alert is destroyed. This property is not reactive and should only be used when initially creating a alert. Note: This feature only applies to inline alerts in JavaScript frameworks such as Angular, React, and Vue.
|
||||
*/
|
||||
"keepContentsMounted": boolean;
|
||||
/**
|
||||
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
|
||||
*/
|
||||
@@ -3594,6 +3606,7 @@ declare global {
|
||||
"willPresent": void;
|
||||
"willDismiss": OverlayEventDetail;
|
||||
"didDismiss": OverlayEventDetail;
|
||||
"ionMount": void;
|
||||
}
|
||||
interface HTMLIonAlertElement extends Components.IonAlert, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLIonAlertElementEventMap>(type: K, listener: (this: HTMLIonAlertElement, ev: IonAlertCustomEvent<HTMLIonAlertElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
@@ -5023,6 +5036,14 @@ declare namespace LocalJSX {
|
||||
* Array of buttons to be added to the alert.
|
||||
*/
|
||||
"buttons"?: (AlertButton | string)[];
|
||||
/**
|
||||
* The component to display inside of the alert.
|
||||
*/
|
||||
"component"?: ComponentRef;
|
||||
/**
|
||||
* The data to pass to the alert component. You only need to use this if you are not using a JavaScript framework. Otherwise, you can just set the props directly on your component.
|
||||
*/
|
||||
"componentProps"?: ComponentProps;
|
||||
/**
|
||||
* Additional classes to apply for custom CSS. If multiple classes are provided they should be separated by spaces.
|
||||
*/
|
||||
@@ -5049,6 +5070,10 @@ declare namespace LocalJSX {
|
||||
* If `true`, the alert will open. If `false`, the alert will close. Use this if you need finer grained control over presentation, otherwise just use the alertController or the `trigger` property. Note: `isOpen` will not automatically be set back to `false` when the alert dismisses. You will need to do that in your code.
|
||||
*/
|
||||
"isOpen"?: boolean;
|
||||
/**
|
||||
* If `true`, the component passed into `ion-alert` will automatically be mounted when the alert is created. The component will remain mounted even when the alert is dismissed. However, the component will be destroyed when the alert is destroyed. This property is not reactive and should only be used when initially creating a alert. Note: This feature only applies to inline alerts in JavaScript frameworks such as Angular, React, and Vue.
|
||||
*/
|
||||
"keepContentsMounted"?: boolean;
|
||||
/**
|
||||
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
|
||||
*/
|
||||
@@ -5089,6 +5114,10 @@ declare namespace LocalJSX {
|
||||
* Emitted before the alert has presented.
|
||||
*/
|
||||
"onIonAlertWillPresent"?: (event: IonAlertCustomEvent<void>) => void;
|
||||
/**
|
||||
* Emitted before the modal has presented, but after the component has been mounted in the DOM. This event exists so iOS can run the entering transition properly
|
||||
*/
|
||||
"onIonMount"?: (event: IonAlertCustomEvent<void>) => void;
|
||||
/**
|
||||
* Emitted before the alert has dismissed. Shorthand for ionAlertWillDismiss.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import type { AnimationBuilder, LiteralUnion, Mode, TextFieldTypes } from '../../interface';
|
||||
import type {
|
||||
AnimationBuilder,
|
||||
LiteralUnion,
|
||||
Mode,
|
||||
TextFieldTypes,
|
||||
ComponentRef,
|
||||
ComponentProps,
|
||||
} from '../../interface';
|
||||
import type { IonicSafeString } from '../../utils/sanitization';
|
||||
|
||||
export interface AlertOptions {
|
||||
export interface AlertOptions<T extends ComponentRef = ComponentRef> {
|
||||
component?: T;
|
||||
componentProps?: ComponentProps<T>;
|
||||
header?: string;
|
||||
subHeader?: string;
|
||||
message?: string | IonicSafeString;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Listen, Method, Prop, Watch, forceUpdate, h } from '@stencil/core';
|
||||
import { ENABLE_HTML_CONTENT_DEFAULT } from '@utils/config';
|
||||
import { CoreDelegate, attachComponent, detachComponent } from '@utils/framework-delegate';
|
||||
import type { Gesture } from '@utils/gesture';
|
||||
import { createButtonActiveGesture } from '@utils/gesture/button-active';
|
||||
import { raf } from '@utils/helpers';
|
||||
import { raf, hasLazyBuild } from '@utils/helpers';
|
||||
import { createLockController } from '@utils/lock-controller';
|
||||
import {
|
||||
createDelegateController,
|
||||
createTriggerController,
|
||||
BACKDROP,
|
||||
dismiss,
|
||||
@@ -19,10 +19,18 @@ import {
|
||||
} from '@utils/overlays';
|
||||
import { sanitizeDOMString } from '@utils/sanitization';
|
||||
import { getClassMap } from '@utils/theme';
|
||||
import { deepReady, waitForMount } from '@utils/transition';
|
||||
|
||||
import { config } from '../../global/config';
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import type { AnimationBuilder, CssClassMap, OverlayInterface, FrameworkDelegate } from '../../interface';
|
||||
import type {
|
||||
AnimationBuilder,
|
||||
CssClassMap,
|
||||
OverlayInterface,
|
||||
FrameworkDelegate,
|
||||
ComponentRef,
|
||||
ComponentProps,
|
||||
} from '../../interface';
|
||||
import type { OverlayEventDetail } from '../../utils/overlays-interface';
|
||||
import type { IonicSafeString } from '../../utils/sanitization';
|
||||
|
||||
@@ -46,7 +54,6 @@ import { mdLeaveAnimation } from './animations/md.leave';
|
||||
scoped: true,
|
||||
})
|
||||
export class Alert implements ComponentInterface, OverlayInterface {
|
||||
private readonly delegateController = createDelegateController(this);
|
||||
private readonly lockController = createLockController();
|
||||
private readonly triggerController = createTriggerController();
|
||||
private customHTMLEnabled = config.get('innerHTMLTemplatesEnabled', ENABLE_HTML_CONTENT_DEFAULT);
|
||||
@@ -57,6 +64,11 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
private wrapperEl?: HTMLElement;
|
||||
private gesture?: Gesture;
|
||||
|
||||
private inline = false;
|
||||
private workingDelegate?: FrameworkDelegate;
|
||||
private coreDelegate: FrameworkDelegate = CoreDelegate();
|
||||
private usersElement?: HTMLElement;
|
||||
|
||||
presented = false;
|
||||
lastFocus?: HTMLElement;
|
||||
|
||||
@@ -150,6 +162,32 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Prop() htmlAttributes?: { [key: string]: any };
|
||||
|
||||
/**
|
||||
* The component to display inside of the alert.
|
||||
*/
|
||||
@Prop() component?: ComponentRef;
|
||||
|
||||
/**
|
||||
* The data to pass to the alert component.
|
||||
* You only need to use this if you are not using
|
||||
* a JavaScript framework. Otherwise, you can just
|
||||
* set the props directly on your component.
|
||||
*/
|
||||
@Prop() componentProps?: ComponentProps;
|
||||
|
||||
/**
|
||||
* If `true`, the component passed into `ion-alert` will
|
||||
* automatically be mounted when the alert is created. The
|
||||
* component will remain mounted even when the alert is dismissed.
|
||||
* However, the component will be destroyed when the alert is
|
||||
* destroyed. This property is not reactive and should only be
|
||||
* used when initially creating a alert.
|
||||
*
|
||||
* Note: This feature only applies to inline alerts in JavaScript
|
||||
* frameworks such as Angular, React, and Vue.
|
||||
*/
|
||||
@Prop() keepContentsMounted = false;
|
||||
|
||||
/**
|
||||
* If `true`, the alert will open. If `false`, the alert will close.
|
||||
* Use this if you need finer grained control over presentation, otherwise
|
||||
@@ -347,6 +385,16 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted before the modal has presented, but after the component
|
||||
* has been mounted in the DOM.
|
||||
* This event exists so iOS can run the entering
|
||||
* transition properly
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
@Event() ionMount!: EventEmitter<void>;
|
||||
|
||||
connectedCallback() {
|
||||
prepareOverlay(this.el);
|
||||
this.triggerChanged();
|
||||
@@ -403,6 +451,52 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
this.triggerChanged();
|
||||
}
|
||||
|
||||
private onLifecycle = (modalEvent: CustomEvent) => {
|
||||
const el = this.usersElement;
|
||||
const name = LIFECYCLE_MAP[modalEvent.type];
|
||||
if (el && name) {
|
||||
const event = new CustomEvent(name, {
|
||||
bubbles: false,
|
||||
cancelable: false,
|
||||
detail: modalEvent.detail,
|
||||
});
|
||||
el.dispatchEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether or not an overlay
|
||||
* is being used inline or via a controller/JS
|
||||
* and returns the correct delegate.
|
||||
* By default, subsequent calls to getDelegate
|
||||
* will use a cached version of the delegate.
|
||||
* This is useful for calling dismiss after
|
||||
* present so that the correct delegate is given.
|
||||
*/
|
||||
private getDelegate(force = false) {
|
||||
if (this.workingDelegate && !force) {
|
||||
return {
|
||||
delegate: this.workingDelegate,
|
||||
inline: this.inline,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If using overlay inline
|
||||
* we potentially need to use the coreDelegate
|
||||
* so that this works in vanilla JS apps.
|
||||
* If a developer has presented this component
|
||||
* via a controller, then we can assume
|
||||
* the component is already in the
|
||||
* correct place.
|
||||
*/
|
||||
const parentEl = this.el.parentNode as HTMLElement | null;
|
||||
const inline = (this.inline = parentEl !== null && !this.hasController);
|
||||
const delegate = (this.workingDelegate = inline ? this.delegate || this.coreDelegate : this.delegate);
|
||||
|
||||
return { inline, delegate };
|
||||
}
|
||||
|
||||
/**
|
||||
* Present the alert overlay after it has been created.
|
||||
*/
|
||||
@@ -410,7 +504,46 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
async present(): Promise<void> {
|
||||
const unlock = await this.lockController.lock();
|
||||
|
||||
await this.delegateController.attachViewToDom();
|
||||
const { el } = this;
|
||||
const { inline, delegate } = this.getDelegate(true);
|
||||
|
||||
/**
|
||||
* Emit ionMount so JS Frameworks have an opportunity
|
||||
* to add the child component to the DOM. The child
|
||||
* component will be assigned to this.usersElement below.
|
||||
*/
|
||||
this.ionMount.emit();
|
||||
|
||||
this.usersElement = await attachComponent(
|
||||
delegate,
|
||||
el,
|
||||
this.component,
|
||||
['alert-viewport'],
|
||||
this.componentProps,
|
||||
inline
|
||||
);
|
||||
|
||||
/**
|
||||
* When using the lazy loaded build of Stencil, we need to wait
|
||||
* for every Stencil component instance to be ready before presenting
|
||||
* otherwise there can be a flash of unstyled content. With the
|
||||
* custom elements bundle we need to wait for the JS framework
|
||||
* mount the inner contents of the overlay otherwise WebKit may
|
||||
* get the transition incorrect.
|
||||
*/
|
||||
if (hasLazyBuild(el)) {
|
||||
await deepReady(this.usersElement);
|
||||
/**
|
||||
* If keepContentsMounted="true" then the
|
||||
* JS Framework has already mounted the inner
|
||||
* contents so there is no need to wait.
|
||||
* Otherwise, we need to wait for the JS
|
||||
* Framework to mount the inner contents
|
||||
* of this component.
|
||||
*/
|
||||
} else if (!this.keepContentsMounted) {
|
||||
await waitForMount();
|
||||
}
|
||||
|
||||
await present(this, 'alertEnter', iosEnterAnimation, mdEnterAnimation).then(() => {
|
||||
/**
|
||||
@@ -449,7 +582,13 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
const dismissed = await dismiss(this, data, role, 'alertLeave', iosLeaveAnimation, mdLeaveAnimation);
|
||||
|
||||
if (dismissed) {
|
||||
this.delegateController.removeViewFromDom();
|
||||
/**
|
||||
* If using alert inline
|
||||
* we potentially need to use the coreDelegate
|
||||
* so that this works in vanilla JS apps
|
||||
*/
|
||||
const { delegate } = this.getDelegate();
|
||||
await detachComponent(delegate, this.usersElement);
|
||||
}
|
||||
|
||||
unlock();
|
||||
@@ -704,6 +843,8 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
const cancelButton = this.processedButtons.find((b) => b.role === 'cancel');
|
||||
this.callButtonHandler(cancelButton);
|
||||
}
|
||||
|
||||
this.onLifecycle(ev);
|
||||
};
|
||||
|
||||
private renderAlertButtons() {
|
||||
@@ -746,7 +887,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { overlayIndex, header, subHeader, message, htmlAttributes } = this;
|
||||
const { overlayIndex, header, subHeader, message, htmlAttributes, onLifecycle } = this;
|
||||
const mode = getIonMode(this);
|
||||
const hdrId = `alert-${overlayIndex}-hdr`;
|
||||
const msgId = `alert-${overlayIndex}-msg`;
|
||||
@@ -773,7 +914,10 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
'overlay-hidden': true,
|
||||
'alert-translucent': this.translucent,
|
||||
}}
|
||||
onIonAlertDidPresent={onLifecycle}
|
||||
onIonAlertWillPresent={onLifecycle}
|
||||
onIonAlertWillDismiss={this.dispatchCancelHandler}
|
||||
onIonAlertDidDismiss={onLifecycle}
|
||||
onIonBackdropTap={this.onBackdropTap}
|
||||
>
|
||||
<ion-backdrop tappable={this.backdropDismiss} />
|
||||
@@ -812,6 +956,8 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
|
||||
{this.renderAlertMessage(msgId)}
|
||||
|
||||
<slot></slot>
|
||||
|
||||
{this.renderAlertInputs()}
|
||||
{this.renderAlertButtons()}
|
||||
</div>
|
||||
@@ -822,6 +968,13 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
}
|
||||
}
|
||||
|
||||
const LIFECYCLE_MAP: any = {
|
||||
ionAlertDidPresent: 'ionViewDidEnter',
|
||||
ionAlertWillPresent: 'ionViewWillEnter',
|
||||
ionAlertWillDismiss: 'ionViewWillLeave',
|
||||
ionAlertDidDismiss: 'ionViewDidLeave',
|
||||
};
|
||||
|
||||
const inputClass = (input: AlertInput): CssClassMap => {
|
||||
return {
|
||||
'alert-input': true,
|
||||
|
||||
@@ -40,6 +40,16 @@
|
||||
<button class="expand" id="radio" onclick="presentAlertRadio()">Radio</button>
|
||||
<button class="expand" id="checkbox" onclick="presentAlertCheckbox()">Checkbox</button>
|
||||
<button class="expand" onclick="presentWithCssClass()">CssClass</button>
|
||||
|
||||
<ion-button id="present-alert">Click Me</ion-button>
|
||||
<ion-alert
|
||||
trigger="present-alert"
|
||||
header="A Short Title Is Best"
|
||||
sub-header="A Sub Header Is Optional"
|
||||
message="A message should be a short, complete sentence."
|
||||
>
|
||||
<p>woah</p>
|
||||
</ion-alert>
|
||||
</ion-content>
|
||||
|
||||
<style>
|
||||
@@ -62,11 +72,30 @@
|
||||
},
|
||||
};
|
||||
|
||||
const alert = document.querySelector('ion-alert');
|
||||
alert.buttons = ['Action'];
|
||||
|
||||
async function openAlert(opts) {
|
||||
const alert = await alertController.create(opts);
|
||||
await alert.present();
|
||||
}
|
||||
|
||||
class ProfilePage extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.innerHTML = `
|
||||
<div>
|
||||
<p>Profile Page</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('profile-page', ProfilePage);
|
||||
|
||||
function presentAlert() {
|
||||
openAlert({
|
||||
header: 'Alert',
|
||||
@@ -76,6 +105,7 @@
|
||||
htmlAttributes: {
|
||||
'data-testid': 'basic-alert',
|
||||
},
|
||||
component: 'profile-page',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1984,7 +1984,7 @@ export class Datetime implements ComponentInterface {
|
||||
});
|
||||
|
||||
this.setActiveParts({
|
||||
...this.getActivePartsWithFallback(),
|
||||
...activePart,
|
||||
hour: ev.detail.value,
|
||||
});
|
||||
|
||||
@@ -2024,7 +2024,7 @@ export class Datetime implements ComponentInterface {
|
||||
});
|
||||
|
||||
this.setActiveParts({
|
||||
...this.getActivePartsWithFallback(),
|
||||
...activePart,
|
||||
minute: ev.detail.value,
|
||||
});
|
||||
|
||||
@@ -2070,7 +2070,7 @@ export class Datetime implements ComponentInterface {
|
||||
});
|
||||
|
||||
this.setActiveParts({
|
||||
...this.getActivePartsWithFallback(),
|
||||
...activePart,
|
||||
ampm: ev.detail.value,
|
||||
hour,
|
||||
});
|
||||
|
||||
@@ -410,13 +410,8 @@ export class Picker implements ComponentInterface {
|
||||
colEl: HTMLIonPickerColumnElement,
|
||||
value: string,
|
||||
zeroBehavior: 'start' | 'end' = 'start'
|
||||
): boolean => {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
) => {
|
||||
const behavior = zeroBehavior === 'start' ? /^0+/ : /0$/;
|
||||
value = value.replace(behavior, '');
|
||||
const option = Array.from(colEl.querySelectorAll('ion-picker-column-option')).find((el) => {
|
||||
return el.disabled !== true && el.textContent!.replace(behavior, '') === value;
|
||||
});
|
||||
@@ -424,58 +419,6 @@ export class Picker implements ComponentInterface {
|
||||
if (option) {
|
||||
colEl.setValue(option.value);
|
||||
}
|
||||
|
||||
return !!option;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempts to intelligently search the first and second
|
||||
* column as if they're number columns for the provided numbers
|
||||
* where the first two numbers are the first column
|
||||
* and the last 2 are the last column. Tries to allow for the first
|
||||
* number to be ignored for situations where typos occurred.
|
||||
*/
|
||||
private multiColumnSearch = (
|
||||
firstColumn: HTMLIonPickerColumnElement,
|
||||
secondColumn: HTMLIonPickerColumnElement,
|
||||
input: string
|
||||
) => {
|
||||
if (input.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inputArray = input.split('');
|
||||
const hourValue = inputArray.slice(0, 2).join('');
|
||||
// Try to find a match for the first two digits in the first column
|
||||
const foundHour = this.searchColumn(firstColumn, hourValue);
|
||||
|
||||
// If we have more than 2 digits and found a match for hours,
|
||||
// use the remaining digits for the second column (minutes)
|
||||
if (inputArray.length > 2 && foundHour) {
|
||||
const minuteValue = inputArray.slice(2, 4).join('');
|
||||
this.searchColumn(secondColumn, minuteValue);
|
||||
}
|
||||
// If we couldn't find a match for the two-digit hour, try single digit approaches
|
||||
else if (!foundHour && inputArray.length >= 1) {
|
||||
// First try the first digit as a single-digit hour
|
||||
let singleDigitHour = inputArray[0];
|
||||
let singleDigitFound = this.searchColumn(firstColumn, singleDigitHour);
|
||||
|
||||
// If that didn't work, try the second digit as a single-digit hour
|
||||
// (handles case where user made a typo in the first digit, or they typed over themselves)
|
||||
if (!singleDigitFound) {
|
||||
inputArray.shift();
|
||||
singleDigitHour = inputArray[0];
|
||||
singleDigitFound = this.searchColumn(firstColumn, singleDigitHour);
|
||||
}
|
||||
|
||||
// If we found a single-digit hour and have remaining digits,
|
||||
// use up to 2 of the remaining digits for the second column
|
||||
if (singleDigitFound && inputArray.length > 1) {
|
||||
const remainingDigits = inputArray.slice(1, 3).join('');
|
||||
this.searchColumn(secondColumn, remainingDigits);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private selectMultiColumn = () => {
|
||||
@@ -490,15 +433,91 @@ export class Picker implements ComponentInterface {
|
||||
const lastColumn = numericPickers[1];
|
||||
|
||||
let value = inputEl.value;
|
||||
if (value.length > 4) {
|
||||
const startIndex = inputEl.value.length - 4;
|
||||
const newString = inputEl.value.substring(startIndex);
|
||||
let minuteValue;
|
||||
switch (value.length) {
|
||||
case 1:
|
||||
this.searchColumn(firstColumn, value);
|
||||
break;
|
||||
case 2:
|
||||
/**
|
||||
* If the first character is `0` or `1` it is
|
||||
* possible that users are trying to type `09`
|
||||
* or `11` into the hour field, so we should look
|
||||
* at that first.
|
||||
*/
|
||||
const firstCharacter = inputEl.value.substring(0, 1);
|
||||
value = firstCharacter === '0' || firstCharacter === '1' ? inputEl.value : firstCharacter;
|
||||
|
||||
inputEl.value = newString;
|
||||
value = newString;
|
||||
this.searchColumn(firstColumn, value);
|
||||
|
||||
/**
|
||||
* If only checked the first value,
|
||||
* we can check the second value
|
||||
* for a match in the minutes column
|
||||
*/
|
||||
if (value.length === 1) {
|
||||
minuteValue = inputEl.value.substring(inputEl.value.length - 1);
|
||||
this.searchColumn(lastColumn, minuteValue, 'end');
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
/**
|
||||
* If the first character is `0` or `1` it is
|
||||
* possible that users are trying to type `09`
|
||||
* or `11` into the hour field, so we should look
|
||||
* at that first.
|
||||
*/
|
||||
const firstCharacterAgain = inputEl.value.substring(0, 1);
|
||||
value =
|
||||
firstCharacterAgain === '0' || firstCharacterAgain === '1'
|
||||
? inputEl.value.substring(0, 2)
|
||||
: firstCharacterAgain;
|
||||
|
||||
this.searchColumn(firstColumn, value);
|
||||
|
||||
/**
|
||||
* If only checked the first value,
|
||||
* we can check the second value
|
||||
* for a match in the minutes column
|
||||
*/
|
||||
minuteValue = value.length === 1 ? inputEl.value.substring(1) : inputEl.value.substring(2);
|
||||
|
||||
this.searchColumn(lastColumn, minuteValue, 'end');
|
||||
break;
|
||||
case 4:
|
||||
/**
|
||||
* If the first character is `0` or `1` it is
|
||||
* possible that users are trying to type `09`
|
||||
* or `11` into the hour field, so we should look
|
||||
* at that first.
|
||||
*/
|
||||
const firstCharacterAgainAgain = inputEl.value.substring(0, 1);
|
||||
value =
|
||||
firstCharacterAgainAgain === '0' || firstCharacterAgainAgain === '1'
|
||||
? inputEl.value.substring(0, 2)
|
||||
: firstCharacterAgainAgain;
|
||||
this.searchColumn(firstColumn, value);
|
||||
|
||||
/**
|
||||
* If only checked the first value,
|
||||
* we can check the second value
|
||||
* for a match in the minutes column
|
||||
*/
|
||||
const minuteValueAgain =
|
||||
value.length === 1
|
||||
? inputEl.value.substring(1, inputEl.value.length)
|
||||
: inputEl.value.substring(2, inputEl.value.length);
|
||||
this.searchColumn(lastColumn, minuteValueAgain, 'end');
|
||||
|
||||
break;
|
||||
default:
|
||||
const startIndex = inputEl.value.length - 4;
|
||||
const newString = inputEl.value.substring(startIndex);
|
||||
|
||||
inputEl.value = newString;
|
||||
this.selectMultiColumn();
|
||||
break;
|
||||
}
|
||||
|
||||
this.multiColumnSearch(firstColumn, lastColumn, value);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -163,172 +163,6 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
await expect(ionChange).toHaveReceivedEventDetail({ value: 12 });
|
||||
await expect(column).toHaveJSProperty('value', 12);
|
||||
});
|
||||
|
||||
test('should allow typing 22 in a column where the max value is 23 and not just set it to 2', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/28877',
|
||||
});
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker>
|
||||
<ion-picker-column id="hours"></ion-picker-column>
|
||||
<ion-picker-column id="minutes"></ion-picker-column>
|
||||
</ion-picker>
|
||||
|
||||
<script>
|
||||
const hoursColumn = document.querySelector('ion-picker-column#hours');
|
||||
hoursColumn.numericInput = true;
|
||||
const hourItems = [
|
||||
{ text: '01', value: 1 },
|
||||
{ text: '02', value: 2},
|
||||
{ text: '20', value: 20 },
|
||||
{ text: '21', value: 21 },
|
||||
{ text: '22', value: 22 },
|
||||
{ text: '23', value: 23 }
|
||||
];
|
||||
|
||||
hourItems.forEach((item) => {
|
||||
const option = document.createElement('ion-picker-column-option');
|
||||
option.value = item.value;
|
||||
option.textContent = item.text;
|
||||
|
||||
hoursColumn.appendChild(option);
|
||||
});
|
||||
|
||||
const minutesColumn = document.querySelector('ion-picker-column#minutes');
|
||||
minutesColumn.numericInput = true;
|
||||
const minuteItems = [
|
||||
{ text: '00', value: 0 },
|
||||
{ text: '15', value: 15 },
|
||||
{ text: '30', value: 30 },
|
||||
{ text: '45', value: 45 }
|
||||
];
|
||||
|
||||
minuteItems.forEach((item) => {
|
||||
const option = document.createElement('ion-picker-column-option');
|
||||
option.value = item.value;
|
||||
option.textContent = item.text;
|
||||
|
||||
minutesColumn.appendChild(option);
|
||||
});
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const hoursColumn = page.locator('ion-picker-column#hours');
|
||||
const minutesColumn = page.locator('ion-picker-column#minutes');
|
||||
const hoursIonChange = await (hoursColumn as E2ELocator).spyOnEvent('ionChange');
|
||||
const minutesIonChange = await (minutesColumn as E2ELocator).spyOnEvent('ionChange');
|
||||
const highlight = page.locator('ion-picker .picker-highlight');
|
||||
|
||||
const box = await highlight.boundingBox();
|
||||
if (box !== null) {
|
||||
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
|
||||
}
|
||||
|
||||
// Simulate typing '2230' (22 hours, 30 minutes)
|
||||
await page.keyboard.press('Digit2');
|
||||
await page.keyboard.press('Digit2');
|
||||
await page.keyboard.press('Digit3');
|
||||
await page.keyboard.press('Digit0');
|
||||
|
||||
// Ensure the hours column is set to 22
|
||||
await expect(hoursIonChange).toHaveReceivedEventDetail({ value: 22 });
|
||||
await expect(hoursColumn).toHaveJSProperty('value', 22);
|
||||
|
||||
// Ensure the minutes column is set to 30
|
||||
await expect(minutesIonChange).toHaveReceivedEventDetail({ value: 30 });
|
||||
await expect(minutesColumn).toHaveJSProperty('value', 30);
|
||||
});
|
||||
|
||||
test('should set value to 2 and not wait for another digit when max value is 12', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/28877',
|
||||
});
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker>
|
||||
<ion-picker-column id="hours"></ion-picker-column>
|
||||
<ion-picker-column id="minutes"></ion-picker-column>
|
||||
</ion-picker>
|
||||
|
||||
<script>
|
||||
const hoursColumn = document.querySelector('ion-picker-column#hours');
|
||||
hoursColumn.numericInput = true;
|
||||
const hourItems = [
|
||||
{ text: '01', value: 1 },
|
||||
{ text: '02', value: 2 },
|
||||
{ text: '03', value: 3 },
|
||||
{ text: '04', value: 4 },
|
||||
{ text: '05', value: 5 },
|
||||
{ text: '06', value: 6 },
|
||||
{ text: '07', value: 7 },
|
||||
{ text: '08', value: 8 },
|
||||
{ text: '09', value: 9 },
|
||||
{ text: '10', value: 10 },
|
||||
{ text: '11', value: 11 },
|
||||
{ text: '12', value: 12 }
|
||||
];
|
||||
|
||||
hourItems.forEach((item) => {
|
||||
const option = document.createElement('ion-picker-column-option');
|
||||
option.value = item.value;
|
||||
option.textContent = item.text;
|
||||
|
||||
hoursColumn.appendChild(option);
|
||||
});
|
||||
|
||||
const minutesColumn = document.querySelector('ion-picker-column#minutes');
|
||||
minutesColumn.numericInput = true;
|
||||
const minuteItems = [
|
||||
{ text: '00', value: 0 },
|
||||
{ text: '15', value: 15 },
|
||||
{ text: '30', value: 30 },
|
||||
{ text: '45', value: 45 }
|
||||
];
|
||||
|
||||
minuteItems.forEach((item) => {
|
||||
const option = document.createElement('ion-picker-column-option');
|
||||
option.value = item.value;
|
||||
option.textContent = item.text;
|
||||
|
||||
minutesColumn.appendChild(option);
|
||||
});
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const hoursColumn = page.locator('ion-picker-column#hours');
|
||||
const minutesColumn = page.locator('ion-picker-column#minutes');
|
||||
const hoursIonChange = await (hoursColumn as E2ELocator).spyOnEvent('ionChange');
|
||||
const minutesIonChange = await (minutesColumn as E2ELocator).spyOnEvent('ionChange');
|
||||
const highlight = page.locator('ion-picker .picker-highlight');
|
||||
|
||||
const box = await highlight.boundingBox();
|
||||
if (box !== null) {
|
||||
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
|
||||
}
|
||||
|
||||
// Simulate typing '245' (2 hours, 45 minutes)
|
||||
await page.keyboard.press('Digit2');
|
||||
await page.keyboard.press('Digit4');
|
||||
await page.keyboard.press('Digit5');
|
||||
|
||||
// Ensure the hours column is set to 2
|
||||
await expect(hoursIonChange).toHaveReceivedEventDetail({ value: 2 });
|
||||
await expect(hoursColumn).toHaveJSProperty('value', 2);
|
||||
|
||||
// Ensure the minutes column is set to 45
|
||||
await expect(minutesIonChange).toHaveReceivedEventDetail({ value: 45 });
|
||||
await expect(minutesColumn).toHaveJSProperty('value', 45);
|
||||
});
|
||||
|
||||
test('pressing Enter should dismiss the keyboard', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ComponentInterface } from '@stencil/core';
|
||||
import { Component, Element, Host, Prop, Method, State, Watch, forceUpdate, h } from '@stencil/core';
|
||||
import type { ButtonInterface } from '@utils/element-interface';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
import { addEventListener, removeEventListener, inheritAttributes, getNextSiblingOfType } from '@utils/helpers';
|
||||
import { addEventListener, removeEventListener, inheritAttributes } from '@utils/helpers';
|
||||
import { hostContext } from '@utils/theme';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
@@ -65,41 +65,7 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
private waitForSegmentContent(ionSegment: HTMLIonSegmentElement | null, contentId: string): Promise<HTMLElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeoutId: NodeJS.Timeout | undefined = undefined;
|
||||
let animationFrameId: number;
|
||||
|
||||
const check = () => {
|
||||
if (!ionSegment) {
|
||||
reject(new Error(`Segment not found when looking for Segment Content`));
|
||||
return;
|
||||
}
|
||||
|
||||
const segmentView = getNextSiblingOfType<HTMLIonSegmentViewElement>(ionSegment); // Skip the text nodes
|
||||
const segmentContent = segmentView?.querySelector(
|
||||
`ion-segment-content[id="${contentId}"]`
|
||||
) as HTMLIonSegmentContentElement | null;
|
||||
if (segmentContent && timeoutId) {
|
||||
clearTimeout(timeoutId); // Clear the timeout if the segmentContent is found
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
resolve(segmentContent);
|
||||
} else {
|
||||
animationFrameId = requestAnimationFrame(check); // Keep checking on the next animation frame
|
||||
}
|
||||
};
|
||||
|
||||
check();
|
||||
|
||||
// Set a timeout to reject the promise
|
||||
timeoutId = setTimeout(() => {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
reject(new Error(`Unable to find Segment Content with id="${contentId} within 1000 ms`));
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
connectedCallback() {
|
||||
const segmentEl = (this.segmentEl = this.el.closest('ion-segment'));
|
||||
if (segmentEl) {
|
||||
this.updateState();
|
||||
@@ -107,27 +73,8 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
addEventListener(segmentEl, 'ionStyle', this.updateStyle);
|
||||
}
|
||||
|
||||
// Return if there is no contentId defined
|
||||
if (!this.contentId) return;
|
||||
|
||||
let segmentContent;
|
||||
try {
|
||||
// Attempt to find the Segment Content by its contentId
|
||||
segmentContent = await this.waitForSegmentContent(segmentEl, this.contentId);
|
||||
} catch (error) {
|
||||
// If no associated Segment Content exists, log an error and return
|
||||
console.error('Segment Button: ', (error as Error).message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the found element is a valid ION-SEGMENT-CONTENT
|
||||
if (segmentContent.tagName !== 'ION-SEGMENT-CONTENT') {
|
||||
console.error(`Segment Button: Element with id="${this.contentId}" is not an <ion-segment-content> element.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent buttons from being disabled when associated with segment content
|
||||
if (this.disabled) {
|
||||
if (this.contentId && this.disabled) {
|
||||
console.warn(`Segment Button: Segment buttons cannot be disabled when associated with an <ion-segment-content>.`);
|
||||
this.disabled = false;
|
||||
}
|
||||
@@ -146,6 +93,24 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
this.inheritedAttributes = {
|
||||
...inheritAttributes(this.el, ['aria-label']),
|
||||
};
|
||||
|
||||
// Return if there is no contentId defined
|
||||
if (!this.contentId) return;
|
||||
|
||||
// Attempt to find the Segment Content by its contentId
|
||||
const segmentContent = document.getElementById(this.contentId) as HTMLIonSegmentContentElement | null;
|
||||
|
||||
// If no associated Segment Content exists, log an error and return
|
||||
if (!segmentContent) {
|
||||
console.error(`Segment Button: Unable to find Segment Content with id="${this.contentId}".`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the found element is a valid ION-SEGMENT-CONTENT
|
||||
if (segmentContent.tagName !== 'ION-SEGMENT-CONTENT') {
|
||||
console.error(`Segment Button: Element with id="${this.contentId}" is not an <ion-segment-content> element.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private get hasLabel() {
|
||||
|
||||
@@ -388,17 +388,6 @@ export const shallowEqualStringMap = (
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getNextSiblingOfType = <T extends Element>(element: Element): T | null => {
|
||||
let sibling = element.nextSibling;
|
||||
while (sibling) {
|
||||
if (sibling.nodeType === Node.ELEMENT_NODE && (sibling as T) !== null) {
|
||||
return sibling as T;
|
||||
}
|
||||
sibling = sibling.nextSibling;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks input for usable number. Not NaN and not Infinite.
|
||||
*/
|
||||
|
||||
@@ -125,7 +125,7 @@ Shorthand for ionActionSheetDidDismiss.
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: ['animated', 'backdropDismiss', 'buttons', 'cssClass', 'enterAnimation', 'header', 'htmlAttributes', 'inputs', 'isOpen', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'subHeader', 'translucent', 'trigger'],
|
||||
inputs: ['animated', 'backdropDismiss', 'buttons', 'component', 'componentProps', 'cssClass', 'enterAnimation', 'header', 'htmlAttributes', 'inputs', 'isOpen', 'keepContentsMounted', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'subHeader', 'translucent', 'trigger'],
|
||||
methods: ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss']
|
||||
})
|
||||
@Component({
|
||||
@@ -133,7 +133,7 @@ Shorthand for ionActionSheetDidDismiss.
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['animated', 'backdropDismiss', 'buttons', 'cssClass', 'enterAnimation', 'header', 'htmlAttributes', 'inputs', 'isOpen', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'subHeader', 'translucent', 'trigger'],
|
||||
inputs: ['animated', 'backdropDismiss', 'buttons', 'component', 'componentProps', 'cssClass', 'enterAnimation', 'header', 'htmlAttributes', 'inputs', 'isOpen', 'keepContentsMounted', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'subHeader', 'translucent', 'trigger'],
|
||||
})
|
||||
export class IonAlert {
|
||||
protected el: HTMLIonAlertElement;
|
||||
|
||||
@@ -205,7 +205,7 @@ Shorthand for ionActionSheetDidDismiss.
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineIonAlert,
|
||||
inputs: ['animated', 'backdropDismiss', 'buttons', 'cssClass', 'enterAnimation', 'header', 'htmlAttributes', 'inputs', 'isOpen', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'subHeader', 'translucent', 'trigger'],
|
||||
inputs: ['animated', 'backdropDismiss', 'buttons', 'component', 'componentProps', 'cssClass', 'enterAnimation', 'header', 'htmlAttributes', 'inputs', 'isOpen', 'keepContentsMounted', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'subHeader', 'translucent', 'trigger'],
|
||||
methods: ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss']
|
||||
})
|
||||
@Component({
|
||||
@@ -213,7 +213,7 @@ Shorthand for ionActionSheetDidDismiss.
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['animated', 'backdropDismiss', 'buttons', 'cssClass', 'enterAnimation', 'header', 'htmlAttributes', 'inputs', 'isOpen', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'subHeader', 'translucent', 'trigger'],
|
||||
inputs: ['animated', 'backdropDismiss', 'buttons', 'component', 'componentProps', 'cssClass', 'enterAnimation', 'header', 'htmlAttributes', 'inputs', 'isOpen', 'keepContentsMounted', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'subHeader', 'translucent', 'trigger'],
|
||||
standalone: true
|
||||
})
|
||||
export class IonAlert {
|
||||
|
||||
Reference in New Issue
Block a user