Slider: improved value fallback handling + validation (#20228)

* term example

* better localization options

* localize range

* ensure range value handling

* extract lox high from value setting

* further improvements

* Update src/Umbraco.Web.UI.Client/src/assets/lang/en.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Niels Lyngsø
2025-10-06 11:28:38 +02:00
committed by GitHub
parent 16c0de803b
commit 02b93e90cb
6 changed files with 172 additions and 22 deletions

View File

@@ -2228,7 +2228,8 @@ export default {
legacyOptionDescription: 'This option is no longer supported, please select something else',
numberMinimum: "Value must be greater than or equal to '%0%'.",
numberMaximum: "Value must be less than or equal to '%0%'.",
numberMisconfigured: "Minimum value '%0%'must be less than the maximum value '%1%'.",
numberMisconfigured: "Minimum value '%0%' must be less than the maximum value '%1%'.",
rangeExceeds: 'The low value must not exceed the high value.',
invalidExtensions: 'One or more of the extensions are invalid.',
allowedExtensions: 'Allowed extensions are:',
disallowedExtensions: 'Disallowed extensions are:',

View File

@@ -114,6 +114,15 @@ export class UmbLocalizationController<LocalizationSetType extends UmbLocalizati
* @param {string} key - the localization key, the indicator of what localization entry you want to retrieve.
* @param {...any} args - the arguments to parse for this localization entry.
* @returns {string} - the translated term as a string.
* @example
* Retrieving a term without any arguments:
* ```ts
* this.localize.term('area_term');
* ```
* Retrieving a term with arguments:
* ```ts
* this.localize.term('general_greeting', ['John']);
* ```
*/
term<K extends keyof LocalizationSetType>(key: K, ...args: FunctionParams<LocalizationSetType[K]>): string {
if (!this.#usedKeys.includes(key)) {

View File

@@ -93,7 +93,7 @@ export class UmbInputNumberRangeElement extends UmbFormControlMixin(UmbLitElemen
this.addValidator(
'patternMismatch',
() => {
return 'The low value must not be exceed the high value';
return '#validation_rangeExceeds';
},
() => {
return this._minValue !== undefined && this._maxValue !== undefined ? this._minValue > this._maxValue : false;

View File

@@ -1,11 +1,42 @@
import { customElement, html, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import type { UUISliderEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbFormControlMixin } from '../../validation/mixins/index.js';
function splitString(value: string | undefined): Partial<[number | undefined, number | undefined]> {
const [from, to] = (value ?? ',').split(',');
const fromNumber = makeNumberOrUndefined(from);
return [fromNumber, makeNumberOrUndefined(to, fromNumber)];
}
function makeNumberOrUndefined(value: string | undefined, fallback?: undefined | number) {
if (value === undefined) {
return fallback;
}
const n = Number(value);
if (isNaN(n)) {
return fallback;
}
return n;
}
function undefinedFallbackToString(value: number | undefined, fallback: number): string {
return (value === undefined ? fallback : value).toString();
}
@customElement('umb-input-slider')
export class UmbInputSliderElement extends UUIFormControlMixin(UmbLitElement, '') {
export class UmbInputSliderElement extends UmbFormControlMixin<string, typeof UmbLitElement, ''>(UmbLitElement, '') {
override set value(value: string) {
const [from, to] = splitString(value);
this.#valueLow = from;
this.#valueHigh = to;
super.value = value;
}
override get value() {
return super.value;
}
@property()
label: string = '';
@@ -19,10 +50,32 @@ export class UmbInputSliderElement extends UUIFormControlMixin(UmbLitElement, ''
step = 1;
@property({ type: Number })
valueLow = 0;
public get valueLow(): number | undefined {
return this.#valueLow;
}
public set valueLow(value: number | undefined) {
this.#valueLow = value;
this.#setValueFromLowHigh();
}
#valueLow?: number | undefined;
@property({ type: Number })
valueHigh = 0;
public get valueHigh(): number | undefined {
return this.#valueHigh;
}
public set valueHigh(value: number | undefined) {
this.#valueHigh = value;
this.#setValueFromLowHigh();
}
#valueHigh?: number | undefined;
#setValueFromLowHigh() {
if (this.enableRange) {
super.value = `${undefinedFallbackToString(this.valueLow, this.min)},${undefinedFallbackToString(this.valueHigh, this.max)}`;
} else {
super.value = `${undefinedFallbackToString(this.valueLow, this.min)}`;
}
}
@property({ type: Boolean, attribute: 'enable-range' })
enableRange = false;
@@ -36,6 +89,62 @@ export class UmbInputSliderElement extends UUIFormControlMixin(UmbLitElement, ''
@property({ type: Boolean, reflect: true })
readonly = false;
constructor() {
super();
this.addValidator(
'rangeUnderflow',
() => {
return this.localize.term('validation_numberMinimum', [this.min?.toString()]);
},
() => {
if (this.min !== undefined) {
const [from, to] = splitString(this.value);
if (to !== undefined && to < this.min) {
return true;
}
if (from !== undefined && from < this.min) {
return true;
}
}
return false;
},
);
this.addValidator(
'rangeOverflow',
() => {
return this.localize.term('validation_numberMaximum', [this.max?.toString()]);
},
() => {
if (this.max !== undefined) {
const [from, to] = splitString(this.value);
if (to !== undefined && to > this.max) {
return true;
}
if (from !== undefined && from > this.max) {
return true;
}
}
return false;
},
);
this.addValidator(
'patternMismatch',
() => {
return this.localize.term('validation_rangeExceeds');
},
() => {
const [from, to] = splitString(this.value);
if (to !== undefined && from !== undefined) {
return from > to;
}
return false;
},
);
}
protected override getFormElement() {
return undefined;
}
@@ -57,7 +166,7 @@ export class UmbInputSliderElement extends UUIFormControlMixin(UmbLitElement, ''
.min=${this.min}
.max=${this.max}
.step=${this.step}
.value=${this.valueLow.toString()}
.value=${undefinedFallbackToString(this.valueLow, this.min).toString()}
@change=${this.#onChange}
?readonly=${this.readonly}>
</uui-slider>
@@ -71,7 +180,10 @@ export class UmbInputSliderElement extends UUIFormControlMixin(UmbLitElement, ''
.min=${this.min}
.max=${this.max}
.step=${this.step}
.value="${this.valueLow},${this.valueHigh}"
.value="${undefinedFallbackToString(this.valueLow, this.min).toString()},${undefinedFallbackToString(
this.valueHigh,
this.max,
).toString()}"
@change=${this.#onChange}
?readonly=${this.readonly}>
</uui-range-slider>

View File

@@ -1,4 +1,4 @@
import type { UmbSliderPropertyEditorUiValue } from './types.js';
import type { UmbSliderPropertyEditorUiValue, UmbSliderPropertyEditorUiValueObject } from './types.js';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import type { UmbInputSliderElement } from '@umbraco-cms/backoffice/components';
import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit';
@@ -8,15 +8,37 @@ import type {
UmbPropertyEditorConfigCollection,
UmbPropertyEditorUiElement,
} from '@umbraco-cms/backoffice/property-editor';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
function stringToValueObject(value: string | undefined): Partial<UmbSliderPropertyEditorUiValueObject> {
const [from, to] = (value ?? ',').split(',');
const fromNumber = makeNumberOrUndefined(from);
return { from: fromNumber, to: makeNumberOrUndefined(to, fromNumber) };
}
function makeNumberOrUndefined(value: string | undefined, fallback?: undefined | number) {
if (value === undefined) {
return fallback;
}
const n = Number(value);
if (isNaN(n)) {
return fallback;
}
return n;
}
function undefinedFallback(value: number | undefined, fallback: number) {
return value === undefined ? fallback : value;
}
/**
* @element umb-property-editor-ui-slider
*/
@customElement('umb-property-editor-ui-slider')
export class UmbPropertyEditorUISliderElement extends UmbLitElement implements UmbPropertyEditorUiElement {
@property({ type: Object })
value: UmbSliderPropertyEditorUiValue | undefined;
export class UmbPropertyEditorUISliderElement
extends UmbFormControlMixin<UmbSliderPropertyEditorUiValue, typeof UmbLitElement>(UmbLitElement)
implements UmbPropertyEditorUiElement
{
/**
* Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content.
* @type {boolean}
@@ -82,6 +104,7 @@ export class UmbPropertyEditorUISliderElement extends UmbLitElement implements U
}
protected override firstUpdated() {
this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-slider')!);
if (this._min && this._max && this._min > this._max) {
console.warn(
`Property '${this._label}' (Slider) has been misconfigured, 'min' is greater than 'max'. Please correct your data type configuration.`,
@@ -95,13 +118,13 @@ export class UmbPropertyEditorUISliderElement extends UmbLitElement implements U
return Number.isNaN(num) ? undefined : num;
}
#getValueObject(value: string) {
const [from, to] = value.split(',').map(Number);
return { from, to: to ?? from };
}
#onChange(event: CustomEvent & { target: UmbInputSliderElement }) {
this.value = this.#getValueObject(event.target.value as string);
const partialValue = stringToValueObject(event.target.value as string);
const handledFrom = undefinedFallback(partialValue.from, this._initVal1);
this.value = {
from: handledFrom,
to: this._enableRange ? undefinedFallback(partialValue.to, this._initVal2) : handledFrom,
};
this.dispatchEvent(new UmbChangeEvent());
}
@@ -109,8 +132,8 @@ export class UmbPropertyEditorUISliderElement extends UmbLitElement implements U
return html`
<umb-input-slider
.label=${this._label ?? 'Slider'}
.valueLow=${this.value?.from ?? this._initVal1}
.valueHigh=${this.value?.to ?? this._initVal2}
.valueLow=${undefinedFallback(this.value?.from, this._initVal1)}
.valueHigh=${undefinedFallback(this.value?.to, this._initVal2)}
.step=${this._step}
.min=${this._min}
.max=${this._max}

View File

@@ -1,4 +1,9 @@
export type UmbSliderPropertyEditorUiValue = { from: number; to: number } | undefined;
export interface UmbSliderPropertyEditorUiValueObject {
from: number;
to: number;
}
export type UmbSliderPropertyEditorUiValue = UmbSliderPropertyEditorUiValueObject | undefined;
/**
* @deprecated this type will be removed in v.17.0, use `UmbPropertyEditorUISliderValue` instead