diff --git a/.changeset/unlucky-mice-ring.md b/.changeset/unlucky-mice-ring.md
new file mode 100644
index 0000000000..74543e16b5
--- /dev/null
+++ b/.changeset/unlucky-mice-ring.md
@@ -0,0 +1,10 @@
+---
+"sit-onyx": major
+---
+
+feat(OnyxStepper): update OnyxStepper API
+
+- remove property `step`, use `stepSize` instead
+- remove property `stripStep`, use `validStepSize` instead. User inputs will no longer be manipulated, instead an error will be shown
+- changed logic of `precision` property. Now determined numbers of decimal places to show. Is no longer the default value of `stepSize` property.
+- fix bug that decimal value is not displayed correctly when `precision` is not set
diff --git a/apps/demo-app/src/components/form-demo/FormDemo.vue b/apps/demo-app/src/components/form-demo/FormDemo.vue
index da8fb78c5b..6741dea560 100644
--- a/apps/demo-app/src/components/form-demo/FormDemo.vue
+++ b/apps/demo-app/src/components/form-demo/FormDemo.vue
@@ -127,7 +127,7 @@ const radioOptions: RadioButtonOption[] = [
:minlength="5"
required
/>
-
+
-
-
{
});
});
-test("should emit events", async ({ mount, makeAxeBuilder }) => {
+test("should emit events", async ({ mount }) => {
const events = {
- updateModelValue: [] as string[],
+ updateModelValue: [] as number[],
};
// ARRANGE
@@ -254,12 +254,6 @@ test("should emit events", async ({ mount, makeAxeBuilder }) => {
updateModelValue: [],
});
- // ACT
- const accessibilityScanResults = await makeAxeBuilder().analyze();
-
- // ASSERT
- expect(accessibilityScanResults.violations).toEqual([]);
-
const inputElement = component.getByLabel("Label");
// ACT
@@ -290,13 +284,10 @@ test("should have aria-label if label is hidden", async ({ mount, makeAxeBuilder
await expect(component.getByLabel("Test label")).toBeAttached();
});
-test("should increment/decrement value by one on counter button click", async ({
- mount,
- makeAxeBuilder,
-}) => {
+test("should increment/decrement value on counter button click", async ({ mount }) => {
// ARRANGE
const on = {
- "update:modelValue": (newValue) => {
+ "update:modelValue": (newValue?: number) => {
component.update({
props: {
modelValue: newValue,
@@ -310,39 +301,29 @@ test("should increment/decrement value by one on counter button click", async ({
props: {
label: "Test label",
style: "width: 12rem;",
+ stepSize: 2,
},
on,
});
const input = component.getByLabel("Test label");
- const incrementButton = component.getByLabel("Increment by 1");
- const decrementButton = component.getByLabel("Decrement by 1");
-
- // ACT
- const accessibilityScanResults = await makeAxeBuilder().analyze();
+ const incrementButton = component.getByLabel("Increment by 2");
+ const decrementButton = component.getByLabel("Decrement by 2");
// ASSERT
- expect(accessibilityScanResults.violations).toEqual([]);
- await expect(component.getByLabel("Test label")).toBeAttached();
-
- await input.click();
- await input.fill("0");
- await expect(input).toHaveValue("0");
+ await expect(input).toHaveValue("");
await incrementButton.click();
- await expect(input).toHaveValue("1");
+ await expect(input).toHaveValue("2");
await decrementButton.click();
await expect(input).toHaveValue("0");
});
-test("should increment/decrement value by step on counter button click", async ({
- mount,
- makeAxeBuilder,
-}) => {
+test("should not allow entering value over the max value that has been set", async ({ mount }) => {
// ARRANGE
const on = {
- "update:modelValue": (newValue) => {
+ "update:modelValue": (newValue?: number) => {
component.update({
props: {
modelValue: newValue,
@@ -356,40 +337,28 @@ test("should increment/decrement value by step on counter button click", async (
props: {
label: "Test label",
style: "width: 12rem;",
+ max: 3,
stepSize: 2,
},
on,
});
const input = component.getByLabel("Test label");
- const addButton = component.getByLabel("Increment");
- const substractButton = component.getByLabel("Decrement by 2");
-
- // ACT
- const accessibilityScanResults = await makeAxeBuilder().analyze();
+ const addButton = component.getByLabel("Increment by 2");
// ASSERT
- expect(accessibilityScanResults.violations).toEqual([]);
- await expect(component.getByLabel("Test label")).toBeAttached();
-
- await input.click();
- await input.fill("0");
- await expect(input).toHaveValue("0");
-
await addButton.click();
await expect(input).toHaveValue("2");
- await substractButton.click();
- await expect(input).toHaveValue("0");
+ await addButton.click();
+ await expect(input).toHaveValue("3");
+ await expect(addButton).toBeDisabled();
});
-test("should not allow entering value over the max value that has been set", async ({
- mount,
- makeAxeBuilder,
-}) => {
+test("should not allow entering value lower the min value that has been set", async ({ mount }) => {
// ARRANGE
const on = {
- "update:modelValue": (newValue) => {
+ "update:modelValue": (newValue?: number) => {
component.update({
props: {
modelValue: newValue,
@@ -403,41 +372,34 @@ test("should not allow entering value over the max value that has been set", asy
props: {
label: "Test label",
style: "width: 12rem;",
- max: 2,
+ min: 2,
+ stepSize: 2,
+ modelValue: 5,
},
on,
});
const input = component.getByLabel("Test label");
- const addButton = component.getByLabel("Increment by 1");
-
- // ACT
- const accessibilityScanResults = await makeAxeBuilder().analyze();
+ const substractButton = component.getByLabel("Decrement by 2");
// ASSERT
- expect(accessibilityScanResults.violations).toEqual([]);
- await expect(component.getByLabel("Test label")).toBeAttached();
-
- await input.click();
- await input.fill("0");
- await expect(input).toHaveValue("0");
-
- await addButton.click();
- await expect(input).toHaveValue("1");
+ await substractButton.click();
+ await expect(input).toHaveValue("3");
- await addButton.click();
+ await substractButton.click();
await expect(input).toHaveValue("2");
-
- await expect(addButton).toBeDisabled();
+ await expect(substractButton).toBeDisabled();
});
-test("should not allow entering value lower the min value that has been set", async ({
+test("Should correctly display decimal places according to the defined precision", async ({
mount,
- makeAxeBuilder,
}) => {
+ const modelValueUpdates = [] as (number | undefined)[];
+
// ARRANGE
const on = {
- "update:modelValue": (newValue) => {
+ "update:modelValue": (newValue?: number) => {
+ modelValueUpdates.push(newValue);
component.update({
props: {
modelValue: newValue,
@@ -451,77 +413,56 @@ test("should not allow entering value lower the min value that has been set", as
props: {
label: "Test label",
style: "width: 12rem;",
- min: 2,
- modelValue: 4,
+ precision: 2,
+ modelValue: 1,
},
on,
});
const input = component.getByLabel("Test label");
- const substractButton = component.getByLabel("Decrement by 1");
-
- // ACT
- const accessibilityScanResults = await makeAxeBuilder().analyze();
// ASSERT
- expect(accessibilityScanResults.violations).toEqual([]);
- await expect(component.getByLabel("Test label")).toBeAttached();
+ await expect(input).toHaveValue("1.00");
- await substractButton.click();
- await expect(input).toHaveValue("3");
+ // ACT
+ await input.fill("3.1");
+ await input.blur();
- await substractButton.click();
- await expect(input).toHaveValue("2");
+ // ASSERT
+ await expect(input).toHaveValue("3.10");
+ expect(modelValueUpdates).toStrictEqual([3.1]);
- await expect(substractButton).toBeDisabled();
-});
+ // ACT
+ await input.fill("3.106");
+ await input.blur();
-test("Should display the same number of decimal places as the smallest possible step", async ({
- mount,
- makeAxeBuilder,
-}) => {
- // ARRANGE
- const on = {
- "update:modelValue": (newValue) => {
- component.update({
- props: {
- modelValue: newValue,
- },
- on,
- });
- },
- };
+ // ASSERT
+ await expect(input).toHaveValue("3.11");
+ expect(modelValueUpdates).toStrictEqual([3.1, 3.11]);
- const component = await mount(OnyxStepper, {
- props: {
- label: "Test label",
- style: "width: 12rem;",
- precision: 0.01,
- },
- on,
- });
+ // ACT
+ await component.update({ props: { precision: 1 }, on });
- const input = component.locator("input");
+ // ASSERT
+ await expect(input).toHaveValue("3.1");
// ACT
- const accessibilityScanResults = await makeAxeBuilder().analyze();
+ await component.update({ props: { precision: -1 }, on });
+ await input.fill("6");
+ await input.blur();
// ASSERT
- expect(accessibilityScanResults.violations).toEqual([]);
-
- await input.fill("1");
- await input.dispatchEvent("change");
- await expect(input).toHaveValue("1.00");
+ await expect(input).toHaveValue("10");
+ expect(modelValueUpdates).toStrictEqual([3.1, 3.11, 10]);
});
-test("Should display an error if the value is not a multiple of the precision", async ({
+test("Should display an error if the value is not a multiple of validStepSize", async ({
page,
mount,
- makeAxeBuilder,
}) => {
// ARRANGE
const on = {
- "update:modelValue": (newValue: number) => {
+ "update:modelValue": (newValue?: number) => {
component.update({
props: {
modelValue: newValue,
@@ -536,7 +477,7 @@ test("Should display an error if the value is not a multiple of the precision",
label: "Test label",
style: "width: 12rem;",
modelValue: 1,
- precision: 0.5,
+ validStepSize: 0.5,
},
on,
});
@@ -545,56 +486,20 @@ test("Should display an error if the value is not a multiple of the precision",
const errorMessage = component.locator(".onyx-form-element__error-message");
// ACT
- const accessibilityScanResults = await makeAxeBuilder().analyze();
-
- // ASSERT
- expect(accessibilityScanResults.violations).toEqual([]);
-
await input.fill("1");
await page.keyboard.press("Enter");
+ // ASSERT
await expect(errorMessage).toBeHidden();
- await page.keyboard.press("Enter");
+ // ACT
await input.fill("3.6");
await page.keyboard.press("Enter");
+ // ASSERT
await expect(errorMessage).toBeVisible();
-});
-
-test("Should revert to the last valid input if the current input is invalid in stripStep mode", async ({
- page,
- mount,
-}) => {
- // ARRANGE
- const on = {
- "update:modelValue": (newValue: number) => {
- component.update({
- props: {
- modelValue: newValue,
- },
- on,
- });
- },
- };
-
- const component = await mount(OnyxStepper, {
- props: {
- label: "Test label",
- style: "width: 12rem;",
- precision: 0.5,
- stripStep: true,
- },
- on,
- });
-
- const input = component.locator("input");
-
- await input.fill("1");
- await page.keyboard.press("Enter");
- await expect(input).toHaveValue("1.0");
- await page.keyboard.press("Enter");
- await input.fill("1.6");
- await page.keyboard.press("Enter");
- await expect(input).toHaveValue("1.0");
+ await expect(errorMessage).toContainText("Invalid number");
+ await expect(errorMessage).toContainText(
+ "Please enter a valid number, that is a multiple of 0.5.",
+ );
});
diff --git a/packages/sit-onyx/src/components/OnyxStepper/OnyxStepper.stories.ts b/packages/sit-onyx/src/components/OnyxStepper/OnyxStepper.stories.ts
index 815d97a4e1..1584deffce 100644
--- a/packages/sit-onyx/src/components/OnyxStepper/OnyxStepper.stories.ts
+++ b/packages/sit-onyx/src/components/OnyxStepper/OnyxStepper.stories.ts
@@ -28,6 +28,17 @@ export const Default = {
},
} satisfies Story;
+/**
+ * This example shows a stepper with precision two always show two decimal places.
+ */
+export const Precision = {
+ args: {
+ label: "Currency",
+ modelValue: 5,
+ precision: 2,
+ },
+} satisfies Story;
+
/**
* This example shows the stepper with a placeholder.
*/
diff --git a/packages/sit-onyx/src/components/OnyxStepper/OnyxStepper.vue b/packages/sit-onyx/src/components/OnyxStepper/OnyxStepper.vue
index 5c2855be10..cdb11020b0 100644
--- a/packages/sit-onyx/src/components/OnyxStepper/OnyxStepper.vue
+++ b/packages/sit-onyx/src/components/OnyxStepper/OnyxStepper.vue
@@ -1,36 +1,38 @@
@@ -148,7 +122,6 @@ const decrementLabel = computed(() =>
:placeholder="props.placeholder"
:readonly="props.readonly"
:required="props.required"
- :step="props.precision"
+ :step="props.validStepSize ?? 'any'"
:title="props.hideLabel ? props.label : undefined"
@change="handleChange"
@keydown.up.prevent="handleClick('stepUp')"
diff --git a/packages/sit-onyx/src/components/OnyxStepper/types.ts b/packages/sit-onyx/src/components/OnyxStepper/types.ts
index b95a939b57..9fb9eebbb8 100644
--- a/packages/sit-onyx/src/components/OnyxStepper/types.ts
+++ b/packages/sit-onyx/src/components/OnyxStepper/types.ts
@@ -24,29 +24,25 @@ export type OnyxStepperProps = FormInjectedProps &
*/
max?: number;
/**
- * Incremental step.
+ * Defines how much the value is adjusted when clicking the +/- button.
*/
+ stepSize?: number;
/**
- * Incremental step.
- * @deprecated
- */
- step?: number; // step-mismatch + step-increment
-
- /**
- * The smallest allowed value and rounded precision
- */
- precision?: number; // step-mismatch => uses :step="props.precision" for the validation
- /**
- * The increment number
- * @default precision is the default stepSize
+ * Number of decimal places to show. Can also be negative. Value will be rounded if needed to match the specified precision.
+ *
+ * @example For `precision=2` with `modelValue=5`, "5.00" will be displayed.
+ * @example For `precision=-2` with `modelValue=60`, "100" will be displayed.
+ * @example For `precision=0`, only full numbers without decimals will be displayed.
*/
- stepSize?: number; // step-increment => number which is used for increment/decrement
-
+ precision?: number;
/**
- * Ensure no wrong number can be inputed
+ * Defines step size for valid/allowed values. Will show an error if invalid value is entered.
+ * Can be independent of the `stepSize` property.
+ *
+ * @example For `validStepSize` 0.01, only multiples of 0.01 are valid (useful for currencies)
+ * @example For `stepSize=4` `validStepSize=2`, only even numbers are valid and the user can adjust the value by 4 when clicking the +/- button.
*/
- stripStep?: boolean;
-
+ validStepSize?: number;
/**
* Specify how to provide automated assistance in filling out the input.
* Some autocomplete values might required specific browser permissions to be allowed by the user.
diff --git a/packages/sit-onyx/src/components/examples/GridPlayground/EditGridElementDialog/EditGridElementDialog.vue b/packages/sit-onyx/src/components/examples/GridPlayground/EditGridElementDialog/EditGridElementDialog.vue
index a2177cfa87..9918861b97 100644
--- a/packages/sit-onyx/src/components/examples/GridPlayground/EditGridElementDialog/EditGridElementDialog.vue
+++ b/packages/sit-onyx/src/components/examples/GridPlayground/EditGridElementDialog/EditGridElementDialog.vue
@@ -77,6 +77,7 @@ const handleCheckboxChange = (isChecked: boolean, breakpoint: OnyxBreakpoint) =>
label="Default number of columns"
placeholder="Default number of columns"
v-bind="STEPPER_VALIDATIONS"
+ :precision="0"
autofocus
required
/>
@@ -107,6 +108,7 @@ const handleCheckboxChange = (isChecked: boolean, breakpoint: OnyxBreakpoint) =>
v-model="state.breakpoints[breakpoint]"
:label="`Number of columns for breakpoint ${breakpoint}`"
v-bind="STEPPER_VALIDATIONS"
+ :precision="0"
hide-label
:disabled="!state.columnCount"
/>
diff --git a/packages/sit-onyx/src/composables/useCustomValidity.ts b/packages/sit-onyx/src/composables/useCustomValidity.ts
index 01309d410f..e3094747e4 100644
--- a/packages/sit-onyx/src/composables/useCustomValidity.ts
+++ b/packages/sit-onyx/src/composables/useCustomValidity.ts
@@ -28,7 +28,7 @@ export type UseCustomValidityOptions = {
minlength?: number;
min?: DateValue;
max?: DateValue;
- precision?: number;
+ validStepSize?: number;
} & Pick;
/**
* Component emit as defined with `const emit = defineEmits()`
@@ -205,7 +205,7 @@ export const useCustomValidity = (options: UseCustomValidityOptions) => {
maxLength: options.props.maxlength,
min: formatMinMax(locale.value, options.props.type, options.props.min),
max: formatMinMax(locale.value, options.props.type, options.props.max),
- step: options.props.precision,
+ step: options.props.validStepSize,
};
return {
diff --git a/packages/sit-onyx/src/utils/numbers.spec.ts b/packages/sit-onyx/src/utils/numbers.spec.ts
index 052f00461d..7c6a9eb318 100644
--- a/packages/sit-onyx/src/utils/numbers.spec.ts
+++ b/packages/sit-onyx/src/utils/numbers.spec.ts
@@ -1,29 +1,5 @@
import { describe, expect, it } from "vitest";
-import { applyLimits, isDivisible, roundToPrecision } from "./numbers";
-
-// Tests for isDivisible function
-describe("isDivisible", () => {
- it("returns true if number is divisible by precision", () => {
- expect(isDivisible(10, 2)).toBe(true);
- expect(isDivisible(15, 5)).toBe(true);
- expect(isDivisible(0.4, 0.2)).toBe(true);
- });
-
- it("returns false if number is not divisible by precision", () => {
- expect(isDivisible(10, 3)).toBe(false);
- expect(isDivisible(15, 4)).toBe(false);
- expect(isDivisible(0.5, 0.3)).toBe(false);
- });
-
- it("returns true if number is 0 and precision is non-zero", () => {
- expect(isDivisible(0, 1)).toBe(true);
- expect(isDivisible(0, 0.1)).toBe(true);
- });
-
- it("returns false if precision is 0 (division by zero)", () => {
- expect(isDivisible(10, 0)).toBe(false);
- });
-});
+import { applyLimits, roundToPrecision } from "./numbers";
// Tests for applyLimits function
describe("applyLimits", () => {
diff --git a/packages/sit-onyx/src/utils/numbers.ts b/packages/sit-onyx/src/utils/numbers.ts
index fdad893f00..3a7adb9a58 100644
--- a/packages/sit-onyx/src/utils/numbers.ts
+++ b/packages/sit-onyx/src/utils/numbers.ts
@@ -1,14 +1,3 @@
-/**
- * Checks if a given number is divisible by a specified precision.
- *
- * @param number - The number to check for divisibility.
- * @param precision - The precision (step size) to check divisibility against.
- * @returns `true` if `number` is divisible by `precision`, otherwise `false`.
- */
-export const isDivisible = (number: number, precision: number): boolean => {
- const quotient = number / precision;
- return quotient % 1 === 0;
-};
/**
* Applies minimum and maximum limits to a given number.
*
@@ -32,15 +21,12 @@ export const applyLimits = (
* Supports both decimal and whole-number rounding based on the precision provided.
*
* @param value - The number to round, or `undefined` to return an empty string.
- * @param precision - The number of decimal places for rounding (e.g., 0.01 for 2 decimals).
+ * @param precision - The number of decimal places for rounding (e.g., 0.01 for 2 decimals). Can also be negative.
* @returns The rounded number as a string. Returns an empty string if `value` is `undefined`.
*/
-export const roundToPrecision = (value: number | undefined, precision: number): string => {
- if (value === undefined) return "";
- if (precision > 0) {
- return value.toFixed(precision);
- }
- const places = precision;
- const factor = Math.pow(10, places);
+export const roundToPrecision = (value: number, precision: number): string => {
+ if (!value) return "";
+ if (precision >= 0) return value.toFixed(precision);
+ const factor = Math.pow(10, precision);
return (Math.round(value * factor) / factor).toString();
};