From cb8b39c3a930b7ef4a8921d7009dc8909c1f3cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20Monda?= Date: Mon, 13 Jul 2026 12:21:56 +0200 Subject: [PATCH 1/2] feat: allow direct entry of slider values Closes #2795. Click the value next to any slider to type a number directly, in addition to dragging the slider thumb. Co-authored-by: Cursor --- .../mouse-speed/mouse-speed.component.ts | 14 +- .../slider-wrapper.component.html | 16 +- .../slider-wrapper.component.scss | 14 ++ .../slider-wrapper.component.ts | 142 ++++++++++++++++-- 4 files changed, 170 insertions(+), 16 deletions(-) diff --git a/packages/uhk-web/src/app/components/device/mouse-speed/mouse-speed.component.ts b/packages/uhk-web/src/app/components/device/mouse-speed/mouse-speed.component.ts index 28e59c216c9..b1b8f5f80a7 100644 --- a/packages/uhk-web/src/app/components/device/mouse-speed/mouse-speed.component.ts +++ b/packages/uhk-web/src/app/components/device/mouse-speed/mouse-speed.component.ts @@ -56,11 +56,11 @@ export class MouseSpeedComponent implements OnInit, OnDestroy { ngOnInit(): void { this.userConfigSubscription = this.store.select(getUserConfiguration) .subscribe(config => { - this.mouseMoveInitialSpeed = config.mouseMoveInitialSpeed * MOUSE_MOVE_VALUE_MULTIPLIER || 0; - this.mouseMoveBaseSpeed = config.mouseMoveBaseSpeed * MOUSE_MOVE_VALUE_MULTIPLIER || 0; - this.mouseMoveAcceleration = config.mouseMoveAcceleration * MOUSE_MOVE_VALUE_MULTIPLIER || 0; - this.mouseMoveDeceleratedSpeed = config.mouseMoveDeceleratedSpeed * MOUSE_MOVE_VALUE_MULTIPLIER || 0; - this.mouseMoveAcceleratedSpeed = config.mouseMoveAcceleratedSpeed * MOUSE_MOVE_VALUE_MULTIPLIER || 0; + this.mouseMoveInitialSpeed = this.toDisplayMoveSpeed(config.mouseMoveInitialSpeed); + this.mouseMoveBaseSpeed = this.toDisplayMoveSpeed(config.mouseMoveBaseSpeed); + this.mouseMoveAcceleration = this.toDisplayMoveSpeed(config.mouseMoveAcceleration); + this.mouseMoveDeceleratedSpeed = this.toDisplayMoveSpeed(config.mouseMoveDeceleratedSpeed); + this.mouseMoveAcceleratedSpeed = this.toDisplayMoveSpeed(config.mouseMoveAcceleratedSpeed); this.mouseMoveAxisSkew = config.mouseMoveAxisSkew || 0; this.mouseScrollInitialSpeed = config.mouseScrollInitialSpeed || 0; @@ -101,4 +101,8 @@ export class MouseSpeedComponent implements OnInit, OnDestroy { resetToMacDefault() { this.store.dispatch(new ResetMacMouseSpeedSettingsAction()); } + + private toDisplayMoveSpeed(stored: number): number { + return Math.round(stored * MOUSE_MOVE_VALUE_MULTIPLIER) || 0; + } } diff --git a/packages/uhk-web/src/app/components/slider-wrapper/slider-wrapper.component.html b/packages/uhk-web/src/app/components/slider-wrapper/slider-wrapper.component.html index 5d917c199cd..2379b4ec908 100644 --- a/packages/uhk-web/src/app/components/slider-wrapper/slider-wrapper.component.html +++ b/packages/uhk-web/src/app/components/slider-wrapper/slider-wrapper.component.html @@ -25,6 +25,20 @@ (ngModelChange)="onSliderChange($event)">
-
{{ getFormatedValue(value) }}
+
{{ getFormatedValue(value) }}
+
diff --git a/packages/uhk-web/src/app/components/slider-wrapper/slider-wrapper.component.scss b/packages/uhk-web/src/app/components/slider-wrapper/slider-wrapper.component.scss index 034ecc8ff31..d837d2730d7 100644 --- a/packages/uhk-web/src/app/components/slider-wrapper/slider-wrapper.component.scss +++ b/packages/uhk-web/src/app/components/slider-wrapper/slider-wrapper.component.scss @@ -28,5 +28,19 @@ $side-value-width: 80px; .value-indicator { white-space: nowrap; + + &.editable { + cursor: pointer; + + &:hover { + text-decoration: underline; + } + } + } + + .value-input { + width: 100%; + padding: 0 0.2rem; + text-align: right; } } diff --git a/packages/uhk-web/src/app/components/slider-wrapper/slider-wrapper.component.ts b/packages/uhk-web/src/app/components/slider-wrapper/slider-wrapper.component.ts index 98b9385d870..9f0fb3ddb82 100644 --- a/packages/uhk-web/src/app/components/slider-wrapper/slider-wrapper.component.ts +++ b/packages/uhk-web/src/app/components/slider-wrapper/slider-wrapper.component.ts @@ -1,4 +1,13 @@ -import { Component, EventEmitter, forwardRef, Input, Output, OnDestroy, ViewChild } from '@angular/core'; +import { + Component, + ElementRef, + EventEmitter, + forwardRef, + Input, + OnDestroy, + Output, + ViewChild +} from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { NouisliderComponent } from 'ng2-nouislider'; @@ -31,6 +40,7 @@ export interface SliderProps { }) export class SliderWrapperComponent implements ControlValueAccessor, OnDestroy { @ViewChild(NouisliderComponent, { static: false }) slider: NouisliderComponent; + @ViewChild('valueInput') valueInput: ElementRef; // eslint-disable-next-line @typescript-eslint/no-explicit-any @Input() config: any = {}; @Input() label: string; @@ -50,6 +60,9 @@ export class SliderWrapperComponent implements ControlValueAccessor, OnDestroy { public value: number; disabled = false; + editingValue = false; + inputValue: string | number = ''; + private cancelPending = false; private changeObserver$: Observer; private changeDebounceTime: number = 300; @@ -62,10 +75,14 @@ export class SliderWrapperComponent implements ControlValueAccessor, OnDestroy { } } + get canEditValue(): boolean { + return !this.disabled && !this.valueFormatter; + } + writeValue(value: number): void { this.value = value === undefined || value === null ? this.min - : value; + : this.normalizeValue(value); } registerOnChange(fn: Function): void { @@ -79,18 +96,124 @@ export class SliderWrapperComponent implements ControlValueAccessor, OnDestroy { } getFormatedValue(value: number): string { + const normalized = this.normalizeValue(value); + if (this.valueFormatter) { - return this.valueFormatter(value); + return this.valueFormatter(normalized); } if (this.valueUnit) { - return value + ' ' + this.valueUnit; + return normalized + ' ' + this.valueUnit; + } + + return normalized.toString(10); + } + + startEditing(): void { + if (!this.canEditValue) { + return; } - return value.toString(10); + this.cancelPending = false; + this.inputValue = this.normalizeValue(this.value).toString(10); + this.editingValue = true; + + setTimeout(() => { + const input = this.valueInput?.nativeElement; + if (input) { + input.focus(); + input.select(); + } + }); + } + + commitInputValue(): void { + if (this.cancelPending) { + this.cancelPending = false; + this.editingValue = false; + return; + } + + if (!this.editingValue) { + return; + } + + const parsed = this.parseInputValue(this.inputValue); + this.editingValue = false; + + if (parsed === null) { + return; + } + + const clamped = this.normalizeValue(Math.min(this.max, Math.max(this.min, parsed))); + this.value = clamped; + this.propagateValueChange(clamped); + } + + cancelEditing(event?: KeyboardEvent): void { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + this.cancelPending = true; + this.editingValue = false; } onSliderChange(value: number): void { + this.propagateValueChange(value, true); + } + + htmlTooltip(): SafeHtml { + return this.sanitizer.bypassSecurityTrustHtml(this.tooltip); + } + + private parseInputValue(raw: string | number | null | undefined): number | null { + if (raw === null || raw === undefined) { + return null; + } + + if (typeof raw === 'number') { + return Number.isFinite(raw) ? raw : null; + } + + if (!raw.trim()) { + return null; + } + + let trimmed = raw.trim(); + if (this.valueUnit) { + const suffix = ' ' + this.valueUnit; + if (trimmed.endsWith(suffix)) { + trimmed = trimmed.slice(0, -suffix.length).trim(); + } + } + + const parsed = Number(trimmed); + return Number.isFinite(parsed) ? parsed : null; + } + + private getStepDecimalPlaces(): number { + if (!this.step || this.step <= 0) { + return 0; + } + + const stepString = this.step.toString(); + const decimalIndex = stepString.indexOf('.'); + if (decimalIndex === -1) { + return 0; + } + + return stepString.length - decimalIndex - 1; + } + + private normalizeValue(value: number): number { + const stepDecimals = this.getStepDecimalPlaces(); + const fractionDigits = Math.min(Math.max(stepDecimals + 2, 6), 10); + return Number(value.toFixed(fractionDigits)); + } + + private propagateValueChange(value: number, skipFirstChange = false): void { if (!this.changeObserver$) { Observable.create(observer => { this.changeObserver$ = observer; @@ -99,13 +222,12 @@ export class SliderWrapperComponent implements ControlValueAccessor, OnDestroy { distinctUntilChanged() ).subscribe(this.propagateChange); - return; // No change event on first change as the value is just being set + if (skipFirstChange) { + return; + } } - this.changeObserver$.next(value); - } - htmlTooltip(): SafeHtml { - return this.sanitizer.bypassSecurityTrustHtml(this.tooltip); + this.changeObserver$.next(value); } private propagateChange: Function = () => {}; From 707b9abce92bdbde8e5c6c28f0b550aa040cd1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiss=20R=C3=B3bert?= Date: Tue, 14 Jul 2026 11:15:59 +0200 Subject: [PATCH 2/2] fix: align --slider-value-width to be visible all values of the input when editing --- .../uhk-web/src/app/components/add-on/add-on.component.scss | 2 +- .../typing-behavior-page/typing-behavior-page.component.scss | 2 +- packages/uhk-web/src/styles/_table.scss | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/uhk-web/src/app/components/add-on/add-on.component.scss b/packages/uhk-web/src/app/components/add-on/add-on.component.scss index 68af9846535..c9dfe383a21 100644 --- a/packages/uhk-web/src/app/components/add-on/add-on.component.scss +++ b/packages/uhk-web/src/app/components/add-on/add-on.component.scss @@ -1,3 +1,3 @@ :host { - --slider-value-width: 35px; + --slider-value-width: 50px; } diff --git a/packages/uhk-web/src/app/components/device/typing-behavior-page/typing-behavior-page.component.scss b/packages/uhk-web/src/app/components/device/typing-behavior-page/typing-behavior-page.component.scss index abc196a7ddf..081a3542887 100644 --- a/packages/uhk-web/src/app/components/device/typing-behavior-page/typing-behavior-page.component.scss +++ b/packages/uhk-web/src/app/components/device/typing-behavior-page/typing-behavior-page.component.scss @@ -1,5 +1,5 @@ :host { - --slider-value-width: 50px; + --slider-value-width: 60px; --table-td-value-margin-left: 1.75em; table.settings { diff --git a/packages/uhk-web/src/styles/_table.scss b/packages/uhk-web/src/styles/_table.scss index ad0d068a6b5..6adadfd8a16 100644 --- a/packages/uhk-web/src/styles/_table.scss +++ b/packages/uhk-web/src/styles/_table.scss @@ -75,7 +75,7 @@ table.settings { } slider-wrapper { - --slider-value-width: 45px; + --slider-value-width: 55px; display: block; margin-left: -1.25rem; margin-right: 2rem;