From d76260a56f2401e4b27c46489faa737997c93d11 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Mon, 26 Feb 2024 15:58:31 -0500 Subject: [PATCH 1/6] add initial props --- core/src/components.d.ts | 40 +++++++++++++++++++++ core/src/components/searchbar/searchbar.tsx | 26 ++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/core/src/components.d.ts b/core/src/components.d.ts index ab938b5bc16..f328b91952d 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -2552,6 +2552,10 @@ export namespace Components { * If `true`, enable searchbar animation. */ "animated": boolean; + /** + * Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user. Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`. + */ + "autocapitalize": string; /** * Set the input's autocomplete property. */ @@ -2580,6 +2584,10 @@ export namespace Components { * Set the amount of time, in milliseconds, to wait to trigger the `ionInput` event after each keystroke. */ "debounce"?: number; + /** + * The direction of the searchbar's text. + */ + "dir"?: 'ltr' | 'rtl' | 'auto'; /** * If `true`, the user cannot interact with the input. */ @@ -2596,6 +2604,18 @@ export namespace Components { * A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`. */ "inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'; + /** + * The language of the searchbar's text. + */ + "lang"?: string; + /** + * This attribute specifies the maximum number of characters that the user can enter. + */ + "maxlength"?: number; + /** + * This attribute specifies the minimum number of characters that the user can enter. + */ + "minlength"?: number; /** * The mode determines which platform styles to use. */ @@ -7280,6 +7300,10 @@ declare namespace LocalJSX { * If `true`, enable searchbar animation. */ "animated"?: boolean; + /** + * Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user. Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`. + */ + "autocapitalize"?: string; /** * Set the input's autocomplete property. */ @@ -7308,6 +7332,10 @@ declare namespace LocalJSX { * Set the amount of time, in milliseconds, to wait to trigger the `ionInput` event after each keystroke. */ "debounce"?: number; + /** + * The direction of the searchbar's text. + */ + "dir"?: 'ltr' | 'rtl' | 'auto'; /** * If `true`, the user cannot interact with the input. */ @@ -7320,6 +7348,18 @@ declare namespace LocalJSX { * A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`. */ "inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'; + /** + * The language of the searchbar's text. + */ + "lang"?: string; + /** + * This attribute specifies the maximum number of characters that the user can enter. + */ + "maxlength"?: number; + /** + * This attribute specifies the minimum number of characters that the user can enter. + */ + "minlength"?: number; /** * The mode determines which platform styles to use. */ diff --git a/core/src/components/searchbar/searchbar.tsx b/core/src/components/searchbar/searchbar.tsx index 21fed733d24..1b9206000bf 100644 --- a/core/src/components/searchbar/searchbar.tsx +++ b/core/src/components/searchbar/searchbar.tsx @@ -51,6 +51,12 @@ export class Searchbar implements ComponentInterface { */ @Prop() animated = false; + /** + * Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user. + * Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`. + */ + @Prop() autocapitalize = 'off'; + /** * Set the input's autocomplete property. */ @@ -93,6 +99,11 @@ export class Searchbar implements ComponentInterface { this.ionInput = debounce === undefined ? originalIonInput ?? ionInput : debounceEvent(ionInput, debounce); } + /** + * The direction of the searchbar's text. + */ + @Prop() dir?: 'ltr' | 'rtl' | 'auto'; + /** * If `true`, the user cannot interact with the input. */ @@ -112,6 +123,21 @@ export class Searchbar implements ComponentInterface { */ @Prop() enterkeyhint?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'; + /** + * The language of the searchbar's text. + */ + @Prop() lang?: string; + + /** + * This attribute specifies the maximum number of characters that the user can enter. + */ + @Prop() maxlength?: number; + + /** + * This attribute specifies the minimum number of characters that the user can enter. + */ + @Prop() minlength?: number; + /** * If used in a form, set the name of the control, which is submitted with the form data. */ From 6bd8d217cbe5a1a1432090fc423240f43f7f2469 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Wed, 28 Feb 2024 15:25:48 -0500 Subject: [PATCH 2/6] test(searchbar): add failing test --- .../searchbar/test/searchbar.spec.ts | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/core/src/components/searchbar/test/searchbar.spec.ts b/core/src/components/searchbar/test/searchbar.spec.ts index ad3a1f9137c..0f8e9c7461e 100644 --- a/core/src/components/searchbar/test/searchbar.spec.ts +++ b/core/src/components/searchbar/test/searchbar.spec.ts @@ -3,7 +3,7 @@ import { newSpecPage } from '@stencil/core/testing'; import { Searchbar } from '../searchbar'; describe('searchbar: rendering', () => { - it('should inherit attributes', async () => { + it('should inherit attributes on load', async () => { const page = await newSpecPage({ components: [Searchbar], html: '', @@ -12,4 +12,25 @@ describe('searchbar: rendering', () => { const nativeEl = page.body.querySelector('ion-searchbar input')!; expect(nativeEl.getAttribute('name')).toBe('search'); }); + + it('should inherit watched attributes', async () => { + const page = await newSpecPage({ + components: [Searchbar], + html: '', + }); + + const searchbarEl = page.body.querySelector('ion-searchbar')!; + const nativeEl = searchbarEl.querySelector('input')!; + + expect(nativeEl.getAttribute('lang')).toBe('en-US'); + expect(nativeEl.getAttribute('dir')).toBe('ltr'); + + searchbarEl.setAttribute('lang', 'es-ES'); + searchbarEl.setAttribute('dir', 'rtl'); + + await page.waitForChanges(); + + expect(nativeEl.getAttribute('lang')).toBe('es-ES'); + expect(nativeEl.getAttribute('dir')).toBe('rtl'); + }); }); From 9bfcb0075f4a4157cf8282b8aa17a7a28cf47853 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Wed, 28 Feb 2024 15:29:32 -0500 Subject: [PATCH 3/6] feat(searchbar): inherit common attributes --- core/api.txt | 3 ++ core/src/components.d.ts | 16 ------- core/src/components/searchbar/searchbar.tsx | 49 ++++++++++++++++----- packages/angular/src/directives/proxies.ts | 4 +- packages/vue/src/proxies.ts | 3 ++ 5 files changed, 46 insertions(+), 29 deletions(-) diff --git a/core/api.txt b/core/api.txt index acdcb8d0faf..a28410917e3 100644 --- a/core/api.txt +++ b/core/api.txt @@ -1158,6 +1158,7 @@ ion-row,shadow ion-searchbar,scoped ion-searchbar,prop,animated,boolean,false,false,false +ion-searchbar,prop,autocapitalize,string,'off',false,false ion-searchbar,prop,autocomplete,"name" | "email" | "tel" | "url" | "on" | "off" | "honorific-prefix" | "given-name" | "additional-name" | "family-name" | "honorific-suffix" | "nickname" | "username" | "new-password" | "current-password" | "one-time-code" | "organization-title" | "organization" | "street-address" | "address-line1" | "address-line2" | "address-line3" | "address-level4" | "address-level3" | "address-level2" | "address-level1" | "country" | "country-name" | "postal-code" | "cc-name" | "cc-given-name" | "cc-additional-name" | "cc-family-name" | "cc-number" | "cc-exp" | "cc-exp-month" | "cc-exp-year" | "cc-csc" | "cc-type" | "transaction-currency" | "transaction-amount" | "language" | "bday" | "bday-day" | "bday-month" | "bday-year" | "sex" | "tel-country-code" | "tel-national" | "tel-area-code" | "tel-local" | "tel-extension" | "impp" | "photo",'off',false,false ion-searchbar,prop,autocorrect,"off" | "on",'off',false,false ion-searchbar,prop,cancelButtonIcon,string,config.get('backButtonIcon', arrowBackSharp) as string,false,false @@ -1168,6 +1169,8 @@ ion-searchbar,prop,debounce,number | undefined,undefined,false,false ion-searchbar,prop,disabled,boolean,false,false,false ion-searchbar,prop,enterkeyhint,"done" | "enter" | "go" | "next" | "previous" | "search" | "send" | undefined,undefined,false,false ion-searchbar,prop,inputmode,"decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined,undefined,false,false +ion-searchbar,prop,maxlength,number | undefined,undefined,false,false +ion-searchbar,prop,minlength,number | undefined,undefined,false,false ion-searchbar,prop,mode,"ios" | "md",undefined,false,false ion-searchbar,prop,name,string,this.inputId,false,false ion-searchbar,prop,placeholder,string,'Search',false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index f328b91952d..564fd007c3d 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -2584,10 +2584,6 @@ export namespace Components { * Set the amount of time, in milliseconds, to wait to trigger the `ionInput` event after each keystroke. */ "debounce"?: number; - /** - * The direction of the searchbar's text. - */ - "dir"?: 'ltr' | 'rtl' | 'auto'; /** * If `true`, the user cannot interact with the input. */ @@ -2604,10 +2600,6 @@ export namespace Components { * A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`. */ "inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'; - /** - * The language of the searchbar's text. - */ - "lang"?: string; /** * This attribute specifies the maximum number of characters that the user can enter. */ @@ -7332,10 +7324,6 @@ declare namespace LocalJSX { * Set the amount of time, in milliseconds, to wait to trigger the `ionInput` event after each keystroke. */ "debounce"?: number; - /** - * The direction of the searchbar's text. - */ - "dir"?: 'ltr' | 'rtl' | 'auto'; /** * If `true`, the user cannot interact with the input. */ @@ -7348,10 +7336,6 @@ declare namespace LocalJSX { * A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`. */ "inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'; - /** - * The language of the searchbar's text. - */ - "lang"?: string; /** * This attribute specifies the maximum number of characters that the user can enter. */ diff --git a/core/src/components/searchbar/searchbar.tsx b/core/src/components/searchbar/searchbar.tsx index 1b9206000bf..9e94aba3b50 100644 --- a/core/src/components/searchbar/searchbar.tsx +++ b/core/src/components/searchbar/searchbar.tsx @@ -1,6 +1,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; import { Component, Element, Event, Host, Method, Prop, State, Watch, forceUpdate, h } from '@stencil/core'; -import { debounceEvent, raf, componentOnReady } from '@utils/helpers'; +import { debounceEvent, raf, componentOnReady, inheritAttributes } from '@utils/helpers'; +import type { Attributes } from '@utils/helpers'; import { isRTL } from '@utils/rtl'; import { createColorClasses } from '@utils/theme'; import { arrowBackSharp, closeCircle, closeSharp, searchOutline, searchSharp } from 'ionicons/icons'; @@ -28,6 +29,7 @@ export class Searchbar implements ComponentInterface { private shouldAlignLeft = true; private originalIonInput?: EventEmitter; private inputId = `ion-searchbar-${searchbarIds++}`; + private inheritedAttributes: Attributes = {}; /** * The value of the input when the textarea is focused. @@ -39,6 +41,31 @@ export class Searchbar implements ComponentInterface { @State() focused = false; @State() noAnimate = true; + /** + * lang and dir are globally enumerated attributes. + * As a result, creating these are properties + * can have unintended side effects. Instead, we + * listen for attribute changes and inherit them + * to the inner `` element. + */ + @Watch('lang') + onLangChanged(newValue: string) { + this.inheritedAttributes = { + ...this.inheritedAttributes, + lang: newValue, + }; + forceUpdate(this); + } + + @Watch('dir') + onDirChanged(newValue: string) { + this.inheritedAttributes = { + ...this.inheritedAttributes, + dir: newValue, + }; + forceUpdate(this); + } + /** * The color to use from your application's color palette. * Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. @@ -99,11 +126,6 @@ export class Searchbar implements ComponentInterface { this.ionInput = debounce === undefined ? originalIonInput ?? ionInput : debounceEvent(ionInput, debounce); } - /** - * The direction of the searchbar's text. - */ - @Prop() dir?: 'ltr' | 'rtl' | 'auto'; - /** * If `true`, the user cannot interact with the input. */ @@ -123,11 +145,6 @@ export class Searchbar implements ComponentInterface { */ @Prop() enterkeyhint?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'; - /** - * The language of the searchbar's text. - */ - @Prop() lang?: string; - /** * This attribute specifies the maximum number of characters that the user can enter. */ @@ -258,6 +275,12 @@ export class Searchbar implements ComponentInterface { this.emitStyle(); } + componentWillLoad() { + this.inheritedAttributes = { + ...inheritAttributes(this.el, ['lang', 'dir']), + }; + } + componentDidLoad() { this.originalIonInput = this.ionInput; this.positionElements(); @@ -640,12 +663,16 @@ export class Searchbar implements ComponentInterface { onChange={this.onChange} onBlur={this.onBlur} onFocus={this.onFocus} + minLength={this.minlength} + maxLength={this.maxlength} placeholder={this.placeholder} type={this.type} value={this.getValue()} + autoCapitalize={this.autocapitalize} autoComplete={this.autocomplete} autoCorrect={this.autocorrect} spellcheck={this.spellcheck} + {...this.inheritedAttributes} /> {mode === 'md' && cancelButton} diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index a592c55894b..b60fd38f7fc 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -1788,7 +1788,7 @@ export declare interface IonRow extends Components.IonRow {} @ProxyCmp({ - inputs: ['animated', 'autocomplete', 'autocorrect', 'cancelButtonIcon', 'cancelButtonText', 'clearIcon', 'color', 'debounce', 'disabled', 'enterkeyhint', 'inputmode', 'mode', 'name', 'placeholder', 'searchIcon', 'showCancelButton', 'showClearButton', 'spellcheck', 'type', 'value'], + inputs: ['animated', 'autocapitalize', 'autocomplete', 'autocorrect', 'cancelButtonIcon', 'cancelButtonText', 'clearIcon', 'color', 'debounce', 'disabled', 'enterkeyhint', 'inputmode', 'maxlength', 'minlength', 'mode', 'name', 'placeholder', 'searchIcon', 'showCancelButton', 'showClearButton', 'spellcheck', 'type', 'value'], methods: ['setFocus', 'getInputElement'] }) @Component({ @@ -1796,7 +1796,7 @@ export declare interface IonRow extends Components.IonRow {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['animated', 'autocomplete', 'autocorrect', 'cancelButtonIcon', 'cancelButtonText', 'clearIcon', 'color', 'debounce', 'disabled', 'enterkeyhint', 'inputmode', 'mode', 'name', 'placeholder', 'searchIcon', 'showCancelButton', 'showClearButton', 'spellcheck', 'type', 'value'], + inputs: ['animated', 'autocapitalize', 'autocomplete', 'autocorrect', 'cancelButtonIcon', 'cancelButtonText', 'clearIcon', 'color', 'debounce', 'disabled', 'enterkeyhint', 'inputmode', 'maxlength', 'minlength', 'mode', 'name', 'placeholder', 'searchIcon', 'showCancelButton', 'showClearButton', 'spellcheck', 'type', 'value'], }) export class IonSearchbar { protected el: HTMLElement; diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 57380e47bde..5d0272ce081 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -676,6 +676,7 @@ export const IonRow = /*@__PURE__*/ defineContainer('ion-row', defin export const IonSearchbar = /*@__PURE__*/ defineContainer('ion-searchbar', defineIonSearchbar, [ 'color', 'animated', + 'autocapitalize', 'autocomplete', 'autocorrect', 'cancelButtonIcon', @@ -685,6 +686,8 @@ export const IonSearchbar = /*@__PURE__*/ defineContainer Date: Wed, 28 Feb 2024 15:33:02 -0500 Subject: [PATCH 4/6] typo --- core/src/components/searchbar/searchbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/components/searchbar/searchbar.tsx b/core/src/components/searchbar/searchbar.tsx index 9e94aba3b50..63be24f4f0f 100644 --- a/core/src/components/searchbar/searchbar.tsx +++ b/core/src/components/searchbar/searchbar.tsx @@ -43,7 +43,7 @@ export class Searchbar implements ComponentInterface { /** * lang and dir are globally enumerated attributes. - * As a result, creating these are properties + * As a result, creating these as properties * can have unintended side effects. Instead, we * listen for attribute changes and inherit them * to the inner `` element. From 5a455bf6b183cc59843ca5aadf8a12376df1d82f Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Wed, 28 Feb 2024 15:40:48 -0500 Subject: [PATCH 5/6] test(searchbar): add tests for properties too --- core/src/components/searchbar/test/searchbar.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/components/searchbar/test/searchbar.spec.ts b/core/src/components/searchbar/test/searchbar.spec.ts index 0f8e9c7461e..0622a69ace5 100644 --- a/core/src/components/searchbar/test/searchbar.spec.ts +++ b/core/src/components/searchbar/test/searchbar.spec.ts @@ -3,14 +3,17 @@ import { newSpecPage } from '@stencil/core/testing'; import { Searchbar } from '../searchbar'; describe('searchbar: rendering', () => { - it('should inherit attributes on load', async () => { + it('should inherit properties on load', async () => { const page = await newSpecPage({ components: [Searchbar], - html: '', + html: '', }); const nativeEl = page.body.querySelector('ion-searchbar input')!; expect(nativeEl.getAttribute('name')).toBe('search'); + expect(nativeEl.getAttribute('maxlength')).toBe('4'); + expect(nativeEl.getAttribute('minlength')).toBe('2'); + expect(nativeEl.getAttribute('autocapitalize')).toBe('off'); }); it('should inherit watched attributes', async () => { From 14cd8b1c1a586bac7e4a6212c9ff1fc004fd21ea Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Wed, 28 Feb 2024 17:33:13 -0500 Subject: [PATCH 6/6] default to undefined --- core/api.txt | 2 +- core/src/components.d.ts | 2 +- core/src/components/searchbar/searchbar.tsx | 17 ++++++++++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/core/api.txt b/core/api.txt index a28410917e3..498263defe7 100644 --- a/core/api.txt +++ b/core/api.txt @@ -1158,7 +1158,7 @@ ion-row,shadow ion-searchbar,scoped ion-searchbar,prop,animated,boolean,false,false,false -ion-searchbar,prop,autocapitalize,string,'off',false,false +ion-searchbar,prop,autocapitalize,string,undefined,true,false ion-searchbar,prop,autocomplete,"name" | "email" | "tel" | "url" | "on" | "off" | "honorific-prefix" | "given-name" | "additional-name" | "family-name" | "honorific-suffix" | "nickname" | "username" | "new-password" | "current-password" | "one-time-code" | "organization-title" | "organization" | "street-address" | "address-line1" | "address-line2" | "address-line3" | "address-level4" | "address-level3" | "address-level2" | "address-level1" | "country" | "country-name" | "postal-code" | "cc-name" | "cc-given-name" | "cc-additional-name" | "cc-family-name" | "cc-number" | "cc-exp" | "cc-exp-month" | "cc-exp-year" | "cc-csc" | "cc-type" | "transaction-currency" | "transaction-amount" | "language" | "bday" | "bday-day" | "bday-month" | "bday-year" | "sex" | "tel-country-code" | "tel-national" | "tel-area-code" | "tel-local" | "tel-extension" | "impp" | "photo",'off',false,false ion-searchbar,prop,autocorrect,"off" | "on",'off',false,false ion-searchbar,prop,cancelButtonIcon,string,config.get('backButtonIcon', arrowBackSharp) as string,false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 564fd007c3d..6102aa8db7b 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -7295,7 +7295,7 @@ declare namespace LocalJSX { /** * Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user. Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`. */ - "autocapitalize"?: string; + "autocapitalize": string; /** * Set the input's autocomplete property. */ diff --git a/core/src/components/searchbar/searchbar.tsx b/core/src/components/searchbar/searchbar.tsx index 63be24f4f0f..cf48cf4a049 100644 --- a/core/src/components/searchbar/searchbar.tsx +++ b/core/src/components/searchbar/searchbar.tsx @@ -78,11 +78,26 @@ export class Searchbar implements ComponentInterface { */ @Prop() animated = false; + /** + * Prior to the addition of this property + * autocapitalize was enabled by default on iOS + * and disabled by default on Android + * for Searchbar. The autocapitalize type on HTMLElement + * requires that it be a string and never undefined. + * However, setting it to a string value would be a breaking change + * in behavior, so we use "!" to tell TypeScript that this property + * is always defined so we can rely on the browser defaults. Browsers + * will automatically set a default value if the developer does not set one. + * + * In the future, this property will default to "off" to align with + * Input and Textarea, and the "!" will not be needed. + */ + /** * Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user. * Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`. */ - @Prop() autocapitalize = 'off'; + @Prop() autocapitalize!: string; /** * Set the input's autocomplete property.