From fdb1dc82e1b94cace742eca433dc7cf5f8cb9ff9 Mon Sep 17 00:00:00 2001 From: Peter Skelin Date: Mon, 24 Feb 2025 09:47:28 +0200 Subject: [PATCH 1/6] chore: improve input value handling --- .../src/thirdparty/preact/preact.module.js | 3 +- packages/main/src/Input.ts | 62 +------------------ packages/main/src/InputTemplate.tsx | 2 +- packages/main/test/pages/Input_old_value.html | 42 +++++++++++++ 4 files changed, 48 insertions(+), 61 deletions(-) create mode 100644 packages/main/test/pages/Input_old_value.html diff --git a/packages/base/src/thirdparty/preact/preact.module.js b/packages/base/src/thirdparty/preact/preact.module.js index d15d0c3b2457..1b943bd67b10 100644 --- a/packages/base/src/thirdparty/preact/preact.module.js +++ b/packages/base/src/thirdparty/preact/preact.module.js @@ -1 +1,2 @@ -var n,l,u,t,i,o,r,e,f,c,s,a,h,p={},v=[],y=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,d=Array.isArray;function w(n,l){for(var u in l)n[u]=l[u];return n}function _(n){n&&n.parentNode&&n.parentNode.removeChild(n)}function g(l,u,t){var i,o,r,e={};for(r in u)"key"==r?i=u[r]:"ref"==r?o=u[r]:e[r]=u[r];if(arguments.length>2&&(e.children=arguments.length>3?n.call(arguments,2):t),"function"==typeof l&&null!=l.defaultProps)for(r in l.defaultProps)void 0===e[r]&&(e[r]=l.defaultProps[r]);return m(l,e,i,o,null)}function m(n,t,i,o,r){var e={type:n,props:t,key:i,ref:o,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:null==r?++u:r,__i:-1,__u:0};return null==r&&null!=l.vnode&&l.vnode(e),e}function b(){return{current:null}}function k(n){return n.children}function x(n,l){this.props=n,this.context=l}function C(n,l){if(null==l)return n.__?C(n.__,n.__i+1):null;for(var u;lu&&i.sort(e));P.__r=0}function $(n,l,u,t,i,o,r,e,f,c,s){var a,h,y,d,w,_,g=t&&t.__k||v,m=l.length;for(f=I(u,l,g,f),a=0;a0?m(o.type,o.props,o.key,o.ref?o.ref:null,o.__v):o).__=n,o.__b=n.__b+1,r=null,-1!==(f=o.__i=O(o,u,e,a))&&(a--,(r=u[f])&&(r.__u|=2)),null==r||null===r.__v?(-1==f&&h--,"function"!=typeof o.type&&(o.__u|=4)):f!==e&&(f==e-1?h--:f==e+1?h++:(f>e?h--:h++,o.__u|=4))):o=n.__k[i]=null;if(a)for(i=0;i(null!=f&&0==(2&f.__u)?1:0))for(;r>=0||e=0){if((f=l[r])&&0==(2&f.__u)&&i==f.key&&o===f.type)return r;r--}if(e2&&(f.children=arguments.length>3?n.call(arguments,2):t),m(l.type,f,i||l.key,o||l.ref,null)}function J(n,l){var u={__c:l="__cC"+h++,__:n,Consumer:function(n,l){return n.children(l)},Provider:function(n){var u,t;return this.getChildContext||(u=new Set,(t={})[l]=this,this.getChildContext=function(){return t},this.componentWillUnmount=function(){u=null},this.shouldComponentUpdate=function(n){this.props.value!==n.value&&u.forEach(function(n){n.__e=!0,M(n)})},this.sub=function(n){u.add(n);var l=n.componentWillUnmount;n.componentWillUnmount=function(){u&&u.delete(n),l&&l.call(n)}}),n.children}};return u.Provider.__=u.Consumer.contextType=u}n=v.slice,l={__e:function(n,l,u,t){for(var i,o,r;l=l.__;)if((i=l.__c)&&!i.__)try{if((o=i.constructor)&&null!=o.getDerivedStateFromError&&(i.setState(o.getDerivedStateFromError(n)),r=i.__d),null!=i.componentDidCatch&&(i.componentDidCatch(n,t||{}),r=i.__d),r)return i.__E=i}catch(l){n=l}throw n}},u=0,t=function(n){return null!=n&&null==n.constructor},x.prototype.setState=function(n,l){var u;u=null!=this.__s&&this.__s!==this.state?this.__s:this.__s=w({},this.state),"function"==typeof n&&(n=n(w({},u),this.props)),n&&w(u,n),null!=n&&this.__v&&(l&&this._sb.push(l),M(this))},x.prototype.forceUpdate=function(n){this.__v&&(this.__e=!0,n&&this.__h.push(n),M(this))},x.prototype.render=k,i=[],r="function"==typeof Promise?Promise.prototype.then.bind(Promise.resolve()):setTimeout,e=function(n,l){return n.__v.__b-l.__v.__b},P.__r=0,f=/(PointerCapture)$|Capture$/i,c=0,s=A(!1),a=A(!0),h=0;export{x as Component,k as Fragment,G as cloneElement,J as createContext,g as createElement,b as createRef,g as h,E as hydrate,t as isValidElement,l as options,D as render,L as toChildArray}; +var n,l,t,u,i,o,r,e,f,c,s,a,h,p={},v=[],y=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,d=Array.isArray;function w(n,l){for(var t in l)n[t]=l[t];return n}function _(n){n&&n.parentNode&&n.parentNode.removeChild(n)}function g(l,t,u){var i,o,r,e={};for(r in t)"key"==r?i=t[r]:"ref"==r?o=t[r]:e[r]=t[r];if(arguments.length>2&&(e.children=arguments.length>3?n.call(arguments,2):u),"function"==typeof l&&null!=l.defaultProps)for(r in l.defaultProps)void 0===e[r]&&(e[r]=l.defaultProps[r]);return m(l,e,i,o,null)}function m(n,u,i,o,r){var e={type:n,props:u,key:i,ref:o,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:null==r?++t:r,__i:-1,__u:0};return null==r&&null!=l.vnode&&l.vnode(e),e}function b(){return{current:null}}function k(n){return n.children}function x(n,l){this.props=n,this.context=l}function C(n,l){if(null==l)return n.__?C(n.__,n.__i+1):null;for(var t;lt&&i.sort(e));P.__r=0}function $(n,l,t,u,i,o,r,e,f,c,s){var a,h,y,d,w,_,g=u&&u.__k||v,m=l.length;for(f=I(t,l,g,f),a=0;a0?m(o.type,o.props,o.key,o.ref?o.ref:null,o.__v):o).__=n,o.__b=n.__b+1,r=null,-1!==(f=o.__i=O(o,t,e,a))&&(a--,(r=t[f])&&(r.__u|=2)),null==r||null===r.__v?(-1==f&&h--,"function"!=typeof o.type&&(o.__u|=4)):f!==e&&(f==e-1?h--:f==e+1?h++:(f>e?h--:h++,o.__u|=4))):o=n.__k[i]=null;if(a)for(i=0;i(null!=f&&0==(2&f.__u)?1:0))for(;r>=0||e=0){if((f=l[r])&&0==(2&f.__u)&&i==f.key&&o===f.type)return r;r--}if(e2&&(f.children=arguments.length>3?n.call(arguments,2):u),m(l.type,f,i||l.key,o||l.ref,null)}function J(n,l){var t={__c:l="__cC"+h++,__:n,Consumer:function(n,l){return n.children(l)},Provider:function(n){var t,u;return this.getChildContext||(t=new Set,(u={})[l]=this,this.getChildContext=function(){return u},this.componentWillUnmount=function(){t=null},this.shouldComponentUpdate=function(n){this.props.value!==n.value&&t.forEach(function(n){n.__e=!0,M(n)})},this.sub=function(n){t.add(n);var l=n.componentWillUnmount;n.componentWillUnmount=function(){t&&t.delete(n),l&&l.call(n)}}),n.children}};return t.Provider.__=t.Consumer.contextType=t}n=v.slice,l={__e:function(n,l,t,u){for(var i,o,r;l=l.__;)if((i=l.__c)&&!i.__)try{if((o=i.constructor)&&null!=o.getDerivedStateFromError&&(i.setState(o.getDerivedStateFromError(n)),r=i.__d),null!=i.componentDidCatch&&(i.componentDidCatch(n,u||{}),r=i.__d),r)return i.__E=i}catch(l){n=l}throw n}},t=0,u=function(n){return null!=n&&null==n.constructor},x.prototype.setState=function(n,l){var t;t=null!=this.__s&&this.__s!==this.state?this.__s:this.__s=w({},this.state),"function"==typeof n&&(n=n(w({},t),this.props)),n&&w(t,n),null!=n&&this.__v&&(l&&this._sb.push(l),M(this))},x.prototype.forceUpdate=function(n){this.__v&&(this.__e=!0,n&&this.__h.push(n),M(this))},x.prototype.render=k,i=[],r="function"==typeof Promise?Promise.prototype.then.bind(Promise.resolve()):setTimeout,e=function(n,l){return n.__v.__b-l.__v.__b},P.__r=0,f=/(PointerCapture)$|Capture$/i,c=0,s=A(!1),a=A(!0),h=0;export{x as Component,k as Fragment,G as cloneElement,J as createContext,g as createElement,b as createRef,g as h,E as hydrate,u as isValidElement,l as options,D as render,L as toChildArray}; +//# sourceMappingURL=preact.module.js.map diff --git a/packages/main/src/Input.ts b/packages/main/src/Input.ts index 32d200a94732..3803ca09d41b 100644 --- a/packages/main/src/Input.ts +++ b/packages/main/src/Input.ts @@ -373,16 +373,6 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement @property() value = ""; - /** - * Defines the inner stored value of the component. - * - * **Note:** The property is updated upon typing. In some special cases the old value is kept (e.g. deleting the value after the dot in a float) - * @default "" - * @private - */ - @property({ noAttribute: true }) - _innerValue = ""; - /** * Defines the value state of the component. * @default "None" @@ -568,7 +558,6 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement lastConfirmedValue: string isTyping: boolean _handleResizeBound: ResizeObserverCallback; - _keepInnerValue: boolean; _shouldAutocomplete?: boolean; _keyDown?: boolean; _isKeyNavigation?: boolean; @@ -643,7 +632,6 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement this._handleResizeBound = this._handleResize.bind(this); - this._keepInnerValue = false; this._focusedAfterClear = false; } @@ -666,10 +654,6 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement } onBeforeRendering() { - if (!this._keepInnerValue) { - this._innerValue = this.value === null ? "" : this.value; - } - if (this.showSuggestions) { this.enableSuggestions(); @@ -737,9 +721,9 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement if (this._performTextSelection) { // this is required to syncronize lit-html input's value and user's input // lit-html does not sync its stored value for the value property when the user is typing - if (innerInput.value !== this._innerValue) { - innerInput.value = this._innerValue; - } + // if (innerInput.value !== this._innerValue) { + // innerInput.value = this._innerValue; + // } if (this.typedInValue.length && this.value.length) { innerInput.setSelectionRange(this.typedInValue.length, this.value.length); @@ -965,7 +949,6 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement return; } - this._keepInnerValue = false; this.focused = false; // invalidating property this._isChangeTriggeredBySuggestion = false; if (this.showClearIcon && !this._effectiveShowClearIcon) { @@ -1074,9 +1057,6 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement _input(e: CustomEvent | InputEvent, eventType: string) { const inputDomRef = this.getInputDOMRefSync(); - const emptyValueFiredOnNumberInput = this.value && this.isTypeNumber && !inputDomRef!.value; - - this._keepInnerValue = false; const allowedEventTypes = [ "deleteWordBackward", @@ -1096,41 +1076,6 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement this._shouldAutocomplete = !allowedEventTypes.includes(eventType) && !this.noTypeahead; - if (e instanceof InputEvent) { - // ---- Special cases of numeric Input ---- - // ---------------- Start ----------------- - - // When the last character after the delimiter is removed. - // In such cases, we want to skip the re-rendering of the - // component as this leads to cursor repositioning and causes user experience issues. - - // There are few scenarios: - // Example: type "123.4" and press BACKSPACE - the native input is firing event with the whole part as value (123). - // Pressing BACKSPACE again will remove the delimiter and the native input will fire event with the whole part as value again (123). - // Example: type "123.456", select/mark "456" and press BACKSPACE - the native input is firing event with the whole part as value (123). - // Example: type "123.456", select/mark "123.456" and press BACKSPACE - the native input is firing event with empty value. - const delimiterCase = this.isTypeNumber - && (e.inputType === "deleteContentForward" || e.inputType === "deleteContentBackward") - && !(e.target as HTMLInputElement).value.includes(".") - && this.value.includes("."); - - // Handle special numeric notation with "e", example "12.5e12" - const eNotationCase = emptyValueFiredOnNumberInput && e.data === "e"; - - // Handle special numeric notation with "-", example "-3" - // When pressing BACKSPACE, the native input fires event with empty value - const minusRemovalCase = emptyValueFiredOnNumberInput - && this.value.startsWith("-") - && this.value.length === 2 - && (e.inputType === "deleteContentForward" || e.inputType === "deleteContentBackward"); - - if (delimiterCase || eNotationCase || minusRemovalCase) { - this.value = (e.target as HTMLInputElement).value; - this._keepInnerValue = true; - } - // ----------------- End ------------------ - } - if (e.target === inputDomRef) { this.focused = true; @@ -1177,7 +1122,6 @@ class Input extends UI5Element implements SuggestionComponent, IFormInputElement _handleTypeAhead(item: IInputSuggestionItemSelectable) { const value = item.text ? item.text : ""; - this._innerValue = value; this.value = value; this._performTextSelection = true; diff --git a/packages/main/src/InputTemplate.tsx b/packages/main/src/InputTemplate.tsx index 65d96b0a7b85..a6ca8a5f718e 100644 --- a/packages/main/src/InputTemplate.tsx +++ b/packages/main/src/InputTemplate.tsx @@ -32,7 +32,7 @@ export default function InputTemplate(this: Input, hooks?: { preContent: Templat inner-input-with-icon={this.icon.length} disabled={this.disabled} readonly={this._readonly} - value={this._innerValue} + value={this.value} placeholder={this._placeholder} maxlength={this.maxlength} role={this.accInfo.role} diff --git a/packages/main/test/pages/Input_old_value.html b/packages/main/test/pages/Input_old_value.html new file mode 100644 index 000000000000..a8cdf70c2b1a --- /dev/null +++ b/packages/main/test/pages/Input_old_value.html @@ -0,0 +1,42 @@ + + + + + + + ui5-input + + + + + + + + + + + + + + + + \ No newline at end of file From e573c4dca508d2e8b432d88f4747e8c64ee6dc11 Mon Sep 17 00:00:00 2001 From: Peter Skelin Date: Mon, 24 Feb 2025 11:25:39 +0200 Subject: [PATCH 2/6] chore: fix color picker --- packages/main/src/ColorPicker.ts | 16 ++++++++++++++++ packages/main/src/ColorPickerTemplate.tsx | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/main/src/ColorPicker.ts b/packages/main/src/ColorPicker.ts index 99e9e4c2d596..2c10f141fca4 100644 --- a/packages/main/src/ColorPicker.ts +++ b/packages/main/src/ColorPicker.ts @@ -156,6 +156,13 @@ class ColorPicker extends UI5Element implements IFormInputElement { @property({ type: Number }) _alpha = 1; + /** + * this is the alpha value in the input only while editing, since it can container invalid/empty values temporarily + * @private + */ + @property() + _alphaTemp?: string; + /** * @private */ @@ -300,6 +307,7 @@ class ColorPicker extends UI5Element implements IFormInputElement { _handleAlphaInput(e: CustomEvent) { const aphaInputValue: string = (e.target as Input).value; + this._alphaTemp = aphaInputValue; this._alpha = parseFloat(aphaInputValue); if (Number.isNaN(this._alpha)) { this._alpha = 1; @@ -432,6 +440,14 @@ class ColorPicker extends UI5Element implements IFormInputElement { } _handleAlphaChange() { + // parse the input value if valid or fallback to default + this._alpha = this._alphaTemp ? parseFloat(this._alphaTemp) : 1; + if (Number.isNaN(this._alpha)) { + this._alpha = 1; + } + // reset input value so _alpha is rendered + this._alphaTemp = undefined; + // normalize range this._alpha = this._alpha < 0 ? 0 : this._alpha; this._alpha = this._alpha > 1 ? 1 : this._alpha; diff --git a/packages/main/src/ColorPickerTemplate.tsx b/packages/main/src/ColorPickerTemplate.tsx index e7f7fd2e2637..a21b95f3a7a7 100644 --- a/packages/main/src/ColorPickerTemplate.tsx +++ b/packages/main/src/ColorPickerTemplate.tsx @@ -103,7 +103,7 @@ export default function ColorPickerTemplate(this: ColorPicker) { id="alpha" disabled={this.inputsDisabled} class="ui5-color-channel-input" - value={String(this._alpha)} + value={this._alphaTemp ?? String(this._alpha)} accessibleName={this.alphaInputLabel} onChange={this._handleAlphaChange} onInput={this._handleAlphaInput} From 8dbe220e5924cb94dd6bbaec1d85f279964a0808 Mon Sep 17 00:00:00 2001 From: Peter Skelin Date: Wed, 26 Feb 2025 16:33:49 +0200 Subject: [PATCH 3/6] feat(framework): add .currentTarget to the type of event handler in TSX and UI5CustomEvent --- packages/ai/src/PromptInput.ts | 9 +++++---- packages/base/src/UI5Element.ts | 10 ++++++++-- packages/base/src/index.d.ts | 6 +++++- packages/fiori/src/NotificationListInternal.ts | 1 - packages/fiori/src/NotificationListItem.ts | 1 - packages/main/src/ColorPicker.ts | 5 +++-- 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/ai/src/PromptInput.ts b/packages/ai/src/PromptInput.ts index 0eeb06960231..8fd0d17b0bbc 100644 --- a/packages/ai/src/PromptInput.ts +++ b/packages/ai/src/PromptInput.ts @@ -21,6 +21,7 @@ import PromptInputTemplate from "./PromptInputTemplate.js"; // Styles import PromptInputCss from "./generated/themes/PromptInput.css.js"; +import type { UI5CustomEvent } from "@ui5/webcomponents-base/dist/index.js"; /** * @class @@ -229,8 +230,8 @@ class PromptInput extends UI5Element { } } - _onInnerInput(e: CustomEvent) { - this.value = (e.target as Input).value; + _onInnerInput(e: UI5CustomEvent) { + this.value = e.currentTarget.value; this.fireDecoratorEvent("input"); } @@ -243,8 +244,8 @@ class PromptInput extends UI5Element { this.fireDecoratorEvent("submit"); } - _onTypeAhead(e: CustomEvent): void { - this.value = (e.target as Input).value; + _onTypeAhead(e: UI5CustomEvent): void { + this.value = e.currentTarget.value; } get _exceededText() { diff --git a/packages/base/src/UI5Element.ts b/packages/base/src/UI5Element.ts index bdd4b8da5862..8a3559a6d75b 100644 --- a/packages/base/src/UI5Element.ts +++ b/packages/base/src/UI5Element.ts @@ -151,7 +151,13 @@ type KebabToPascal = Capitalize>; type GlobalHTMLAttributeNames = "accesskey" | "autocapitalize" | "autofocus" | "autocomplete" | "contenteditable" | "contextmenu" | "class" | "dir" | "draggable" | "enterkeyhint" | "hidden" | "id" | "inputmode" | "lang" | "nonce" | "part" | "exportparts" | "pattern" | "slot" | "spellcheck" | "style" | "tabIndex" | "tabindex" | "title" | "translate" | "ref" | "inert"; type ElementProps = Partial>; -type Convert = { [Property in keyof T as `on${KebabToPascal}` ]: IsAny) => void> } +type TargetedCustomEvent = Omit, "currentTarget"> & { currentTarget: T }; +// define as method and extract the function signature from the method to make it bivariant so that inheritance of event handlers is not checked via strictFunctionTypes +// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-6.html#strict-function-types +type TargetedEventHandler = { + asMethod(e: TargetedCustomEvent): void +}["asMethod"]; +type Convert = { [Property in keyof T as `on${KebabToPascal}` ]: IsAny> } /** * @class @@ -164,7 +170,7 @@ abstract class UI5Element extends HTMLElement { eventDetails!: NotEqual extends true ? object : { [k: string]: any }; - _jsxEvents!: Omit, keyof Convert | "onClose" | "onToggle" | "onChange" | "onSelect" | "onInput"> & Convert + _jsxEvents!: Omit, keyof Convert | "onClose" | "onToggle" | "onChange" | "onSelect" | "onInput"> & Convert _jsxProps!: Pick, GlobalHTMLAttributeNames> & ElementProps & Partial & { key?: any }; __id?: string; _suppressInvalidation: boolean; diff --git a/packages/base/src/index.d.ts b/packages/base/src/index.d.ts index 42bfc3104868..283ec053bae5 100644 --- a/packages/base/src/index.d.ts +++ b/packages/base/src/index.d.ts @@ -1,6 +1,10 @@ import type { JSX } from "./jsx-runtime.d.ts"; -export type UI5CustomEvent = CustomEvent; +// type b = Parameters[0]; + +type TargetedCustomEvent = Omit, "currentTarget"> & { currentTarget: T }; +// export type UI5NativeEvent = Parameters[0]; +export type UI5CustomEvent = TargetedCustomEvent; export type JsxTemplateResult = JSX.Element | void; export type JsxTemplate = () => JsxTemplateResult; diff --git a/packages/fiori/src/NotificationListInternal.ts b/packages/fiori/src/NotificationListInternal.ts index ad52edbf6f35..cce41646f0bb 100644 --- a/packages/fiori/src/NotificationListInternal.ts +++ b/packages/fiori/src/NotificationListInternal.ts @@ -42,7 +42,6 @@ class NotificationListInternal extends List { if (item instanceof NotificationListGroupItem && !item.collapsed && !item.loading) { item.items.forEach(subItem => { - // @ts-expect-error strictEvents items.push(subItem); allNavigationItems.push(subItem); }); diff --git a/packages/fiori/src/NotificationListItem.ts b/packages/fiori/src/NotificationListItem.ts index a271226a75cd..e5cb7aa7e8fb 100644 --- a/packages/fiori/src/NotificationListItem.ts +++ b/packages/fiori/src/NotificationListItem.ts @@ -546,7 +546,6 @@ class NotificationListItem extends NotificationListItemBase { // NotificationListItem will never be assigned to a variable of type ListItemBase // typescipt complains here, if that is the case, the parameter to the _press event handler could be a ListItemBase item, // but this is never the case, all components are used by their class and never assigned to a variable with a type of ListItemBase - // @ts-expect-error this.fireDecoratorEvent("_press", { item: this }); } diff --git a/packages/main/src/ColorPicker.ts b/packages/main/src/ColorPicker.ts index 99e9e4c2d596..5d54d0358456 100644 --- a/packages/main/src/ColorPicker.ts +++ b/packages/main/src/ColorPicker.ts @@ -39,6 +39,7 @@ import { // Styles import ColorPickerCss from "./generated/themes/ColorPicker.css.js"; +import type { UI5CustomEvent } from "@ui5/webcomponents-base/dist/index.js"; const PICKER_POINTER_WIDTH = 6.5; @@ -298,8 +299,8 @@ class ColorPicker extends UI5Element implements IFormInputElement { this._changeSelectedColor(e.offsetX, e.offsetY); } - _handleAlphaInput(e: CustomEvent) { - const aphaInputValue: string = (e.target as Input).value; + _handleAlphaInput(e: UI5CustomEvent | UI5CustomEvent) { + const aphaInputValue = String(e.currentTarget.value); this._alpha = parseFloat(aphaInputValue); if (Number.isNaN(this._alpha)) { this._alpha = 1; From b8aa55195e3113953377ea83decad28e845e304c Mon Sep 17 00:00:00 2001 From: Peter Skelin Date: Wed, 26 Feb 2025 16:52:54 +0200 Subject: [PATCH 4/6] chore: lint --- packages/ai/src/PromptInput.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ai/src/PromptInput.ts b/packages/ai/src/PromptInput.ts index 8fd0d17b0bbc..c269fe2f7ee6 100644 --- a/packages/ai/src/PromptInput.ts +++ b/packages/ai/src/PromptInput.ts @@ -7,7 +7,7 @@ import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; import type ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; -import type { IInputSuggestionItem, InputEventDetail } from "@ui5/webcomponents/dist/Input.js"; +import type { IInputSuggestionItem } from "@ui5/webcomponents/dist/Input.js"; import type Input from "@ui5/webcomponents/dist/Input.js"; import { isEnter, From a2cfca687cc7d6098afd82998f7d0ad6ba4938be Mon Sep 17 00:00:00 2001 From: Peter Skelin Date: Thu, 27 Feb 2025 08:55:53 +0200 Subject: [PATCH 5/6] chore: add docs --- docs/4-development/08-templates.md | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/docs/4-development/08-templates.md b/docs/4-development/08-templates.md index c8c5e7035267..64605d554f62 100644 --- a/docs/4-development/08-templates.md +++ b/docs/4-development/08-templates.md @@ -276,6 +276,52 @@ class MyCompponent { For native browser events, the most common way is to simply specify `KeyboardEvent` or `MouseEvent` +### Event handler `.currentTarget` + +When writing an inline event handler, `e.currentTarget` will be set to the element on which the handler is attached, and it will have the correct type. This removes the necessity to use type assertions which also might be wrong in case the same event handler is attached to a different element. + +```tsx +// Before + (e.target as Input))} /> + // ^^^^^^^^^^^^^^^^ + // Casting the event target to input might be wrong and is not checked + +// After + e.currentTarget)} /> + // ^^^^^^^^^^^^^ + // instance of `Input` class +``` + +The same typing information is also available via the `UI5CustomEvent` type helper + +```ts +// Before +handleInput(e: CustomEvent) { + console.log(e.target as Input); + // ^^^^^^^^^^^^^^^^^ + // this is of type Input, but TypeScript will not check in case the handler is attached to a Slider +} + +// After +handleInput(e: UI5CustomEvent) { + console.log(e.currentTarget); + // ^^^^^^^^^^^^^^^ + // this is of type Input and checked +} +``` + +Typescript will check that the `handleInput` handler can only be attached on an Input element. If the same handler is attached on a Slider, you you will have to add it in the parameters +```tsx +handleInput(e: UI5CustomEvent | UI5CustomEvent) { + console.log(e.currentTarget); + // ^^^^^^^^^^^^^^^ + // Input | Slider + console.log(e.currentTarget.value); + // ^^^^^^^^^^^^^^^^^^^^^ + // string | number +} +``` + ### Event handlers and `this` UI5 Web Components are authored as classes and event handlers are methods, they usually access the component state via `this.prop`. In order for this to work when event handlers are attached to the DOM, the framework automatically binds all event handlers to the instance that is being rendered, so accessing `this` from the event handlers works as expected without any additional work. From 90f2109b5c2e60ead918623c554aa280554b13c1 Mon Sep 17 00:00:00 2001 From: Peter Skelin Date: Tue, 4 Mar 2025 10:22:28 +0200 Subject: [PATCH 6/6] chore: fix RangeSlider --- packages/main/src/RangeSlider.ts | 19 +++++++++++++++++++ packages/main/src/RangeSliderTemplate.tsx | 10 ++++++---- packages/main/test/specs/RangeSlider.spec.js | 16 ++++++++++------ 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/packages/main/src/RangeSlider.ts b/packages/main/src/RangeSlider.ts index c1ff3ded949a..4c07e138314d 100644 --- a/packages/main/src/RangeSlider.ts +++ b/packages/main/src/RangeSlider.ts @@ -24,6 +24,7 @@ import { // Styles import rangeSliderStyles from "./generated/themes/RangeSlider.css.js"; +import type { UI5CustomEvent } from "@ui5/webcomponents-base/dist/index.js"; type AriaHandlesText = { startHandleText?: string, @@ -104,6 +105,9 @@ class RangeSlider extends SliderBase implements IFormInputElement { @property({ type: Number }) startValue = 0; + @property() + startValueTemp?: string; + /** * Defines end point of a selection - position of a second handle on the slider. * @default 100 @@ -114,6 +118,9 @@ class RangeSlider extends SliderBase implements IFormInputElement { @property({ type: Number }) endValue = 100; + @property() + endValueTemp?: string; + @property({ type: Boolean }) rangePressed = false; @@ -312,7 +319,19 @@ class RangeSlider extends SliderBase implements IFormInputElement { } } + _onStartInputInput(e: UI5CustomEvent) { + super._onInputInput(); + this.startValueTemp = e.currentTarget.value; + } + + _onEndInputInput(e: UI5CustomEvent) { + super._onInputInput(); + this.endValueTemp = e.currentTarget.value; + } + _onInputFocusOut(e: FocusEvent) { + this.startValueTemp = undefined; + this.endValueTemp = undefined; const tooltipInput = e.target as Input; const oppositeTooltipInput: Input = tooltipInput.hasAttribute("data-sap-ui-start-value") ? this.shadowRoot!.querySelector("[ui5-input][data-sap-ui-end-value]")! : this.shadowRoot!.querySelector("[ui5-input][data-sap-ui-start-value]")!; const relatedTarget = e.relatedTarget as HTMLElement; diff --git a/packages/main/src/RangeSliderTemplate.tsx b/packages/main/src/RangeSliderTemplate.tsx index f4688d385afa..8abb1ecb722c 100644 --- a/packages/main/src/RangeSliderTemplate.tsx +++ b/packages/main/src/RangeSliderTemplate.tsx @@ -73,12 +73,13 @@ export function handles(this: RangeSlider) { {this.editableTooltip ? { this.startValueTemp = this.startValue.toString(); }} onFocusOut={this._onInputFocusOut} onKeyDown={this._onInputKeydown} onChange={this._onInputChange} - onInput={this._onInputInput} + onInput={this._onStartInputInput} data-sap-ui-start-value tabIndex={-1} > @@ -115,12 +116,13 @@ export function handles(this: RangeSlider) { {this.editableTooltip ? { this.endValueTemp = this.endValue.toString(); }} onFocusOut={this._onInputFocusOut} onKeyDown={this._onInputKeydown} onChange={this._onInputChange} - onInput={this._onInputInput} + onInput={this._onEndInputInput} data-sap-ui-end-value tabIndex={-1} > diff --git a/packages/main/test/specs/RangeSlider.spec.js b/packages/main/test/specs/RangeSlider.spec.js index 751b45314fe7..70e97306137d 100644 --- a/packages/main/test/specs/RangeSlider.spec.js +++ b/packages/main/test/specs/RangeSlider.spec.js @@ -273,17 +273,17 @@ describe("Range Slider elements - tooltip, step, tickmarks, labels", () => { await browser.keys("ArrowUp"); assert.strictEqual(await rangeSlider.getProperty("startValue"), 1, "The start value is not changed on arrow up"); - + await browser.keys("ArrowDown"); assert.strictEqual(await rangeSlider.getProperty("startValue"), 1, "The start value is not changed on arrow down"); - + await rangeSlider.setProperty("endValue", 10); await rangeSliderEndHandle.click(); await rangeSliderEndTooltipInput.click(); await browser.keys("ArrowUp"); assert.strictEqual(await rangeSlider.getProperty("endValue"), 10, "The end value is not changed on arrow up"); - + await browser.keys("ArrowDown"); assert.strictEqual(await rangeSlider.getProperty("endValue"), 10, "The end value is not changed on arrow down"); }); @@ -377,7 +377,7 @@ describe("Range Slider elements - tooltip, step, tickmarks, labels", () => { assert.strictEqual(await rangeSlider.getProperty("startValue"), 1, "The end value is now start value"); assert.strictEqual(await rangeSliderStartTooltipInput.getProperty("value"), "1", "The end input value is now start value"); }); - + it("Invalid tooltip value should not be changed on 'Enter'", async () => { const rangeSlider = await browser.$("#range-slider-tickmarks-labels"); const rangeSliderTooltipInput = await rangeSlider.shadow$(".ui5-slider-tooltip--end ui5-input"); @@ -387,8 +387,12 @@ describe("Range Slider elements - tooltip, step, tickmarks, labels", () => { await rangeSliderHandle.click(); await rangeSliderTooltipInput.click(); - await rangeSliderTooltipInput.setProperty("value", "60"); - + // await rangeSliderTooltipInput.setProperty("value", "60"); + await browser.keys("ArrowRight"); + await browser.keys("ArrowRight"); + await browser.keys("Backspace"); + await browser.keys("Backspace"); + await browser.keys("60"); await browser.keys("Enter"); assert.strictEqual(await rangeSlider.getProperty("endValue"), 12, "The slider's value is not changed when invalid");