Merge pull request #1254 from ing-bank/fix/success-show
fix(form-core): only show success after recovery from shown feedback
This commit is contained in:
commit
3791a5e13c
9 changed files with 179 additions and 21 deletions
5
.changeset/orange-walls-sniff.md
Normal file
5
.changeset/orange-walls-sniff.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@lion/form-core': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Only show success feedback if the user is recovering from a shown error/warning.
|
||||||
|
|
@ -2,6 +2,7 @@ node_modules
|
||||||
coverage/
|
coverage/
|
||||||
bundlesize/
|
bundlesize/
|
||||||
.history/
|
.history/
|
||||||
|
storybook-static/
|
||||||
*.d.ts
|
*.d.ts
|
||||||
_site-dev
|
_site-dev
|
||||||
_site
|
_site
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,19 @@ export class ResultValidator extends Validator {
|
||||||
* @param {Object} context
|
* @param {Object} context
|
||||||
* @param {Validator[]} context.regularValidationResult
|
* @param {Validator[]} context.regularValidationResult
|
||||||
* @param {Validator[]} context.prevValidationResult
|
* @param {Validator[]} context.prevValidationResult
|
||||||
|
* @param {Validator[]} context.prevShownValidationResult
|
||||||
* @param {Validator[]} [context.validators]
|
* @param {Validator[]} [context.validators]
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line no-unused-vars, class-methods-use-this
|
/* eslint-disable no-unused-vars */
|
||||||
executeOnResults({ regularValidationResult, prevValidationResult, validators }) {
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
executeOnResults({
|
||||||
|
regularValidationResult,
|
||||||
|
prevValidationResult,
|
||||||
|
prevShownValidationResult,
|
||||||
|
validators,
|
||||||
|
}) {
|
||||||
|
/* eslint-enable no-unused-vars */
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,8 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
this.__validationResult = [];
|
this.__validationResult = [];
|
||||||
/** @type {Validator[]} */
|
/** @type {Validator[]} */
|
||||||
this.__prevValidationResult = [];
|
this.__prevValidationResult = [];
|
||||||
|
/** @type {Validator[]} */
|
||||||
|
this.__prevShownValidationResult = [];
|
||||||
|
|
||||||
this.__onValidatorUpdated = this.__onValidatorUpdated.bind(this);
|
this.__onValidatorUpdated = this.__onValidatorUpdated.bind(this);
|
||||||
this._updateFeedbackComponent = this._updateFeedbackComponent.bind(this);
|
this._updateFeedbackComponent = this._updateFeedbackComponent.bind(this);
|
||||||
|
|
@ -279,7 +281,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.__storePrevResult();
|
this.__prevValidationResult = this.__validationResult;
|
||||||
if (clearCurrentResult) {
|
if (clearCurrentResult) {
|
||||||
// Clear ('invalidate') all pending and existing validation results.
|
// Clear ('invalidate') all pending and existing validation results.
|
||||||
// This is needed because we have async (pending) validators whose results
|
// This is needed because we have async (pending) validators whose results
|
||||||
|
|
@ -289,10 +291,6 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
await this.__executeValidators();
|
await this.__executeValidators();
|
||||||
}
|
}
|
||||||
|
|
||||||
__storePrevResult() {
|
|
||||||
this.__prevValidationResult = this.__validationResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc step A1-3 + B (as explained in 'validate')
|
* @desc step A1-3 + B (as explained in 'validate')
|
||||||
*/
|
*/
|
||||||
|
|
@ -402,6 +400,7 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
v.executeOnResults({
|
v.executeOnResults({
|
||||||
regularValidationResult,
|
regularValidationResult,
|
||||||
prevValidationResult: this.__prevValidationResult,
|
prevValidationResult: this.__prevValidationResult,
|
||||||
|
prevShownValidationResult: this.__prevShownValidationResult,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -582,8 +581,12 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
this.__prioritizedResult = this._prioritizeAndFilterFeedback({
|
this.__prioritizedResult = this._prioritizeAndFilterFeedback({
|
||||||
validationResult: this.__validationResult,
|
validationResult: this.__validationResult,
|
||||||
});
|
});
|
||||||
const messageMap = await this.__getFeedbackMessages(this.__prioritizedResult);
|
|
||||||
|
|
||||||
|
if (this.__prioritizedResult.length > 0) {
|
||||||
|
this.__prevShownValidationResult = this.__prioritizedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageMap = await this.__getFeedbackMessages(this.__prioritizedResult);
|
||||||
_feedbackNode.feedbackData = messageMap.length ? messageMap : [];
|
_feedbackNode.feedbackData = messageMap.length ? messageMap : [];
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -636,10 +639,15 @@ export const ValidateMixinImplementation = superclass =>
|
||||||
_updateShouldShowFeedbackFor() {
|
_updateShouldShowFeedbackFor() {
|
||||||
const ctor = /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (this
|
const ctor = /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (this
|
||||||
.constructor);
|
.constructor);
|
||||||
|
|
||||||
// Necessary typecast because types aren't smart enough to understand that we filter out undefined
|
// Necessary typecast because types aren't smart enough to understand that we filter out undefined
|
||||||
this.shouldShowFeedbackFor = /** @type {string[]} */ (ctor.validationTypes
|
const newShouldShowFeedbackFor = /** @type {string[]} */ (ctor.validationTypes
|
||||||
.map(type => (this._showFeedbackConditionFor(type) ? type : undefined))
|
.map(type => (this._showFeedbackConditionFor(type) ? type : undefined))
|
||||||
.filter(_ => !!_));
|
.filter(_ => !!_));
|
||||||
|
|
||||||
|
if (JSON.stringify(this.shouldShowFeedbackFor) !== JSON.stringify(newShouldShowFeedbackFor)) {
|
||||||
|
this.shouldShowFeedbackFor = newShouldShowFeedbackFor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,16 @@ export class DefaultSuccess extends ResultValidator {
|
||||||
* @param {Object} context
|
* @param {Object} context
|
||||||
* @param {Validator[]} context.regularValidationResult
|
* @param {Validator[]} context.regularValidationResult
|
||||||
* @param {Validator[]} context.prevValidationResult
|
* @param {Validator[]} context.prevValidationResult
|
||||||
|
* @param {Validator[]} context.prevShownValidationResult
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
executeOnResults({ regularValidationResult, prevValidationResult }) {
|
executeOnResults({ regularValidationResult, prevShownValidationResult }) {
|
||||||
const errorOrWarning = /** @param {Validator} v */ v =>
|
const errorOrWarning = /** @param {Validator} v */ v =>
|
||||||
v.type === 'error' || v.type === 'warning';
|
v.type === 'error' || v.type === 'warning';
|
||||||
const hasErrorOrWarning = !!regularValidationResult.filter(errorOrWarning).length;
|
const hasErrorOrWarning = !!regularValidationResult.filter(errorOrWarning).length;
|
||||||
const prevHadErrorOrWarning = !!prevValidationResult.filter(errorOrWarning).length;
|
const hasShownErrorOrWarning = !!prevShownValidationResult.filter(errorOrWarning).length;
|
||||||
return !hasErrorOrWarning && prevHadErrorOrWarning;
|
|
||||||
|
return !hasErrorOrWarning && hasShownErrorOrWarning;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -548,15 +548,18 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {{ regularValidationResult: Validator[], prevValidationResult: Validator[]}} param0
|
* @param {Object} context
|
||||||
|
* @param {Validator[]} context.regularValidationResult
|
||||||
|
* @param {Validator[]} context.prevShownValidationResult
|
||||||
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
executeOnResults({ regularValidationResult, prevValidationResult }) {
|
executeOnResults({ regularValidationResult, prevShownValidationResult }) {
|
||||||
const errorOrWarning = /** @param {Validator} v */ v =>
|
const errorOrWarning = /** @param {Validator} v */ v =>
|
||||||
v.type === 'error' || v.type === 'warning';
|
v.type === 'error' || v.type === 'warning';
|
||||||
const hasErrorOrWarning = !!regularValidationResult.filter(errorOrWarning).length;
|
const hasErrorOrWarning = !!regularValidationResult.filter(errorOrWarning).length;
|
||||||
const prevHadErrorOrWarning = !!prevValidationResult.filter(errorOrWarning).length;
|
const hasShownErrorOrWarning = !!prevShownValidationResult.filter(errorOrWarning).length;
|
||||||
return !hasErrorOrWarning && prevHadErrorOrWarning;
|
return !hasErrorOrWarning && hasShownErrorOrWarning;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -608,14 +611,16 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
>${lightDom}</${withSuccessTag}>
|
>${lightDom}</${withSuccessTag}>
|
||||||
`));
|
`));
|
||||||
const prevValidationResult = el.__prevValidationResult;
|
const prevValidationResult = el.__prevValidationResult;
|
||||||
|
const prevShownValidationResult = el.__prevShownValidationResult;
|
||||||
const regularValidationResult = [
|
const regularValidationResult = [
|
||||||
...el.__syncValidationResult,
|
...el.__syncValidationResult,
|
||||||
...el.__asyncValidationResult,
|
...el.__asyncValidationResult,
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(resultValidateSpy.args[0][0]).to.eql({
|
expect(resultValidateSpy.args[0][0]).to.eql({
|
||||||
prevValidationResult,
|
|
||||||
regularValidationResult,
|
regularValidationResult,
|
||||||
|
prevValidationResult,
|
||||||
|
prevShownValidationResult,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1140,7 +1145,7 @@ export function runValidateMixinSuite(customConfig) {
|
||||||
>${lightDom}</${elTag}>
|
>${lightDom}</${elTag}>
|
||||||
`));
|
`));
|
||||||
|
|
||||||
const spy = sinon.spy(el, '_updateFeedbackComponent');
|
const spy = sinon.spy(el, '_updateShouldShowFeedbackFor');
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
// for ... of is already allowed we should update eslint...
|
// for ... of is already allowed we should update eslint...
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { expect } from '@open-wc/testing';
|
import { expect } from '@open-wc/testing';
|
||||||
import { ResultValidator } from '../../src/validate/ResultValidator.js';
|
import { ResultValidator } from '../../src/validate/ResultValidator.js';
|
||||||
|
import { DefaultSuccess } from '../../src/validate/resultValidators/DefaultSuccess.js';
|
||||||
import { Required } from '../../src/validate/validators/Required.js';
|
import { Required } from '../../src/validate/validators/Required.js';
|
||||||
import { MinLength } from '../../src/validate/validators/StringValidators.js';
|
import { MinLength } from '../../src/validate/validators/StringValidators.js';
|
||||||
|
|
||||||
|
|
@ -16,17 +17,30 @@ describe('ResultValidator', () => {
|
||||||
* @param {Object} context
|
* @param {Object} context
|
||||||
* @param {Validator[]} context.regularValidationResult
|
* @param {Validator[]} context.regularValidationResult
|
||||||
* @param {Validator[]} context.prevValidationResult
|
* @param {Validator[]} context.prevValidationResult
|
||||||
|
* @param {Validator[]} context.prevShownValidationResult
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
executeOnResults({ regularValidationResult, prevValidationResult }) {
|
executeOnResults({ regularValidationResult, prevShownValidationResult }) {
|
||||||
const hasSuccess = regularValidationResult.length && !prevValidationResult.length;
|
const errorOrWarning = /** @param {Validator} v */ v =>
|
||||||
return !!hasSuccess;
|
v.type === 'error' || v.type === 'warning';
|
||||||
|
const hasErrorOrWarning = !!regularValidationResult.filter(errorOrWarning).length;
|
||||||
|
const hasShownErrorOrWarning = !!prevShownValidationResult.filter(errorOrWarning).length;
|
||||||
|
|
||||||
|
return !hasErrorOrWarning && hasShownErrorOrWarning;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
expect(
|
expect(
|
||||||
new MyResultValidator().executeOnResults({
|
new MyResultValidator().executeOnResults({
|
||||||
regularValidationResult: [new Required(), new MinLength(3)],
|
regularValidationResult: [new Required(), new MinLength(3)],
|
||||||
prevValidationResult: [],
|
prevValidationResult: [],
|
||||||
|
prevShownValidationResult: [],
|
||||||
|
}),
|
||||||
|
).to.be.false;
|
||||||
|
expect(
|
||||||
|
new MyResultValidator().executeOnResults({
|
||||||
|
regularValidationResult: [new DefaultSuccess()],
|
||||||
|
prevValidationResult: [new Required()],
|
||||||
|
prevShownValidationResult: [new Required()],
|
||||||
}),
|
}),
|
||||||
).to.be.true;
|
).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ export declare class ValidateHost {
|
||||||
__asyncValidationResult: Validator[];
|
__asyncValidationResult: Validator[];
|
||||||
__validationResult: Validator[];
|
__validationResult: Validator[];
|
||||||
__prevValidationResult: Validator[];
|
__prevValidationResult: Validator[];
|
||||||
|
__prevShownValidationResult: Validator[];
|
||||||
|
|
||||||
connectedCallback(): void;
|
connectedCallback(): void;
|
||||||
disconnectedCallback(): void;
|
disconnectedCallback(): void;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { expect, fixture, html, defineCE, unsafeStatic } from '@open-wc/testing';
|
||||||
|
import { Required, DefaultSuccess, Validator } from '@lion/form-core';
|
||||||
|
import { loadDefaultFeedbackMessages } from '@lion/validate-messages';
|
||||||
|
import { LionInput } from '@lion/input';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
describe('Form Validation Integrations', () => {
|
||||||
|
const lightDom = '';
|
||||||
|
before(() => {
|
||||||
|
loadDefaultFeedbackMessages();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DefaultSuccess', () => {
|
||||||
|
it('does not show success feedback if the user is not recovering from a shown error/warning feedback', async () => {
|
||||||
|
class WarnValidator extends Validator {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {?} [param]
|
||||||
|
* @param {Object.<string,?>} [config]
|
||||||
|
*/
|
||||||
|
constructor(param, config) {
|
||||||
|
super(param, config);
|
||||||
|
this.type = 'warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} value */
|
||||||
|
execute(value) {
|
||||||
|
let hasError = false;
|
||||||
|
if (value === 'warn') {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ValidateElementCustomTypes extends LionInput {
|
||||||
|
static get validationTypes() {
|
||||||
|
return ['error', 'warning', 'success'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const elTagString = defineCE(ValidateElementCustomTypes);
|
||||||
|
const elTag = unsafeStatic(elTagString);
|
||||||
|
const el = /** @type {ValidateElementCustomTypes} */ (await fixture(html`
|
||||||
|
<${elTag}
|
||||||
|
.validators=${[
|
||||||
|
new Required(null, { getMessage: () => 'error' }),
|
||||||
|
new WarnValidator(null, { getMessage: () => 'warning' }),
|
||||||
|
new DefaultSuccess(),
|
||||||
|
]}
|
||||||
|
>${lightDom}</${elTag}>
|
||||||
|
`));
|
||||||
|
|
||||||
|
expect(el._feedbackNode.feedbackData?.length).to.equal(0);
|
||||||
|
|
||||||
|
el.modelValue = 'w';
|
||||||
|
el.touched = true;
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(el.showsFeedbackFor).to.eql([]);
|
||||||
|
|
||||||
|
el.modelValue = 'warn';
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('warning');
|
||||||
|
|
||||||
|
el.modelValue = 'war';
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.feedbackComplete;
|
||||||
|
|
||||||
|
expect([
|
||||||
|
'Okay',
|
||||||
|
'Correct',
|
||||||
|
'Succeeded',
|
||||||
|
'Ok!',
|
||||||
|
'This is right.',
|
||||||
|
'Changed!',
|
||||||
|
'Ok, correct.',
|
||||||
|
]).to.include(
|
||||||
|
/** @type {{ message: string ;type: string; validator?: Validator | undefined;}[]} */ (el
|
||||||
|
._feedbackNode.feedbackData)[0].message,
|
||||||
|
);
|
||||||
|
|
||||||
|
el.modelValue = '';
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(el._feedbackNode.feedbackData?.[0].message).to.equal('error');
|
||||||
|
|
||||||
|
el.modelValue = 'war';
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.feedbackComplete;
|
||||||
|
|
||||||
|
expect([
|
||||||
|
'Okay',
|
||||||
|
'Correct',
|
||||||
|
'Succeeded',
|
||||||
|
'Ok!',
|
||||||
|
'This is right.',
|
||||||
|
'Changed!',
|
||||||
|
'Ok, correct.',
|
||||||
|
]).to.include(
|
||||||
|
/** @type {{ message: string ;type: string; validator?: Validator | undefined;}[]} */ (el
|
||||||
|
._feedbackNode.feedbackData)[0].message,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check that change in focused or other interaction states does not refresh the success message
|
||||||
|
// without a change in validation results
|
||||||
|
const spy = sinon.spy(el, '_updateFeedbackComponent');
|
||||||
|
el._updateShouldShowFeedbackFor();
|
||||||
|
await el.updateComplete;
|
||||||
|
await el.feedbackComplete;
|
||||||
|
expect(spy.called).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue