Skip to content

Commit

Permalink
Merge pull request #67 from vaadin/validation-api
Browse files Browse the repository at this point in the history
Validation api
  • Loading branch information
manolo committed Jul 9, 2018
2 parents 8592edd + 380d8d1 commit 9e9b629
Show file tree
Hide file tree
Showing 9 changed files with 720 additions and 133 deletions.
543 changes: 432 additions & 111 deletions analysis.json

Large diffs are not rendered by default.

27 changes: 14 additions & 13 deletions demo/radio-group-demos.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ <h3>Radio Group</h3>
<h3>Radio Group Disabled</h3>
<vaadin-demo-snippet id="radio-group-demos-radio-group-disabled">
<template preserve-content>
<vaadin-radio-group disabled>
<vaadin-radio-group label="Mark" disabled>
<vaadin-radio-button>1</vaadin-radio-button>
<vaadin-radio-button>2</vaadin-radio-button>
<vaadin-radio-button>3</vaadin-radio-button>
<vaadin-radio-button>4</vaadin-radio-button>
<vaadin-radio-button>5</vaadin-radio-button>
</vaadin-radio-group>
</template>
</vaadin-demo-snippet>
Expand All @@ -45,16 +46,16 @@ <h3>Radio Group with Value</h3>
</template>
</vaadin-demo-snippet>

<h3>Radio Group with Iron Form</h3>
<h3>Radio Group with Validation in Iron Form</h3>
<vaadin-demo-snippet id="radio-group-demos-radio-group-with-iron-form">
<template preserve-content>
<iron-form id="test-form">
<form>
<vaadin-radio-group>
<vaadin-radio-button name="radio-group-first">1</vaadin-radio-button>
<vaadin-radio-button name="radio-group-second">2</vaadin-radio-button>
<vaadin-radio-button name="radio-group-third">3</vaadin-radio-button>
<vaadin-radio-button name="radio-group-fourth">4</vaadin-radio-button>
<vaadin-radio-group label="Complexity Level" required error-message="Please select an option">
<vaadin-radio-button name="complexity-one">1</vaadin-radio-button>
<vaadin-radio-button name="complexity-two">2</vaadin-radio-button>
<vaadin-radio-button name="complexity-three">3</vaadin-radio-button>
<vaadin-radio-button name="complexity-four">4</vaadin-radio-button>
</vaadin-radio-group>
<vaadin-button id="submit-button">Submit</vaadin-button>
</form>
Expand All @@ -77,16 +78,16 @@ <h3>Radio Group with Iron Form</h3>
</template>
</vaadin-demo-snippet>

<h3>Radio Group with Iron Form with custom value</h3>
<h3>Radio Group with Custom Value Name in Iron Form</h3>
<vaadin-demo-snippet id="radio-group-demos-radio-group-with-iron-form-with-custom-value">
<template preserve-content>
<iron-form id="test-form-custom-value">
<form>
<vaadin-radio-group>
<vaadin-radio-button name="radio-group" value="first">1</vaadin-radio-button>
<vaadin-radio-button name="radio-group" value="second">2</vaadin-radio-button>
<vaadin-radio-button name="radio-group" value="third">3</vaadin-radio-button>
<vaadin-radio-button name="radio-group" value="fourth">4</vaadin-radio-button>
<vaadin-radio-group label="Complexity Level" required error-message="Please select an option">
<vaadin-radio-button name="complexity" value="1">1</vaadin-radio-button>
<vaadin-radio-button name="complexity" value="2">2</vaadin-radio-button>
<vaadin-radio-button name="complexity" value="3">3</vaadin-radio-button>
<vaadin-radio-button name="complexity" value="4">4</vaadin-radio-button>
</vaadin-radio-group>
<vaadin-button id="submit-button-custom-value">Submit</vaadin-button>
</form>
Expand Down
112 changes: 104 additions & 8 deletions src/vaadin-radio-group.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,12 @@
<slot id="slot"></slot>
</div>

<div part="error-message"
id="[[_errorId]]"
aria-live="assertive"
aria-hidden$="[[_getErrorMessageAriaHidden(invalid, errorMessage, _errorId)]]"
>[[errorMessage]]</div>
</div>

</template>

<script>
Expand Down Expand Up @@ -73,7 +77,9 @@
*
* Attribute | Description | Part name
* -----------|-------------|------------
* `disabled` | Set when the radio group and its children are disabled. | :host
* `disabled` | Set when the radio group and its children are disabled. | :host
* `readonly` | Set to a readonly text field | :host
* `invalid` | Set when the element is invalid | :host
* `has-label` | Set when the element has a label | :host
*
* See [ThemableMixin – how to apply styles for shadow parts](https://github.com/vaadin/vaadin-themable-mixin/wiki)
Expand All @@ -99,6 +105,45 @@
observer: '_disabledChanged'
},

/**
* This attribute indicates that the user cannot modify the value of the control.
*/
readonly: {
type: Boolean,
reflectToAttribute: true,
observer: '_readonlyChanged'
},

/**
* This property is set to true when the value is invalid.
*/
invalid: {
type: Boolean,
reflectToAttribute: true,
notify: true,
value: false
},

/**
* Specifies that the user must fill in a value.
*/
required: {
type: Boolean,
reflectToAttribute: true
},

/**
* Error to show when the input value is invalid.
*/
errorMessage: {
type: String,
value: ''
},

_errorId: {
type: String
},

_checkedButton: {
type: Object,
observer: '_checkedButtonChanged'
Expand Down Expand Up @@ -160,6 +205,11 @@
}

this.setAttribute('role', 'radiogroup');

this.addEventListener('focusout', e => this.validate());

const uniqueId = RadioGroupElement._uniqueId = 1 + RadioGroupElement._uniqueId || 0;
this._errorId = `${this.constructor.is}-error-${uniqueId}`;
}

get _radioButtons() {
Expand All @@ -173,17 +223,31 @@

_disabledChanged(disabled) {
this.setAttribute('aria-disabled', disabled);
this._updateDisableButtons();
}

_updateDisableButtons() {
this._radioButtons.forEach(button => {
if (this.disabled) {
button.disabled = true;
} else if (this.readonly) {
// it's not possible to set readonly to radio buttons, but we can
// unchecked ones instead.
button.disabled = button !== this._checkedButton && this.readonly;
} else {
button.disabled = false;
}
});
}

this._radioButtons.forEach(button => button.disabled = disabled);
_readonlyChanged(newV, oldV) {
(newV || oldV) && this._updateDisableButtons();
}

_addActiveListeners() {
this.addEventListener('keydown', e => {
// if e.target is vaadin-radio-group then assign to checkedRadioButton currently checked radio button
var checkedRadioButton = (e.target == this) ? this._checkedButton : e.target;
if (this.disabled) {
return;
}

// LEFT, UP - select previous radio button
if (e.keyCode === 37 || e.keyCode === 38) {
Expand Down Expand Up @@ -216,6 +280,7 @@
return this.contains(activeElement);
}


_hasEnabledButtons() {
return !this._radioButtons.every((button) => button.disabled);
}
Expand Down Expand Up @@ -250,14 +315,14 @@

_changeSelectedButton(button) {
this._checkedButton = button;

this.readonly && this._updateDisableButtons();
this._setFocusable(this._radioButtons.indexOf(button));
}

_checkedButtonChanged(checkedButton) {
this._radioButtons.forEach(button => button.checked = button === checkedButton);

this.value = checkedButton.value;
this.validate();
}

_valueChanged(newV, oldV) {
Expand All @@ -270,6 +335,30 @@
console.warn(`No <vaadin-radio-button> with value ${newV} found.`);
}
}

if (newV !== '' && newV != null) {
this.setAttribute('has-value', '');
} else {
this.removeAttribute('has-value');
}
}

/**
* Returns true if `value` is valid.
* `<iron-form>` uses this to check the validity or all its elements.
*
* @return {boolean} True if the value is valid.
*/
validate() {
return !(this.invalid = !this.checkValidity());
}

/**
* Returns true if the current input value satisfies all constraints (if any)
* @returns {boolean}
*/
checkValidity() {
return !this.required || !!this.value;
}

_setFocusable(idx) {
Expand All @@ -285,6 +374,13 @@
}
}

_getActiveErrorId(invalid, errorMessage, errorId) {
return errorMessage && invalid ? errorId : undefined;
}

_getErrorMessageAriaHidden(invalid, errorMessage, errorId) {
return (!this._getActiveErrorId(invalid, errorMessage, errorId)).toString();
}
}

customElements.define(RadioGroupElement.is, RadioGroupElement);
Expand Down
124 changes: 123 additions & 1 deletion test/vaadin-radio-group.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,23 @@

<script>
describe('vaadin-radio-group', () => {
var vaadinRadioButtonGroup, vaadinRadioButtonList,

function blur(e) {
e.dispatchEvent(new CustomEvent('focusout', {bubbles: true, composed: true}));
}

function visible(e) {
const rect = e.getBoundingClientRect();
return !!(rect.width && rect.height);
}

var vaadinRadioButtonGroup, vaadinRadioButtonList, errorElement,
buttonsListWithDisabled, groupWithDisabledButton;

beforeEach(() => {
vaadinRadioButtonGroup = fixture('default');
vaadinRadioButtonList = vaadinRadioButtonGroup.querySelectorAll('vaadin-radio-button');
errorElement = vaadinRadioButtonGroup.shadowRoot.querySelector('[part="error-message"]');

groupWithDisabledButton = fixture('with-disabled-button');
buttonsListWithDisabled = groupWithDisabledButton.querySelectorAll('vaadin-radio-button');
Expand Down Expand Up @@ -107,6 +118,29 @@
expect(vaadinRadioButtonList[2].checked).to.be.false;
});

it('should check radio button with keyboard if not disabled or readonly', () => {
vaadinRadioButtonList[1].checked = true;

MockInteractions.keyDownOn(vaadinRadioButtonGroup, 39);
expect(vaadinRadioButtonList[2].checked).to.be.true;
});

it('should not check radio button with keyboard if disabled', () => {
vaadinRadioButtonList[1].checked = true;
vaadinRadioButtonGroup.disabled = true;

MockInteractions.keyDownOn(vaadinRadioButtonGroup, 39);
expect(vaadinRadioButtonList[2].checked).to.be.false;
});

it('should not check radio button with keyboard if readonly', () => {
vaadinRadioButtonList[1].checked = true;
vaadinRadioButtonGroup.readonly = true;

MockInteractions.keyDownOn(vaadinRadioButtonGroup, 39);
expect(vaadinRadioButtonList[2].checked).to.be.false;
});

it('previous/last radio button should be focused and checked after left/up', () => {
vaadinRadioButtonList[1].checked = true;

Expand Down Expand Up @@ -215,6 +249,94 @@
expect(vaadinRadioButtonGroup.label).to.be.equal('foo');
});

it('should disable unchecked buttons when readonly', () => {
vaadinRadioButtonGroup.readonly = true;
expect(vaadinRadioButtonList[0].disabled).to.be.true;
expect(vaadinRadioButtonList[1].disabled).to.be.true;
expect(vaadinRadioButtonList[2].disabled).to.be.true;

vaadinRadioButtonGroup.value = '2';
expect(vaadinRadioButtonList[0].disabled).to.be.true;
expect(vaadinRadioButtonList[1].disabled).to.be.false;
expect(vaadinRadioButtonList[2].disabled).to.be.true;

vaadinRadioButtonGroup.readonly = false;
expect(vaadinRadioButtonList[0].disabled).to.be.false;
expect(vaadinRadioButtonList[1].disabled).to.be.false;
expect(vaadinRadioButtonList[2].disabled).to.be.false;
});

it('should not have the has-value attribute by default', () => {
expect(vaadinRadioButtonGroup.hasAttribute('has-value')).to.be.false;
});

it('should change the has-value attribute on value', () => {
vaadinRadioButtonGroup.value = '2';
expect(vaadinRadioButtonGroup.hasAttribute('has-value')).to.be.true;

vaadinRadioButtonGroup.value = '';
expect(vaadinRadioButtonGroup.hasAttribute('has-value')).to.be.false;
});

it('should pass validation when field is not required', () => {
expect(vaadinRadioButtonGroup.checkValidity()).to.be.true;
expect(vaadinRadioButtonGroup.invalid).to.be.false;
});

it('should not set invalid when field is required and user has not blurred yet', () => {
vaadinRadioButtonGroup.required = true;
expect(vaadinRadioButtonGroup.checkValidity()).to.be.false;
expect(vaadinRadioButtonGroup.invalid).to.be.false;
});

it('validate method should set invalid', () => {
vaadinRadioButtonGroup.required = true;
vaadinRadioButtonGroup.validate();
expect(vaadinRadioButtonGroup.invalid).to.be.true;
});

it('should validate after changing selected option', () => {
vaadinRadioButtonGroup.required = true;
vaadinRadioButtonGroup.validate();

vaadinRadioButtonList[1].checked = true;
expect(vaadinRadioButtonGroup.invalid).to.be.false;
});

it('should pass validation and set invalid when field is required and user blurs', () => {
vaadinRadioButtonGroup.required = true;
blur(vaadinRadioButtonGroup);
expect(vaadinRadioButtonGroup.checkValidity()).to.be.false;
expect(vaadinRadioButtonGroup.invalid).to.be.true;
});

it('error should have appropriate aria attributes and id', () => {
expect(errorElement.getAttribute('aria-live')).to.be.equal('assertive');
expect(errorElement.getAttribute('aria-hidden')).to.be.equal('true');
expect(/^vaadin-radio-group-error-\d+$/.test(errorElement.id)).to.be.true;
});

it('should remove aria-hidden when error is shown', () => {
vaadinRadioButtonGroup.errorMessage = 'Bad input!';
vaadinRadioButtonGroup.invalid = true;
expect(errorElement.getAttribute('aria-hidden')).to.be.equal('false');
});

it('should hide error message by default', () => {
vaadinRadioButtonGroup.errorMessage = 'Bad input!';
expect(visible(errorElement)).to.be.false;
});

it('should show error message on invalid', done => {
vaadinRadioButtonGroup.required = true;
vaadinRadioButtonGroup.errorMessage = 'Bad input!';
blur(vaadinRadioButtonGroup);

Polymer.Base.async(() => {
expect(visible(errorElement)).to.be.true;
done();
}, 100);
});
});
</script>
</body>
Loading

0 comments on commit 9e9b629

Please sign in to comment.