diff --git a/angular/src/directives/proxies.ts b/angular/src/directives/proxies.ts index e7e8a46950..24896ee324 100644 --- a/angular/src/directives/proxies.ts +++ b/angular/src/directives/proxies.ts @@ -977,7 +977,7 @@ export declare interface IonInfiniteScrollContent extends Components.IonInfinite @ProxyCmp({ - inputs: ['accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'legacy', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'size', 'spellcheck', 'step', 'type', 'value'], + inputs: ['accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'legacy', 'mask', 'maskPlaceholder', 'maskVisibility', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'size', 'spellcheck', 'step', 'type', 'value'], methods: ['setFocus', 'getInputElement'] }) @Component({ @@ -985,7 +985,7 @@ export declare interface IonInfiniteScrollContent extends Components.IonInfinite changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'legacy', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'size', 'spellcheck', 'step', 'type', 'value'], + inputs: ['accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'legacy', 'mask', 'maskPlaceholder', 'maskVisibility', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'size', 'spellcheck', 'step', 'type', 'value'], }) export class IonInput { protected el: HTMLElement; diff --git a/core/api.txt b/core/api.txt index f25a05b642..089d7459dd 100644 --- a/core/api.txt +++ b/core/api.txt @@ -553,6 +553,9 @@ ion-input,prop,inputmode,"decimal" | "email" | "none" | "numeric" | "search" | " ion-input,prop,label,string | undefined,undefined,false,false ion-input,prop,labelPlacement,"end" | "fixed" | "floating" | "stacked" | "start",'start',false,false ion-input,prop,legacy,boolean | undefined,undefined,false,false +ion-input,prop,mask,(string | RegExp)[] | RegExp | undefined,undefined,false,false +ion-input,prop,maskPlaceholder,null | string | undefined,'_',false,false +ion-input,prop,maskVisibility,"always" | "focus" | "never" | undefined,'always',false,false ion-input,prop,max,number | string | undefined,undefined,false,false ion-input,prop,maxlength,number | undefined,undefined,false,false ion-input,prop,min,number | string | undefined,undefined,false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index b61bac1a57..4943d8862a 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -17,6 +17,7 @@ import { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interf import { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface"; import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimePresentation, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface"; import { SpinnerTypes } from "./components/spinner/spinner-configs"; +import { MaskExpression, MaskPlaceholder, MaskVisibility } from "./utils/input-masking"; import { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface"; import { CounterFormatter } from "./components/item/item-interface"; import { MenuChangeEventDetail, Side } from "./components/menu/menu-interface"; @@ -53,6 +54,7 @@ export { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interf export { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface"; export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimePresentation, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface"; export { SpinnerTypes } from "./components/spinner/spinner-configs"; +export { MaskExpression, MaskPlaceholder, MaskVisibility } from "./utils/input-masking"; export { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface"; export { CounterFormatter } from "./components/item/item-interface"; export { MenuChangeEventDetail, Side } from "./components/menu/menu-interface"; @@ -1225,6 +1227,18 @@ export namespace Components { * Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt components in to the modern form markup when they are using either the `aria-label` attribute or the `label` property. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup. */ "legacy"?: boolean; + /** + * The predefined format of the user's input. For example, you can set a mask that only accepts digits, or you can configure a more complex pattern like a phone number or credit card number. The mask supports two formats: 1. A valid [regular expression pattern](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) 2. An array containing regular expression and fixed character patterns The fixed characters in the mask cannot be erased or replaced by the user. For example in a phone number mask, the `(`, `)` and `-` are examples of fixed characters. + */ + "mask"?: MaskExpression; + /** + * Character or string to cover unfilled parts of the mask. The default character is `_`. If set to `null`, `undefined` or an empty string, unfilled parts will be empty as in a regular input. + */ + "maskPlaceholder"?: MaskPlaceholder; + /** + * The visibility of the mask placeholder. With `always`, the placeholder will be visible even when the control does not have focus. With `focus`, the placeholder will only be visible when the control has focus. With `never`, the placeholder will never be visibly displayed. + */ + "maskVisibility"?: MaskVisibility; /** * The maximum value, which must not be less than its minimum (min attribute) value. */ @@ -5255,6 +5269,18 @@ declare namespace LocalJSX { * Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt components in to the modern form markup when they are using either the `aria-label` attribute or the `label` property. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup. */ "legacy"?: boolean; + /** + * The predefined format of the user's input. For example, you can set a mask that only accepts digits, or you can configure a more complex pattern like a phone number or credit card number. The mask supports two formats: 1. A valid [regular expression pattern](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) 2. An array containing regular expression and fixed character patterns The fixed characters in the mask cannot be erased or replaced by the user. For example in a phone number mask, the `(`, `)` and `-` are examples of fixed characters. + */ + "mask"?: MaskExpression; + /** + * Character or string to cover unfilled parts of the mask. The default character is `_`. If set to `null`, `undefined` or an empty string, unfilled parts will be empty as in a regular input. + */ + "maskPlaceholder"?: MaskPlaceholder; + /** + * The visibility of the mask placeholder. With `always`, the placeholder will be visible even when the control does not have focus. With `focus`, the placeholder will only be visible when the control has focus. With `never`, the placeholder will never be visibly displayed. + */ + "maskVisibility"?: MaskVisibility; /** * The maximum value, which must not be less than its minimum (min attribute) value. */ diff --git a/core/src/components/input/input.tsx b/core/src/components/input/input.tsx index 166f5c2539..b05fd693ea 100644 --- a/core/src/components/input/input.tsx +++ b/core/src/components/input/input.tsx @@ -8,6 +8,7 @@ import type { LegacyFormController } from '../../utils/forms'; import { createLegacyFormController } from '../../utils/forms'; import type { Attributes } from '../../utils/helpers'; import { inheritAriaAttributes, debounceEvent, findItemLabel, inheritAttributes } from '../../utils/helpers'; +import type { MaskExpression, MaskPlaceholder, MaskVisibility } from '../../utils/input-masking'; import { printIonWarning } from '../../utils/logging'; import { createColorClasses, hostContext } from '../../utils/theme'; @@ -272,6 +273,35 @@ export class Input implements ComponentInterface { */ @Prop({ mutable: true }) value?: string | number | null = ''; + /** + * The predefined format of the user's input. For example, you can set a mask + * that only accepts digits, or you can configure a more complex pattern like + * a phone number or credit card number. + * + * The mask supports two formats: + * 1. A valid [regular expression pattern](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) + * 2. An array containing regular expression and fixed character patterns + * + * The fixed characters in the mask cannot be erased or replaced by the user. For example + * in a phone number mask, the `(`, `)` and `-` are examples of fixed characters. + * + */ + @Prop() mask?: MaskExpression; + + /** + * The visibility of the mask placeholder. With `always`, the placeholder will be + * visible even when the control does not have focus. With `focus`, the placeholder + * will only be visible when the control has focus. With `never`, the placeholder will + * never be visibly displayed. + */ + @Prop() maskVisibility?: MaskVisibility = 'always'; + + /** + * Character or string to cover unfilled parts of the mask. The default character is `_`. + * If set to `null`, `undefined` or an empty string, unfilled parts will be empty as in a regular input. + */ + @Prop() maskPlaceholder?: MaskPlaceholder = '_'; + /** * The `ionInput` event fires when the `value` of an `` element * has been changed. diff --git a/core/src/components/input/test/input.spec.ts b/core/src/components/input/test/input.spec.ts index f07d46c8f0..d2ae1e417b 100644 --- a/core/src/components/input/test/input.spec.ts +++ b/core/src/components/input/test/input.spec.ts @@ -1,4 +1,5 @@ import { newSpecPage } from '@stencil/core/testing'; + import { Input } from '../input'; it('should inherit attributes', async () => { @@ -7,7 +8,7 @@ it('should inherit attributes', async () => { html: '', }); - const nativeEl = page.body.querySelector('ion-input input'); + const nativeEl = page.body.querySelector('ion-input input')!; expect(nativeEl.getAttribute('title')).toBe('my title'); expect(nativeEl.getAttribute('tabindex')).toBe('-1'); expect(nativeEl.getAttribute('data-form-type')).toBe('password'); diff --git a/core/src/interface.d.ts b/core/src/interface.d.ts index 4e2c085a63..be14e3f26a 100644 --- a/core/src/interface.d.ts +++ b/core/src/interface.d.ts @@ -43,6 +43,7 @@ export { AnimationKeyFrames, AnimationLifecycle, } from './utils/animation/animation-interface'; +export { MaskExpression, MaskPlaceholder, MaskVisibility } from './utils/input-masking'; export { HTMLIonOverlayElement, OverlayController, OverlayInterface } from './utils/overlays-interface'; export { Config, config } from './global/config'; export { Gesture, GestureConfig, GestureDetail } from './utils/gesture'; diff --git a/core/src/utils/input-masking/index.ts b/core/src/utils/input-masking/index.ts new file mode 100644 index 0000000000..79c5527fd3 --- /dev/null +++ b/core/src/utils/input-masking/index.ts @@ -0,0 +1 @@ +export * from './input-masking-interface'; diff --git a/core/src/utils/input-masking/input-masking-interface.ts b/core/src/utils/input-masking/input-masking-interface.ts new file mode 100644 index 0000000000..9f2fae70c0 --- /dev/null +++ b/core/src/utils/input-masking/input-masking-interface.ts @@ -0,0 +1,5 @@ +export type MaskVisibility = 'always' | 'focus' | 'never'; + +export type MaskExpression = RegExp | (string | RegExp)[]; + +export type MaskPlaceholder = string | null | undefined; diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index f4cae3f9a9..f41fca49d2 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -428,6 +428,9 @@ export const IonInput = /*@__PURE__*/ defineContainer