[FIX] web, *: fix ProgressBarField behaviors

*: survey

This commit brings back the correct behavior on blur
to the progressbar field. It turns as a text value as
soon as the user has focused out of the input. It
also removes the wrong usage of the max_value option.

The behavior was wrongly adapted from the legacy
progressbar implementation. Tests have been adapted
to assert the correct behavior.

closes odoo/odoo#98902

Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
luvi
2022-09-01 11:32:32 +02:00
parent 70dbae8a01
commit 679739a63d
4 changed files with 59 additions and 29 deletions
+1 -4
View File
@@ -263,10 +263,7 @@
type="object"
name="action_survey_user_input_certified"
class="fw-bold">
<field name="success_ratio" widget="progressbar"
options="{'current_value': 'success_ratio',
'max_value': 100,
'editable': false}"/>
<field name="success_ratio" widget="progressbar"/>
<span class="text-muted" t-if="!record.certification.raw_value">Passed</span>
<span class="text-muted" t-else="">Certified</span>
</a>
@@ -3,24 +3,27 @@
import { _lt } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { useAutofocus } from "@web/core/utils/hooks";
import { useDebounced } from "@web/core/utils/timing";
import { useNumpadDecimal } from "../numpad_decimal_hook";
import { parseFloat } from "../parsers";
import { standardFieldProps } from "../standard_field_props";
const { Component, onWillUpdateProps, useState } = owl;
const { Component, onWillUpdateProps, useRef, useState } = owl;
const formatters = registry.category("formatters");
const parsers = registry.category("parsers");
export class ProgressBarField extends Component {
setup() {
useNumpadDecimal();
useAutofocus({ refName: "max-value", selectAll: true });
useAutofocus({ refName: "current-value", selectAll: true });
useAutofocus({ refName: "maxValue", selectAll: true });
useAutofocus({ refName: "currentValue", selectAll: true });
this.root = useRef("numpadDecimal");
this.state = useState({
currentValue: this.getCurrentValue(this.props),
maxValue: this.getMaxValue(this.props),
isEditing: false,
});
this.onBlurDebounced = useDebounced(this.onBlur);
onWillUpdateProps((nextProps) => {
Object.assign(this.state, {
currentValue: this.getCurrentValue(nextProps),
@@ -38,6 +41,9 @@ export class ProgressBarField extends Component {
get isMaxValueInteger() {
return this.state.maxValue % 1 === 0;
}
get isPercentage() {
return !this.props.maxValueField || !isNaN(this.props.maxValueField);
}
getCurrentValueField(p) {
return typeof p.currentValueField === "string" ? p.currentValueField : p.name;
@@ -80,7 +86,6 @@ export class ProgressBarField extends Component {
this.state.currentValue = parsedValue;
this.props.record.update({ [this.getCurrentValueField(this.props)]: parsedValue });
if (this.props.readonly) {
this.state.isEditing = false;
this.props.record.save();
}
}
@@ -99,7 +104,6 @@ export class ProgressBarField extends Component {
this.state.maxValue = parsedValue;
this.props.record.update({ [this.getMaxValueField(this.props)]: parsedValue });
if (this.props.readonly) {
this.state.isEditing = false;
this.props.record.save();
}
}
@@ -109,10 +113,14 @@ export class ProgressBarField extends Component {
this.state.isEditing = true;
}
}
// When both max and current value are editable, as one input is blurred when
// switching to the other, the state would revert to isEditing = false. We need
// to stay in edition mode if the focus is still in the field.
onBlur() {
if (this.props.readonly) {
this.state.isEditing = false;
if (this.root.el && this.root.el.contains(document.activeElement)) {
return;
}
this.state.isEditing = false;
}
onCurrentValueInput(ev) {
@@ -138,7 +146,6 @@ ProgressBarField.props = {
...standardFieldProps,
maxValueField: { type: [String, Number], optional: true },
currentValueField: { type: String, optional: true },
isPercentage: { type: Boolean, optional: true },
isEditable: { type: Boolean, optional: true },
isEditableInReadonly: { type: Boolean, optional: true },
isCurrentValueEditable: { type: Boolean, optional: true },
@@ -153,7 +160,6 @@ ProgressBarField.extractProps = ({ attrs }) => {
return {
maxValueField: attrs.options.max_value,
currentValueField: attrs.options.current_value,
isPercentage: !attrs.options.max_value,
isEditable: !attrs.options.readonly && attrs.options.editable,
isEditableInReadonly: attrs.options.editable_readonly,
isCurrentValueEditable:
@@ -3,12 +3,12 @@
<t t-name="web.ProgressBarField" owl="1">
<t t-if="!state.isEditing">
<div class="o_progressbar w-100" t-on-click="onClick">
<div class="o_progressbar w-100 d-flex align-items-center" t-on-click="onClick">
<div t-if="props.title" class="o_progressbar_title"><t t-esc="props.title"/></div>
<div class="o_progress" aria-valuemin="0" t-att-aria-valuemax="state.maxValue" t-att-aria-valuenow="state.currentValue">
<div class="o_progressbar_complete" t-att-style="'width: min(' + 100 * state.currentValue / state.maxValue + '%, 100%)'"></div>
</div>
<t t-if="props.isPercentage">
<t t-if="isPercentage">
<div class="o_progressbar_value" t-esc="formatCurrentValue(true) + '%'"/>
</t>
<t t-else="">
@@ -17,15 +17,16 @@
</div>
</t>
<t t-else="">
<div class="o_progressbar w-100 d-flex" t-ref="numpadDecimal">
<div class="o_progressbar w-100 d-flex align-items-center" t-ref="numpadDecimal">
<div t-if="props.title" class="o_progressbar_title"><t t-esc="props.title"/></div>
<div class="o_progress" aria-valuemin="0" t-att-aria-valuemax="state.maxValue" t-att-aria-valuenow="state.currentValue">
<div class="o_progressbar_complete" t-att-style="'width: min(' + 100 * state.currentValue / state.maxValue + '%, 100%)'"></div>
</div>
<t t-if="props.isPercentage">
<t t-if="isPercentage">
<div class="d-flex">
<input
class="o_input o_progressbar_value"
t-ref="currentValue"
class="o_input o_progressbar_value h-100"
type="text"
inputmode="numeric"
t-att-value="formatCurrentValue() or ''"
@@ -40,26 +41,26 @@
<t t-else="">
<input
t-if="props.isCurrentValueEditable"
t-ref="current-value"
class="o_progressbar_value o_input"
t-ref="currentValue"
class="o_progressbar_value o_input h-100"
type="text"
inputmode="numeric"
t-att-value="formatCurrentValue()"
t-on-change="onCurrentValueChange"
t-on-input="onCurrentValueInput"
t-on-blur="onBlur"
t-on-blur="onBlurDebounced"
/>
<span t-if="props.isCurrentValueEditable and props.isMaxValueEditable" t-esc="'/'" />
<input
t-if="props.isMaxValueEditable"
t-ref="max-value"
class="o_progressbar_value o_input"
t-ref="maxValue"
class="o_progressbar_value o_input h-100"
type="text"
inputmode="numeric"
t-att-value="formatMaxValue()"
t-on-change="onMaxValueChange"
t-on-input="onMaxValueInput"
t-on-blur="onBlur"
t-on-blur="onBlurDebounced"
/>
</t>
</div>
@@ -4,7 +4,12 @@ import {
makeFakeLocalizationService,
makeFakeNotificationService,
} from "@web/../tests/helpers/mock_services";
import { click, editInput, getFixture } from "@web/../tests/helpers/utils";
import {
click,
editInput,
getFixture,
nextTick
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { registry } from "@web/core/registry";
@@ -54,7 +59,6 @@ QUnit.module("Fields", (hooks) => {
QUnit.test("ProgressBarField: max_value should update", async function (assert) {
assert.expect(3);
serverData.models.partner.records = serverData.models.partner.records.slice(0, 1);
serverData.models.partner.records[0].float_field = 2;
serverData.models.partner.onchanges = {
@@ -107,7 +111,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.test(
"ProgressBarField: value should update in edit mode when typing in input",
async function (assert) {
assert.expect(5);
assert.expect(6);
serverData.models.partner.records[0].int_field = 99;
await makeView({
@@ -151,13 +155,21 @@ QUnit.module("Fields", (hooks) => {
);
await editInput(target, ".o_progressbar_value.o_input", "69");
target.querySelector(".o_progressbar_value.o_input").blur();
await nextTick();
assert.strictEqual(
target.querySelector(".o_progressbar_value").textContent,
"69%",
"New value should be different after focusing out of the field"
);
await click(target.querySelector(".o_form_button_save"));
assert.strictEqual(
target.querySelector(".o_progressbar_value").textContent,
"69%",
"New value should be different than initial after click"
"New value is still displayed after save"
);
}
);
@@ -271,7 +283,7 @@ QUnit.module("Fields", (hooks) => {
QUnit.test(
"ProgressBarField: update both max value and current value in edit mode when both options are given",
async function (assert) {
assert.expect(7);
assert.expect(10);
serverData.models.partner.records[0].int_field = 99;
await makeView({
@@ -310,9 +322,23 @@ QUnit.module("Fields", (hooks) => {
const currentVal = target.querySelectorAll(".o_progressbar_value.o_input")[0];
const maxVal = target.querySelectorAll(".o_progressbar_value.o_input")[1];
assert.strictEqual(document.activeElement, currentVal, "First input is focused");
assert.strictEqual(currentVal.value, "99", "Initial value in input is correct");
assert.strictEqual(maxVal.value, "0.44", "Initial value in input is correct");
maxVal.focus();
await nextTick();
assert.strictEqual(document.activeElement, maxVal, "Second input is focused");
maxVal.blur();
await nextTick();
assert.containsNone(
target,
".o_progressbar input",
"Inputs are no longer visible when focusing out"
);
await click(target.querySelector(".o_progress"));
await editInput(target, ".o_progressbar input:nth-of-type(1)", "2000");
await editInput(target, ".o_progressbar input:nth-of-type(2)", "69");