Skip to content

Commit

Permalink
Merge 67c1f5a into 19bc182
Browse files Browse the repository at this point in the history
  • Loading branch information
manolo committed Jun 29, 2018
2 parents 19bc182 + 67c1f5a commit 84539ab
Show file tree
Hide file tree
Showing 12 changed files with 715 additions and 120 deletions.
543 changes: 432 additions & 111 deletions analysis.json

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions demo/radio-group-demos.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ <h3>Radio Group Disabled</h3>
</template>
</vaadin-demo-snippet>

<h3>Validation</h3>
<vaadin-demo-snippet id="radio-group-demos-radio-group-validate">
<template preserve-content>
<vaadin-radio-group required error-message="Please select a valid option" label="Occupation" theme="vertical">
<vaadin-radio-button value="professional">Professional</vaadin-radio-button>
<vaadin-radio-button value="teacher">Teacher</vaadin-radio-button>
<vaadin-radio-button value="student">Student</vaadin-radio-button>
</vaadin-radio-group>
</template>
</vaadin-demo-snippet>

<h3>Radio Group with Value</h3>
<vaadin-demo-snippet id="radio-group-demos-radio-group-value">
<template preserve-content>
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-hiden 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>
5 changes: 5 additions & 0 deletions test/visual/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
<vaadin-radio-button disabled>2</vaadin-radio-button>
<vaadin-radio-button>3</vaadin-radio-button>
</vaadin-radio-group>

<vaadin-radio-group error-message='Error message' invalid>
<vaadin-radio-button checked>1</vaadin-radio-button>
<vaadin-radio-button>2</vaadin-radio-button>
</vaadin-radio-group>
</div>

</body>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions theme/lumo/vaadin-radio-group-styles.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,29 @@
:host([invalid]) [part="label"] {
color: var(--lumo-error-text-color);
}

[part="error-message"] {
margin-left: calc(var(--lumo-border-radius) / 4);
font-size: var(--lumo-font-size-xs);
line-height: var(--lumo-line-height-xs);
color: var(--lumo-error-text-color);
will-change: max-height;
transition: 0.4s max-height;
max-height: 5em;
}

/* Margin that doesn’t reserve space when there’s no error message */
[part="error-message"]:not(:empty)::before,
[part="error-message"]:not(:empty)::after {
content: "";
display: block;
height: 0.4em;
}

:host(:not([invalid])) [part="error-message"] {
max-height: 0;
overflow: hidden;
}
</style>
</template>
</dom-module>
Loading

0 comments on commit 84539ab

Please sign in to comment.