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

Validation api #67

Merged
merged 11 commits into from
Jul 9, 2018
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>