Skip to content

Commit

Permalink
fix: synchronize aria-labelledby with has-label (#2856)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomivirkki authored Oct 14, 2021
1 parent b659a72 commit 8a6f105
Show file tree
Hide file tree
Showing 13 changed files with 44 additions and 27 deletions.
2 changes: 1 addition & 1 deletion packages/checkbox/src/vaadin-checkbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class Checkbox extends SlotLabelMixin(
this.stateTarget = input;
})
);
this.addController(new AriaLabelController(this.inputElement, this._labelNode));
this.addController(new AriaLabelController(this, this.inputElement, this._labelNode));
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/combo-box/src/vaadin-combo-box.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ class ComboBox extends ComboBoxDataProviderMixin(
this.ariaTarget = input;
})
);
this.addController(new AriaLabelController(this.inputElement, this._labelNode));
this.addController(new AriaLabelController(this, this.inputElement, this._labelNode));
this._positionTarget = this.shadowRoot.querySelector('[part="input-field"]');
this._toggleElement = this.$.toggleButton;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/date-picker/src/vaadin-date-picker.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ class DatePicker extends DatePickerMixin(
this.ariaTarget = input;
})
);
this.addController(new AriaLabelController(this.inputElement, this._labelNode));
this.addController(new AriaLabelController(this, this.inputElement, this._labelNode));
this.$.overlay.positionTarget = this.shadowRoot.querySelector('[part="input-field"]');
}

Expand Down
2 changes: 1 addition & 1 deletion packages/date-picker/test/wai-aria.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe('WAI-ARIA', () => {
let datepicker, toggleButton, input, label, helper, error;

beforeEach(() => {
datepicker = fixtureSync(`<vaadin-date-picker helper-text="Week day"></vaadin-date-picker>`);
datepicker = fixtureSync(`<vaadin-date-picker helper-text="Week day" label="Date"></vaadin-date-picker>`);
toggleButton = datepicker.shadowRoot.querySelector('[part="toggle-button"]');
input = datepicker.inputElement;
label = datepicker.querySelector('[slot=label]');
Expand Down
35 changes: 20 additions & 15 deletions packages/field-base/src/aria-label-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,21 @@
* A controller to link an input element with a slotted `<label>` element.
*/
export class AriaLabelController {
constructor(input, label) {
constructor(host, input, label) {
this.input = input;
this.label = label;
this.__preventDuplicateLabelClick = this.__preventDuplicateLabelClick.bind(this);
}

hostConnected() {
const label = this.label;
const input = this.input;

if (label) {
label.addEventListener('click', this.__preventDuplicateLabelClick);

if (input) {
input.setAttribute('aria-labelledby', label.id);
label.setAttribute('for', input.id);
}
}
}

hostDisconnected() {
const label = this.label;
if (label) {
label.removeEventListener('click', this.__preventDuplicateLabelClick);
this.__setAriaLabelledBy(input, host.hasAttribute('has-label') ? label.id : null);
host.addEventListener('has-label-changed', (event) =>
this.__setAriaLabelledBy(input, event.detail.value ? label.id : null)
);
}
}
}

Expand All @@ -50,4 +41,18 @@ export class AriaLabelController {
};
this.input.addEventListener('click', inputClickHandler);
}

/**
* Sets or removes the `aria-labelledby` attribute on the input element.
* @param {HTMLElement} input
* @param {string | null | undefined} value
* @private
*/
__setAriaLabelledBy(input, value) {
if (value) {
input.setAttribute('aria-labelledby', value);
} else {
input.removeAttribute('aria-labelledby');
}
}
}
1 change: 1 addition & 0 deletions packages/field-base/src/label-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const LabelMixin = dedupingMixin(
const hasLabel = this._labelNode.children.length > 0 || this._labelNode.textContent.trim() !== '';

this.toggleAttribute('has-label', hasLabel);
this.dispatchEvent(new CustomEvent('has-label-changed', { detail: { value: hasLabel } }));
}
}
}
Expand Down
15 changes: 13 additions & 2 deletions packages/field-base/test/aria-label-controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ describe('aria-label-mixin', () => {
['input', 'textarea'].forEach((el) => {
describe(el, () => {
beforeEach(() => {
element = fixtureSync(`<aria-label-${el}-mixin-element></aria-label-${el}-mixin-element>`);
element = fixtureSync(`<aria-label-${el}-mixin-element label="label"></aria-label-${el}-mixin-element>`);
label = element.querySelector('[slot=label]');
target = document.createElement(el);
target.setAttribute('slot', el);
element.appendChild(target);
element._setInputElement(target);
element.addController(new AriaLabelController(target, label));
element.addController(new AriaLabelController(element, target, label));
});

it('should set for attribute on the label', () => {
Expand All @@ -48,6 +48,17 @@ describe('aria-label-mixin', () => {
expect(target.getAttribute('aria-labelledby')).to.equal(label.id);
});

it('should remove aria-labelledby attribute from the ' + el, () => {
element.label = '';
expect(target.hasAttribute('aria-labelledby')).to.be.false;
});

it('should restore aria-labelledby attribute on the ' + el, () => {
element.label = '';
element.label = 'label';
expect(target.getAttribute('aria-labelledby')).to.equal(label.id);
});

it('should only run click handler once on label click', () => {
const spy = sinon.spy();
element.addEventListener('click', spy);
Expand Down
2 changes: 1 addition & 1 deletion packages/number-field/src/vaadin-number-field.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ export class NumberField extends InputFieldMixin(SlotStylesMixin(ThemableMixin(E
this.ariaTarget = input;
})
);
this.addController(new AriaLabelController(this.inputElement, this._labelNode));
this.addController(new AriaLabelController(this, this.inputElement, this._labelNode));
}

/** @private */
Expand Down
2 changes: 1 addition & 1 deletion packages/radio-group/src/vaadin-radio-button.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ class RadioButton extends SlotLabelMixin(
this.ariaTarget = input;
})
);
this.addController(new AriaLabelController(this.inputElement, this._labelNode));
this.addController(new AriaLabelController(this, this.inputElement, this._labelNode));
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/text-area/src/vaadin-text-area.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export class TextArea extends InputFieldMixin(ThemableMixin(ElementMixin(Polymer
this.ariaTarget = input;
})
);
this.addController(new AriaLabelController(this.inputElement, this._labelNode));
this.addController(new AriaLabelController(this, this.inputElement, this._labelNode));
this.addEventListener('animationend', this._onAnimationEnd);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/text-field/src/vaadin-text-field.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export class TextField extends PatternMixin(InputFieldMixin(ThemableMixin(Elemen
this.ariaTarget = input;
})
);
this.addController(new AriaLabelController(this.inputElement, this._labelNode));
this.addController(new AriaLabelController(this, this.inputElement, this._labelNode));
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/time-picker/src/vaadin-time-picker.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ class TimePicker extends PatternMixin(InputControlMixin(ThemableMixin(ElementMix
this.ariaTarget = input;
})
);
this.addController(new AriaLabelController(this.inputElement, this._labelNode));
this.addController(new AriaLabelController(this, this.inputElement, this._labelNode));
this._inputContainer = this.shadowRoot.querySelector('[part~="input-field"]');
}

Expand Down
2 changes: 1 addition & 1 deletion packages/time-picker/test/aria.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe('ARIA', () => {
let timePicker, comboBox, input, label, helper, error;

beforeEach(() => {
timePicker = fixtureSync(`<vaadin-time-picker helper-text="Time slot"></vaadin-time-picker>`);
timePicker = fixtureSync(`<vaadin-time-picker helper-text="Time slot" label="Time"></vaadin-time-picker>`);
comboBox = timePicker.$.comboBox;
input = timePicker.inputElement;
label = timePicker.querySelector('[slot=label]');
Expand Down

0 comments on commit 8a6f105

Please sign in to comment.