Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update Stepper API #2416

Merged
merged 13 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 7 additions & 55 deletions packages/sit-onyx/src/components/OnyxStepper/OnyxStepper.ct.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -336,53 +336,6 @@ test("should increment/decrement value by one on counter button click", async ({
await expect(input).toHaveValue("0");
});

test("should increment/decrement value by step on counter button click", async ({
mount,
makeAxeBuilder,
}) => {
// ARRANGE
const on = {
"update:modelValue": (newValue) => {
component.update({
props: {
modelValue: newValue,
},
on,
});
},
};

const component = await mount(OnyxStepper, {
props: {
label: "Test label",
style: "width: 12rem;",
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();

// 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");
});

test("should not allow entering value over the max value that has been set", async ({
mount,
makeAxeBuilder,
Expand Down Expand Up @@ -476,7 +429,7 @@ test("should not allow entering value lower the min value that has been set", as
await expect(substractButton).toBeDisabled();
});

test("Should display the same number of decimal places as the smallest possible step", async ({
test("Should correctly display decimal places according to the defined precision", async ({
mount,
makeAxeBuilder,
}) => {
Expand All @@ -496,7 +449,7 @@ test("Should display the same number of decimal places as the smallest possible
props: {
label: "Test label",
style: "width: 12rem;",
precision: 0.01,
precision: 2,
},
on,
});
Expand All @@ -514,7 +467,7 @@ test("Should display the same number of decimal places as the smallest possible
await expect(input).toHaveValue("1.00");
});

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,
Expand All @@ -536,7 +489,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,
});
Expand All @@ -554,7 +507,6 @@ test("Should display an error if the value is not a multiple of the precision",
await page.keyboard.press("Enter");

await expect(errorMessage).toBeHidden();
await page.keyboard.press("Enter");

await input.fill("3.6");
await page.keyboard.press("Enter");
Expand Down Expand Up @@ -582,7 +534,7 @@ test("Should revert to the last valid input if the current input is invalid in s
props: {
label: "Test label",
style: "width: 12rem;",
precision: 0.5,
validStepSize: 0.5,
stripStep: true,
},
on,
Expand All @@ -592,9 +544,9 @@ test("Should revert to the last valid input if the current input is invalid in s

await input.fill("1");
await page.keyboard.press("Enter");
await expect(input).toHaveValue("1.0");
await expect(input).toHaveValue("1");
await page.keyboard.press("Enter");
await input.fill("1.6");
await page.keyboard.press("Enter");
await expect(input).toHaveValue("1.0");
await expect(input).toHaveValue("1");
});
65 changes: 33 additions & 32 deletions packages/sit-onyx/src/components/OnyxStepper/OnyxStepper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import OnyxLoadingIndicator from "../OnyxLoadingIndicator/OnyxLoadingIndicator.v
import OnyxSkeleton from "../OnyxSkeleton/OnyxSkeleton.vue";
import type { OnyxStepperProps } from "./types";
const props = withDefaults(defineProps<OnyxStepperProps>(), {
precision: 1,
precision: undefined,
stepSize: 1,
validStepSize: undefined,
larsrickert marked this conversation as resolved.
Show resolved Hide resolved
stripStep: false,
readonly: false,
loading: false,
Expand Down Expand Up @@ -50,47 +52,48 @@ const modelValue = defineModel<number>();
*/
const inputValue = ref<string>();

const decimalPlaces = computed(() => {
const precision = props.precision;
const precisionStr = precision.toString();
if (precisionStr.includes(".")) {
return precisionStr.split(".")[1].length;
}
return -Math.floor(Math.log10(precision));
});

watch(
modelValue,
() => (inputValue.value = roundToPrecision(modelValue.value, decimalPlaces.value)),
() => {
if (props.precision) {
inputValue.value = roundToPrecision(modelValue.value, props.precision);
} else {
inputValue.value = modelValue.value?.toString();
}
},
{
immediate: true,
},
);

// * stepSize must be precision or bigger
// * use precision as fallback
const determinedStepSize = computed(() =>
Math.max(props.stepSize ?? props.precision, props.precision),
);

const handleClick = (direction: "stepUp" | "stepDown") => {
if (!inputRef.value) return;
wasTouched.value = true;
const currentValue = modelValue.value || 0;
const stepValue = (direction === "stepUp" ? 1 : -1) * determinedStepSize.value;
const stepValue = (direction === "stepUp" ? 1 : -1) * props.stepSize;
const newValue = currentValue + stepValue;
const roundedValue = Math.round(newValue / props.precision) * props.precision;

modelValue.value = applyLimits(roundedValue, props.min, props.max);
// correcting invalid inputs to the next valid number
const roundedValue = props.validStepSize
? Math.round((newValue - (props.min || 0)) / props.validStepSize) * props.validStepSize +
(props.min || 0)
: newValue;
const precisionAdjustedValue = props.precision
? parseFloat(roundToPrecision(roundedValue, props.precision))
: roundedValue;
modelValue.value = applyLimits(precisionAdjustedValue, props.min, props.max);
};

const handleChange = () => {
if (!inputRef.value) return;
wasTouched.value = true;
const newValue = parseFloat(inputValue.value ?? "");
const rounded = parseFloat(roundToPrecision(newValue, decimalPlaces.value));
const rounded = props.precision
? parseFloat(roundToPrecision(newValue, props.precision))
: newValue;
// reset input
inputValue.value = roundToPrecision(newValue, decimalPlaces.value);
inputValue.value = props.precision
? roundToPrecision(newValue, props.precision)
: newValue.toString();

if (!newValue || isNaN(newValue)) {
modelValue.value = undefined;
Expand All @@ -99,22 +102,20 @@ const handleChange = () => {

if (
props.stripStep &&
(!isDivisible(newValue, props.precision) ||
((props.validStepSize && !isDivisible(newValue, props.validStepSize)) ||
(props.min !== undefined && props.min > newValue) ||
(props.max !== undefined && props.max < newValue))
) {
inputValue.value = roundToPrecision(modelValue.value, decimalPlaces.value);
inputValue.value = props.precision
? roundToPrecision(modelValue.value, props.precision)
: modelValue.value?.toString();
return;
}
modelValue.value = rounded;
};

const incrementLabel = computed(() =>
t.value("stepper.increment", { stepSize: determinedStepSize.value }),
);
const decrementLabel = computed(() =>
t.value("stepper.decrement", { stepSize: determinedStepSize.value }),
);
const incrementLabel = computed(() => t.value("stepper.increment", { stepSize: props.stepSize }));
const decrementLabel = computed(() => t.value("stepper.decrement", { stepSize: props.stepSize }));
</script>

<template>
Expand Down Expand Up @@ -163,7 +164,7 @@ 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')"
Expand Down
33 changes: 24 additions & 9 deletions packages/sit-onyx/src/components/OnyxStepper/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,39 @@ export type OnyxStepperProps = FormInjectedProps &
*/
max?: number;
/**
* Incremental step.
*/
/**
* Ensure no wrong number can be inputed
ChristianBusshoff marked this conversation as resolved.
Show resolved Hide resolved
*/ /**
* Incremental step.
* @deprecated
*/
step?: number; // step-mismatch + step-increment

/**
* The smallest allowed value and rounded precision
* 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.
* @default undefined
*/
precision?: number; // step-mismatch => uses :step="props.precision" for the validation
precision?: number;

/**
* The increment number
* @default precision is the default stepSize
* Defines how much the value is adjusted when clicking the +/- button.
*
* @default 1
*/
stepSize?: number; // step-increment => number which is used for increment/decrement

stepSize?: number;
/**
* Defines step size for valid/allowed values. Can be independent of the `stepSize` property.
* Uses the `min` property as base so if defining min=3 with validStepSize=2, only odd numbers will
*
* @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.
* @example For `min=3` and `validStepSize=2`, only odd numbers will be valid
* @default undefined
MajaZarkova marked this conversation as resolved.
Show resolved Hide resolved
*/
validStepSize?: number;
/**
* Ensure no wrong number can be inputed
*/
Expand Down
4 changes: 2 additions & 2 deletions packages/sit-onyx/src/composables/useCustomValidity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export type UseCustomValidityOptions = {
minlength?: number;
min?: DateValue;
max?: DateValue;
precision?: number;
validStepSize?: number;
} & Pick<BaseSelectOption, "hideLabel" | "label">;
/**
* Component emit as defined with `const emit = defineEmits()`
Expand Down Expand Up @@ -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 {
Expand Down
Loading