fix: allow multi-digit values in duration inputs (#15429)

## Description

The shared duration input enforced its minimum after every keypress.
When the minimum was 10, typing `45` changed the first digit to `10`, so
the second digit produced `105`.

This change lets people finish typing before the input applies its
minimum and maximum. The input now normalizes the value when it loses
focus or when the person presses Enter.

## Type of change

- [x] Bug fix

## How has this been tested?

- Added a component test that types `4`, then `5`, and confirms that the
value stays `45`.
- Confirmed that values below 10 and above 100 are normalized when the
input loses focus.
- Ran the focused Vitest file and ESLint.
- Ran a local deterministic Playwright test through the delayed
automation form. The test typed `45`, saved the rule, confirmed
`execution_delay: 45` in the API response, confirmed `Runs after 45m` in
the list, and removed the test rule.

## Checklist

- [x] My code follows the style guidelines of this project.
- [x] I have performed a self-review of my code.
- [x] I have added tests that prove the fix works.
- [x] The focused unit and browser tests pass locally.
This commit is contained in:
Aakash Bakhle
2026-08-13 12:52:13 +05:30
committed by GitHub
parent 45878923b6
commit b24a970795
2 changed files with 85 additions and 3 deletions

View File

@@ -46,12 +46,16 @@ const transformedValue = computed({
duration.value = null;
return;
}
let minuteValue = convertToMinutes(newValue);
duration.value = Math.min(Math.max(minuteValue, props.min), props.max);
duration.value = convertToMinutes(newValue);
},
});
const normalizeDuration = () => {
if (duration.value == null) return;
duration.value = Math.min(Math.max(duration.value, props.min), props.max);
};
// when unit is changed set the nearest value to that unit
// so if the minute is set to 900, and the user changes the unit to "days"
// the transformed value will show 0, but the real value will still be 900
@@ -72,6 +76,8 @@ watch(unit, () => {
:disabled="disabled"
:placeholder="t('DURATION_INPUT.PLACEHOLDER')"
class="flex-grow w-full disabled:"
@blur="normalizeDuration"
@keydown.enter="normalizeDuration"
/>
<select
v-model="unit"

View File

@@ -0,0 +1,76 @@
import { defineComponent, ref } from 'vue';
import { mount } from '@vue/test-utils';
import DurationInput from '../DurationInput.vue';
const mountDurationInput = ({ initialValue = null } = {}) => {
const TestHost = defineComponent({
components: { DurationInput },
setup() {
const duration = ref(initialValue);
const unit = ref('minutes');
const submittedDuration = ref(null);
const handleSubmit = () => {
submittedDuration.value = duration.value;
};
return { duration, unit, submittedDuration, handleSubmit };
},
template: `
<form @submit.prevent="handleSubmit">
<DurationInput
v-model="duration"
v-model:unit="unit"
:min="10"
:max="100"
/>
<output data-testid="duration">{{ duration }}</output>
<output data-testid="submitted-duration">
{{ submittedDuration }}
</output>
</form>
`,
});
return mount(TestHost);
};
describe('DurationInput', () => {
it('allows a multi-digit value to be typed before enforcing the minimum', async () => {
const wrapper = mountDurationInput();
const input = wrapper.get('input');
await input.setValue('4');
expect(input.element.value).toBe('4');
await input.setValue(`${input.element.value}5`);
expect(input.element.value).toBe('45');
expect(wrapper.get('[data-testid="duration"]').text()).toBe('45');
});
it('normalizes values to the configured range on blur', async () => {
const wrapper = mountDurationInput();
const input = wrapper.get('input');
await input.setValue('4');
await input.trigger('blur');
expect(input.element.value).toBe('10');
await input.setValue('125');
await input.trigger('blur');
expect(input.element.value).toBe('100');
});
it('normalizes the value before Enter submits the form', async () => {
const wrapper = mountDurationInput();
const input = wrapper.get('input');
await input.setValue('125');
await input.trigger('keydown', { key: 'Enter' });
await wrapper.get('form').trigger('submit');
expect(wrapper.get('[data-testid="submitted-duration"]').text()).toBe(
'100'
);
});
});