mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-15 02:54:11 +08:00
fix(ios): TextField keyboard handling with emoji, autofill, and shortcuts (#10154)
closes https://github.com/NativeScript/NativeScript/issues/10108
This commit is contained in:
@ -10,6 +10,7 @@
|
|||||||
<Button text="box-shadow" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
<Button text="box-shadow" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
||||||
<Button text="css-playground" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
<Button text="css-playground" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
||||||
<Button text="datepicker" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
<Button text="datepicker" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
||||||
|
<Button text="forms" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
||||||
<Button text="image-async" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
<Button text="image-async" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
||||||
<Button text="image-handling" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
<Button text="image-handling" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
||||||
<Button text="labels" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
<Button text="labels" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
|
||||||
|
16
apps/toolbox/src/pages/forms.ts
Normal file
16
apps/toolbox/src/pages/forms.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Page, Observable, EventData } from '@nativescript/core';
|
||||||
|
|
||||||
|
let page: Page;
|
||||||
|
|
||||||
|
export function navigatingTo(args: EventData) {
|
||||||
|
page = <Page>args.object;
|
||||||
|
page.bindingContext = new SampleData();
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SampleData extends Observable {
|
||||||
|
textInput = '';
|
||||||
|
textChange(args) {
|
||||||
|
console.log(args.object.text);
|
||||||
|
this.notifyPropertyChange('textInput', args.object.text);
|
||||||
|
}
|
||||||
|
}
|
15
apps/toolbox/src/pages/forms.xml
Normal file
15
apps/toolbox/src/pages/forms.xml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
|
||||||
|
<Page.actionBar>
|
||||||
|
<ActionBar title="Labels and TextView" class="action-bar">
|
||||||
|
</ActionBar>
|
||||||
|
</Page.actionBar>
|
||||||
|
<ScrollView>
|
||||||
|
<StackLayout padding="20">
|
||||||
|
<Label text="TextField:" fontWeight="bold" />
|
||||||
|
<TextField textChange="{{ textChange }}" marginTop="6" backgroundColor="#efefef" padding="8" fontSize="18" keyboardType="url" />
|
||||||
|
|
||||||
|
<Label text="{{ textInput }}" marginTop="6" />
|
||||||
|
|
||||||
|
</StackLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</Page>
|
@ -44,6 +44,7 @@
|
|||||||
"css": "^3.0.0",
|
"css": "^3.0.0",
|
||||||
"css-tree": "^1.1.2",
|
"css-tree": "^1.1.2",
|
||||||
"dotenv": "10.0.0",
|
"dotenv": "10.0.0",
|
||||||
|
"emoji-regex": "^10.2.1",
|
||||||
"eslint": "7.22.0",
|
"eslint": "7.22.0",
|
||||||
"eslint-config-prettier": "^8.1.0",
|
"eslint-config-prettier": "^8.1.0",
|
||||||
"gonzales": "^1.0.7",
|
"gonzales": "^1.0.7",
|
||||||
|
3
packages/core/index.d.ts
vendored
3
packages/core/index.d.ts
vendored
@ -105,7 +105,7 @@ export type { InstrumentationMode, TimerInfo } from './profiling';
|
|||||||
export { encoding } from './text';
|
export { encoding } from './text';
|
||||||
export * from './trace';
|
export * from './trace';
|
||||||
export * from './ui';
|
export * from './ui';
|
||||||
import { GC, isFontIconURI, isDataURI, isFileOrResourcePath, executeOnMainThread, mainThreadify, isMainThread, dispatchToMainThread, executeOnUIThread, releaseNativeObject, getModuleName, openFile, openUrl, isRealDevice, layout, ad as androidUtils, iOSNativeHelper as iosUtils, Source, escapeRegexSymbols, convertString, dismissSoftInput, dismissKeyboard, queueMacrotask, queueGC, throttle, debounce, dataSerialize, dataDeserialize, copyToClipboard, getFileExtension } from './utils';
|
import { GC, isFontIconURI, isDataURI, isFileOrResourcePath, executeOnMainThread, mainThreadify, isMainThread, dispatchToMainThread, executeOnUIThread, releaseNativeObject, getModuleName, openFile, openUrl, isRealDevice, layout, ad as androidUtils, iOSNativeHelper as iosUtils, Source, escapeRegexSymbols, convertString, dismissSoftInput, dismissKeyboard, queueMacrotask, queueGC, throttle, debounce, dataSerialize, dataDeserialize, copyToClipboard, getFileExtension, isEmoji } from './utils';
|
||||||
import { SDK_VERSION } from './utils/constants';
|
import { SDK_VERSION } from './utils/constants';
|
||||||
import { ClassInfo, getClass, getBaseClasses, getClassInfo, isBoolean, isDefined, isFunction, isNullOrUndefined, isNumber, isObject, isString, isUndefined, toUIString, verifyCallback, numberHasDecimals, numberIs64Bit } from './utils/types';
|
import { ClassInfo, getClass, getBaseClasses, getClassInfo, isBoolean, isDefined, isFunction, isNullOrUndefined, isNumber, isObject, isString, isUndefined, toUIString, verifyCallback, numberHasDecimals, numberIs64Bit } from './utils/types';
|
||||||
export declare const Utils: {
|
export declare const Utils: {
|
||||||
@ -163,5 +163,6 @@ export declare const Utils: {
|
|||||||
dismissSoftInput: typeof dismissSoftInput;
|
dismissSoftInput: typeof dismissSoftInput;
|
||||||
dismissKeyboard: typeof dismissKeyboard;
|
dismissKeyboard: typeof dismissKeyboard;
|
||||||
copyToClipboard: typeof copyToClipboard;
|
copyToClipboard: typeof copyToClipboard;
|
||||||
|
isEmoji: typeof isEmoji;
|
||||||
};
|
};
|
||||||
export { XmlParser, ParserEventType, ParserEvent } from './xml';
|
export { XmlParser, ParserEventType, ParserEvent } from './xml';
|
||||||
|
@ -137,7 +137,7 @@ export * from './trace';
|
|||||||
|
|
||||||
export * from './ui';
|
export * from './ui';
|
||||||
|
|
||||||
import { GC, isFontIconURI, isDataURI, isFileOrResourcePath, executeOnMainThread, mainThreadify, isMainThread, dispatchToMainThread, executeOnUIThread, queueMacrotask, queueGC, debounce, throttle, releaseNativeObject, getModuleName, openFile, openUrl, isRealDevice, layout, ad as androidUtils, iOSNativeHelper as iosUtils, Source, RESOURCE_PREFIX, FILE_PREFIX, escapeRegexSymbols, convertString, dismissSoftInput, dismissKeyboard, dataDeserialize, dataSerialize, copyToClipboard, getFileExtension } from './utils';
|
import { GC, isFontIconURI, isDataURI, isFileOrResourcePath, executeOnMainThread, mainThreadify, isMainThread, dispatchToMainThread, executeOnUIThread, queueMacrotask, queueGC, debounce, throttle, releaseNativeObject, getModuleName, openFile, openUrl, isRealDevice, layout, ad as androidUtils, iOSNativeHelper as iosUtils, Source, RESOURCE_PREFIX, FILE_PREFIX, escapeRegexSymbols, convertString, dismissSoftInput, dismissKeyboard, dataDeserialize, dataSerialize, copyToClipboard, getFileExtension, isEmoji } from './utils';
|
||||||
import { SDK_VERSION } from './utils/constants';
|
import { SDK_VERSION } from './utils/constants';
|
||||||
import { ClassInfo, getClass, getBaseClasses, getClassInfo, isBoolean, isDefined, isFunction, isNullOrUndefined, isNumber, isObject, isString, isUndefined, toUIString, verifyCallback, numberHasDecimals, numberIs64Bit } from './utils/types';
|
import { ClassInfo, getClass, getBaseClasses, getClassInfo, isBoolean, isDefined, isFunction, isNullOrUndefined, isNumber, isObject, isString, isUndefined, toUIString, verifyCallback, numberHasDecimals, numberIs64Bit } from './utils/types';
|
||||||
|
|
||||||
@ -199,6 +199,7 @@ export const Utils = {
|
|||||||
dismissSoftInput,
|
dismissSoftInput,
|
||||||
dismissKeyboard,
|
dismissKeyboard,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
|
isEmoji,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { XmlParser, ParserEventType, ParserEvent } from './xml';
|
export { XmlParser, ParserEventType, ParserEvent } from './xml';
|
||||||
|
@ -47,6 +47,7 @@
|
|||||||
"@nativescript/hook": "~2.0.0",
|
"@nativescript/hook": "~2.0.0",
|
||||||
"acorn": "^8.7.0",
|
"acorn": "^8.7.0",
|
||||||
"css-tree": "^1.1.2",
|
"css-tree": "^1.1.2",
|
||||||
|
"emoji-regex": "^10.2.1",
|
||||||
"reduce-css-calc": "^2.1.7",
|
"reduce-css-calc": "^2.1.7",
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
},
|
},
|
||||||
|
@ -4,7 +4,7 @@ import { hintProperty, placeholderColorProperty, _updateCharactersInRangeReplace
|
|||||||
import { CoreTypes } from '../../core-types';
|
import { CoreTypes } from '../../core-types';
|
||||||
import { Color } from '../../color';
|
import { Color } from '../../color';
|
||||||
import { colorProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty, paddingLeftProperty } from '../styling/style-properties';
|
import { colorProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty, paddingLeftProperty } from '../styling/style-properties';
|
||||||
import { layout } from '../../utils';
|
import { layout, isEmoji } from '../../utils';
|
||||||
import { profile } from '../../profiling';
|
import { profile } from '../../profiling';
|
||||||
|
|
||||||
export * from './text-field-common';
|
export * from './text-field-common';
|
||||||
@ -200,7 +200,10 @@ export class TextField extends TextFieldBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.updateTextTrigger === 'textChanged') {
|
if (this.updateTextTrigger === 'textChanged') {
|
||||||
const shouldReplaceString = (textField.secureTextEntry && this.firstEdit) || delta > 1;
|
// 1. secureTextEntry with firstEdit should not replace
|
||||||
|
// 2. emoji's should not replace value
|
||||||
|
// 3. convenient keyboard shortcuts should not replace value (eg, '.com')
|
||||||
|
const shouldReplaceString = (textField.secureTextEntry && this.firstEdit) || (delta > 1 && !isEmoji(replacementString) && delta !== replacementString.length);
|
||||||
if (shouldReplaceString) {
|
if (shouldReplaceString) {
|
||||||
textProperty.nativeValueChange(this, replacementString);
|
textProperty.nativeValueChange(this, replacementString);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
import { dispatchToMainThread, dispatchToUIThread, isMainThread } from './mainthread-helper';
|
import { dispatchToMainThread, dispatchToUIThread, isMainThread } from './mainthread-helper';
|
||||||
import { sanitizeModuleName } from '../ui/builder/module-name-sanitizer';
|
import { sanitizeModuleName } from '../ui/builder/module-name-sanitizer';
|
||||||
|
import emojiRegex from 'emoji-regex';
|
||||||
|
|
||||||
import { GC } from './index';
|
import { GC } from './index';
|
||||||
|
|
||||||
@ -203,3 +204,9 @@ export function queueGC(delay = 900, useThrottle?: boolean) {
|
|||||||
debouncedGC.get(delay)();
|
debouncedGC.get(delay)();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isEmoji(value: string): boolean {
|
||||||
|
// TODO: In a future runtime update, we can switch to using Unicode Property Escapes:
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Unicode_Property_Escapes
|
||||||
|
return emojiRegex().test(value);
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user