Skip to content

Commit a0a77f7

Browse files
authored
feat(searchbar): autocapitalize, dir, lang, maxlength, and minlength are inherited to native input (#29098)
Issue number: resolves #27606 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> Certain attributes are not be inherited to the inner searchbar. Developers need control over these attributes to provide important context to users for things like language and text direction. Additionally, being able to control things like autocapitalize, maxlength, and minlength can help improve the user experience by a) guiding what should be entered into an input and b) removing autocapitalize where it's not appropriate. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Added autocapitalize, maxlength, and minlength properties - lang and dir are global attributes, so adding them as properties will cause issues. However, developers can still set them as attributes and they will be inherited to the native `input` element. We also watch them so any changes to the attributes are also inherited to the native `input`. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/.github/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> Note: We expanded the scope of this work to also include input and textarea, and this work will be handled separately. However, the original request was only for searchbar so that's why I associated this PR with the linked issue. Dev build: `7.7.3-dev.11709159644.114cd8b1`
1 parent 7cdbc1b commit a0a77f7

File tree

6 files changed

+127
-5
lines changed

6 files changed

+127
-5
lines changed

core/api.txt

+3
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,7 @@ ion-row,shadow
11581158

11591159
ion-searchbar,scoped
11601160
ion-searchbar,prop,animated,boolean,false,false,false
1161+
ion-searchbar,prop,autocapitalize,string,undefined,true,false
11611162
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
11621163
ion-searchbar,prop,autocorrect,"off" | "on",'off',false,false
11631164
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
11681169
ion-searchbar,prop,disabled,boolean,false,false,false
11691170
ion-searchbar,prop,enterkeyhint,"done" | "enter" | "go" | "next" | "previous" | "search" | "send" | undefined,undefined,false,false
11701171
ion-searchbar,prop,inputmode,"decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined,undefined,false,false
1172+
ion-searchbar,prop,maxlength,number | undefined,undefined,false,false
1173+
ion-searchbar,prop,minlength,number | undefined,undefined,false,false
11711174
ion-searchbar,prop,mode,"ios" | "md",undefined,false,false
11721175
ion-searchbar,prop,name,string,this.inputId,false,false
11731176
ion-searchbar,prop,placeholder,string,'Search',false,false

core/src/components.d.ts

+24
Original file line numberDiff line numberDiff line change
@@ -2552,6 +2552,10 @@ export namespace Components {
25522552
* If `true`, enable searchbar animation.
25532553
*/
25542554
"animated": boolean;
2555+
/**
2556+
* 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"`.
2557+
*/
2558+
"autocapitalize": string;
25552559
/**
25562560
* Set the input's autocomplete property.
25572561
*/
@@ -2596,6 +2600,14 @@ export namespace Components {
25962600
* A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`.
25972601
*/
25982602
"inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
2603+
/**
2604+
* This attribute specifies the maximum number of characters that the user can enter.
2605+
*/
2606+
"maxlength"?: number;
2607+
/**
2608+
* This attribute specifies the minimum number of characters that the user can enter.
2609+
*/
2610+
"minlength"?: number;
25992611
/**
26002612
* The mode determines which platform styles to use.
26012613
*/
@@ -7280,6 +7292,10 @@ declare namespace LocalJSX {
72807292
* If `true`, enable searchbar animation.
72817293
*/
72827294
"animated"?: boolean;
7295+
/**
7296+
* 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"`.
7297+
*/
7298+
"autocapitalize": string;
72837299
/**
72847300
* Set the input's autocomplete property.
72857301
*/
@@ -7320,6 +7336,14 @@ declare namespace LocalJSX {
73207336
* A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`.
73217337
*/
73227338
"inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
7339+
/**
7340+
* This attribute specifies the maximum number of characters that the user can enter.
7341+
*/
7342+
"maxlength"?: number;
7343+
/**
7344+
* This attribute specifies the minimum number of characters that the user can enter.
7345+
*/
7346+
"minlength"?: number;
73237347
/**
73247348
* The mode determines which platform styles to use.
73257349
*/

core/src/components/searchbar/searchbar.tsx

+69-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ComponentInterface, EventEmitter } from '@stencil/core';
22
import { Component, Element, Event, Host, Method, Prop, State, Watch, forceUpdate, h } from '@stencil/core';
3-
import { debounceEvent, raf, componentOnReady } from '@utils/helpers';
3+
import { debounceEvent, raf, componentOnReady, inheritAttributes } from '@utils/helpers';
4+
import type { Attributes } from '@utils/helpers';
45
import { isRTL } from '@utils/rtl';
56
import { createColorClasses } from '@utils/theme';
67
import { arrowBackSharp, closeCircle, closeSharp, searchOutline, searchSharp } from 'ionicons/icons';
@@ -28,6 +29,7 @@ export class Searchbar implements ComponentInterface {
2829
private shouldAlignLeft = true;
2930
private originalIonInput?: EventEmitter<SearchbarInputEventDetail>;
3031
private inputId = `ion-searchbar-${searchbarIds++}`;
32+
private inheritedAttributes: Attributes = {};
3133

3234
/**
3335
* The value of the input when the textarea is focused.
@@ -39,6 +41,31 @@ export class Searchbar implements ComponentInterface {
3941
@State() focused = false;
4042
@State() noAnimate = true;
4143

44+
/**
45+
* lang and dir are globally enumerated attributes.
46+
* As a result, creating these as properties
47+
* can have unintended side effects. Instead, we
48+
* listen for attribute changes and inherit them
49+
* to the inner `<input>` element.
50+
*/
51+
@Watch('lang')
52+
onLangChanged(newValue: string) {
53+
this.inheritedAttributes = {
54+
...this.inheritedAttributes,
55+
lang: newValue,
56+
};
57+
forceUpdate(this);
58+
}
59+
60+
@Watch('dir')
61+
onDirChanged(newValue: string) {
62+
this.inheritedAttributes = {
63+
...this.inheritedAttributes,
64+
dir: newValue,
65+
};
66+
forceUpdate(this);
67+
}
68+
4269
/**
4370
* The color to use from your application's color palette.
4471
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
@@ -51,6 +78,27 @@ export class Searchbar implements ComponentInterface {
5178
*/
5279
@Prop() animated = false;
5380

81+
/**
82+
* Prior to the addition of this property
83+
* autocapitalize was enabled by default on iOS
84+
* and disabled by default on Android
85+
* for Searchbar. The autocapitalize type on HTMLElement
86+
* requires that it be a string and never undefined.
87+
* However, setting it to a string value would be a breaking change
88+
* in behavior, so we use "!" to tell TypeScript that this property
89+
* is always defined so we can rely on the browser defaults. Browsers
90+
* will automatically set a default value if the developer does not set one.
91+
*
92+
* In the future, this property will default to "off" to align with
93+
* Input and Textarea, and the "!" will not be needed.
94+
*/
95+
96+
/**
97+
* Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user.
98+
* Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`.
99+
*/
100+
@Prop() autocapitalize!: string;
101+
54102
/**
55103
* Set the input's autocomplete property.
56104
*/
@@ -112,6 +160,16 @@ export class Searchbar implements ComponentInterface {
112160
*/
113161
@Prop() enterkeyhint?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send';
114162

163+
/**
164+
* This attribute specifies the maximum number of characters that the user can enter.
165+
*/
166+
@Prop() maxlength?: number;
167+
168+
/**
169+
* This attribute specifies the minimum number of characters that the user can enter.
170+
*/
171+
@Prop() minlength?: number;
172+
115173
/**
116174
* If used in a form, set the name of the control, which is submitted with the form data.
117175
*/
@@ -232,6 +290,12 @@ export class Searchbar implements ComponentInterface {
232290
this.emitStyle();
233291
}
234292

293+
componentWillLoad() {
294+
this.inheritedAttributes = {
295+
...inheritAttributes(this.el, ['lang', 'dir']),
296+
};
297+
}
298+
235299
componentDidLoad() {
236300
this.originalIonInput = this.ionInput;
237301
this.positionElements();
@@ -614,12 +678,16 @@ export class Searchbar implements ComponentInterface {
614678
onChange={this.onChange}
615679
onBlur={this.onBlur}
616680
onFocus={this.onFocus}
681+
minLength={this.minlength}
682+
maxLength={this.maxlength}
617683
placeholder={this.placeholder}
618684
type={this.type}
619685
value={this.getValue()}
686+
autoCapitalize={this.autocapitalize}
620687
autoComplete={this.autocomplete}
621688
autoCorrect={this.autocorrect}
622689
spellcheck={this.spellcheck}
690+
{...this.inheritedAttributes}
623691
/>
624692

625693
{mode === 'md' && cancelButton}

core/src/components/searchbar/test/searchbar.spec.ts

+26-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,37 @@ import { newSpecPage } from '@stencil/core/testing';
33
import { Searchbar } from '../searchbar';
44

55
describe('searchbar: rendering', () => {
6-
it('should inherit attributes', async () => {
6+
it('should inherit properties on load', async () => {
77
const page = await newSpecPage({
88
components: [Searchbar],
9-
html: '<ion-searchbar name="search"></ion-searchbar>',
9+
html: '<ion-searchbar autocapitalize="off" maxlength="4" minlength="2" name="search"></ion-searchbar>',
1010
});
1111

1212
const nativeEl = page.body.querySelector('ion-searchbar input')!;
1313
expect(nativeEl.getAttribute('name')).toBe('search');
14+
expect(nativeEl.getAttribute('maxlength')).toBe('4');
15+
expect(nativeEl.getAttribute('minlength')).toBe('2');
16+
expect(nativeEl.getAttribute('autocapitalize')).toBe('off');
17+
});
18+
19+
it('should inherit watched attributes', async () => {
20+
const page = await newSpecPage({
21+
components: [Searchbar],
22+
html: '<ion-searchbar dir="ltr" lang="en-US"></ion-searchbar>',
23+
});
24+
25+
const searchbarEl = page.body.querySelector('ion-searchbar')!;
26+
const nativeEl = searchbarEl.querySelector('input')!;
27+
28+
expect(nativeEl.getAttribute('lang')).toBe('en-US');
29+
expect(nativeEl.getAttribute('dir')).toBe('ltr');
30+
31+
searchbarEl.setAttribute('lang', 'es-ES');
32+
searchbarEl.setAttribute('dir', 'rtl');
33+
34+
await page.waitForChanges();
35+
36+
expect(nativeEl.getAttribute('lang')).toBe('es-ES');
37+
expect(nativeEl.getAttribute('dir')).toBe('rtl');
1438
});
1539
});

packages/angular/src/directives/proxies.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1788,15 +1788,15 @@ export declare interface IonRow extends Components.IonRow {}
17881788

17891789

17901790
@ProxyCmp({
1791-
inputs: ['animated', 'autocomplete', 'autocorrect', 'cancelButtonIcon', 'cancelButtonText', 'clearIcon', 'color', 'debounce', 'disabled', 'enterkeyhint', 'inputmode', 'mode', 'name', 'placeholder', 'searchIcon', 'showCancelButton', 'showClearButton', 'spellcheck', 'type', 'value'],
1791+
inputs: ['animated', 'autocapitalize', 'autocomplete', 'autocorrect', 'cancelButtonIcon', 'cancelButtonText', 'clearIcon', 'color', 'debounce', 'disabled', 'enterkeyhint', 'inputmode', 'maxlength', 'minlength', 'mode', 'name', 'placeholder', 'searchIcon', 'showCancelButton', 'showClearButton', 'spellcheck', 'type', 'value'],
17921792
methods: ['setFocus', 'getInputElement']
17931793
})
17941794
@Component({
17951795
selector: 'ion-searchbar',
17961796
changeDetection: ChangeDetectionStrategy.OnPush,
17971797
template: '<ng-content></ng-content>',
17981798
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
1799-
inputs: ['animated', 'autocomplete', 'autocorrect', 'cancelButtonIcon', 'cancelButtonText', 'clearIcon', 'color', 'debounce', 'disabled', 'enterkeyhint', 'inputmode', 'mode', 'name', 'placeholder', 'searchIcon', 'showCancelButton', 'showClearButton', 'spellcheck', 'type', 'value'],
1799+
inputs: ['animated', 'autocapitalize', 'autocomplete', 'autocorrect', 'cancelButtonIcon', 'cancelButtonText', 'clearIcon', 'color', 'debounce', 'disabled', 'enterkeyhint', 'inputmode', 'maxlength', 'minlength', 'mode', 'name', 'placeholder', 'searchIcon', 'showCancelButton', 'showClearButton', 'spellcheck', 'type', 'value'],
18001800
})
18011801
export class IonSearchbar {
18021802
protected el: HTMLElement;

packages/vue/src/proxies.ts

+3
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,7 @@ export const IonRow = /*@__PURE__*/ defineContainer<JSX.IonRow>('ion-row', defin
676676
export const IonSearchbar = /*@__PURE__*/ defineContainer<JSX.IonSearchbar, JSX.IonSearchbar["value"]>('ion-searchbar', defineIonSearchbar, [
677677
'color',
678678
'animated',
679+
'autocapitalize',
679680
'autocomplete',
680681
'autocorrect',
681682
'cancelButtonIcon',
@@ -685,6 +686,8 @@ export const IonSearchbar = /*@__PURE__*/ defineContainer<JSX.IonSearchbar, JSX.
685686
'disabled',
686687
'inputmode',
687688
'enterkeyhint',
689+
'maxlength',
690+
'minlength',
688691
'name',
689692
'placeholder',
690693
'searchIcon',

0 commit comments

Comments
 (0)