@lion/field
Advanced tools
Comparing version 0.5.0 to 0.6.0
@@ -6,2 +6,13 @@ # Change Log | ||
# [0.6.0](https://github.com/ing-bank/lion/compare/@lion/field@0.5.0...@lion/field@0.6.0) (2019-11-22) | ||
### Features | ||
* **field:** order aria attributes based on nodes ([95d553e](https://github.com/ing-bank/lion/commit/95d553e23994181b827a091b724572678bea3b2f)) | ||
# [0.5.0](https://github.com/ing-bank/lion/compare/@lion/field@0.4.1...@lion/field@0.5.0) (2019-11-18) | ||
@@ -8,0 +19,0 @@ |
{ | ||
"name": "@lion/field", | ||
"version": "0.5.0", | ||
"version": "0.6.0", | ||
"description": "Fields are the most fundamental building block of the Form System", | ||
@@ -45,3 +45,3 @@ "author": "ing-bank", | ||
}, | ||
"gitHead": "da30ee3bb13d26f873d633ed960163bec9cba164" | ||
"gitHead": "3cd6c43e9a24f15b9f87c1761f71573f55f2985e" | ||
} |
import { html, css, nothing, dedupeMixin, SlotMixin } from '@lion/core'; | ||
import { FormRegisteringMixin } from './FormRegisteringMixin.js'; | ||
import { getAriaElementsInRightDomOrder } from './utils/getAriaElementsInRightDomOrder.js'; | ||
/** | ||
* Generates random unique identifier (for dom elements) | ||
* @param {string} prefix | ||
*/ | ||
function uuid(prefix) { | ||
return `${prefix}-${Math.random() | ||
.toString(36) | ||
.substr(2, 10)}`; | ||
} | ||
/** | ||
* #FormControlMixin : | ||
@@ -21,29 +32,23 @@ * | ||
/** | ||
* A list of ids that will be put on the _inputNode as a serialized string | ||
* When no light dom defined and prop set | ||
*/ | ||
_ariaDescribedby: { | ||
type: String, | ||
}, | ||
label: String, | ||
/** | ||
* A list of ids that will be put on the _inputNode as a serialized string | ||
* When no light dom defined and prop set | ||
*/ | ||
_ariaLabelledby: { | ||
helpText: { | ||
type: String, | ||
attribute: 'help-text', | ||
}, | ||
/** | ||
* When no light dom defined and prop set | ||
* Contains all elements that should end up in aria-labelledby of `._inputNode` | ||
*/ | ||
label: { | ||
type: String, | ||
}, | ||
_ariaLabelledNodes: Array, | ||
/** | ||
* When no light dom defined and prop set | ||
* Contains all elements that should end up in aria-describedby of `._inputNode` | ||
*/ | ||
helpText: { | ||
type: String, | ||
attribute: 'help-text', | ||
}, | ||
_ariaDescribedNodes: Array, | ||
}; | ||
@@ -71,8 +76,16 @@ } | ||
if (changedProps.has('_ariaLabelledby')) { | ||
this._onAriaLabelledbyChanged({ _ariaLabelledby: this._ariaLabelledby }); | ||
if (changedProps.has('_ariaLabelledNodes')) { | ||
this.__reflectAriaAttr( | ||
'aria-labelledby', | ||
this._ariaLabelledNodes, | ||
this.__reorderAriaLabelledNodes, | ||
); | ||
} | ||
if (changedProps.has('_ariaDescribedby')) { | ||
this._onAriaDescribedbyChanged({ _ariaDescribedby: this._ariaDescribedby }); | ||
if (changedProps.has('_ariaDescribedNodes')) { | ||
this.__reflectAriaAttr( | ||
'aria-describedby', | ||
this._ariaDescribedNodes, | ||
this.__reorderAriaDescribedNodes, | ||
); | ||
} | ||
@@ -107,7 +120,5 @@ | ||
super(); | ||
this._inputId = `${this.localName}-${Math.random() | ||
.toString(36) | ||
.substr(2, 10)}`; | ||
this._ariaLabelledby = ''; | ||
this._ariaDescribedby = ''; | ||
this._inputId = uuid(this.localName); | ||
this._ariaLabelledNodes = []; | ||
this._ariaDescribedNodes = []; | ||
} | ||
@@ -124,3 +135,2 @@ | ||
*/ | ||
_enhanceLightDomClasses() { | ||
@@ -140,22 +150,10 @@ if (this._inputNode) { | ||
_labelNode.setAttribute('for', this._inputId); | ||
_labelNode.id = _labelNode.id || `label-${this._inputId}`; | ||
const labelledById = ` ${_labelNode.id}`; | ||
if (this._ariaLabelledby.indexOf(labelledById) === -1) { | ||
this._ariaLabelledby += ` ${_labelNode.id}`; | ||
} | ||
this.addToAriaLabelledBy(_labelNode, { idPrefix: 'label' }); | ||
} | ||
if (_helpTextNode) { | ||
_helpTextNode.id = _helpTextNode.id || `help-text-${this._inputId}`; | ||
const describeIdHelpText = ` ${_helpTextNode.id}`; | ||
if (this._ariaDescribedby.indexOf(describeIdHelpText) === -1) { | ||
this._ariaDescribedby += ` ${_helpTextNode.id}`; | ||
} | ||
this.addToAriaDescribedBy(_helpTextNode, { idPrefix: 'help-text' }); | ||
} | ||
if (_feedbackNode) { | ||
_feedbackNode.setAttribute('aria-live', 'polite'); | ||
_feedbackNode.id = _feedbackNode.id || `feedback-${this._inputId}`; | ||
const describeIdFeedback = ` ${_feedbackNode.id}`; | ||
if (this._ariaDescribedby.indexOf(describeIdFeedback) === -1) { | ||
this._ariaDescribedby += ` ${_feedbackNode.id}`; | ||
} | ||
this.addToAriaDescribedBy(_feedbackNode, { idPrefix: 'feedback' }); | ||
} | ||
@@ -177,8 +175,7 @@ this._enhanceLightDomA11yForAdditionalSlots(); | ||
if (element) { | ||
element.id = element.id || `${additionalSlot}-${this._inputId}`; | ||
if (element.hasAttribute('data-label') === true) { | ||
this._ariaLabelledby += ` ${element.id}`; | ||
this.addToAriaLabelledBy(element, { idPrefix: additionalSlot }); | ||
} | ||
if (element.hasAttribute('data-description') === true) { | ||
this._ariaDescribedby += ` ${element.id}`; | ||
this.addToAriaDescribedBy(element, { idPrefix: additionalSlot }); | ||
} | ||
@@ -190,13 +187,2 @@ } | ||
/** | ||
* Will handle label, prefix/suffix/before/after (if they contain data-label flag attr). | ||
* Also, contents of id references that will be put in the <lion-field>._ariaLabelledby property | ||
* from an external context, will be read by a screen reader. | ||
*/ | ||
_onAriaLabelledbyChanged({ _ariaLabelledby }) { | ||
if (this._inputNode) { | ||
this._inputNode.setAttribute('aria-labelledby', _ariaLabelledby); | ||
} | ||
} | ||
/** | ||
* Will handle help text, validation feedback and character counter, | ||
@@ -207,5 +193,10 @@ * prefix/suffix/before/after (if they contain data-description flag attr). | ||
*/ | ||
_onAriaDescribedbyChanged({ _ariaDescribedby }) { | ||
__reflectAriaAttr(attrName, nodes, reorder) { | ||
if (this._inputNode) { | ||
this._inputNode.setAttribute('aria-describedby', _ariaDescribedby); | ||
if (reorder) { | ||
// eslint-disable-next-line no-param-reassign | ||
nodes = getAriaElementsInRightDomOrder(nodes); | ||
} | ||
const string = nodes.map(n => n.id).join(' '); | ||
this._inputNode.setAttribute(attrName, string); | ||
} | ||
@@ -475,32 +466,2 @@ } | ||
// aria-labelledby and aria-describedby helpers | ||
// TODO: consider extracting to generic ariaLabel helper mixin | ||
/** | ||
* Let the order of adding ids to aria element by DOM order, so that the screen reader | ||
* respects visual order when reading: | ||
* https://developers.google.com/web/fundamentals/accessibility/focus/dom-order-matters | ||
* @param {array} descriptionElements - holds references to description or label elements whose | ||
* id should be returned | ||
* @returns {array} sorted set of elements based on dom order | ||
* | ||
* TODO: make this method part of a more generic mixin or util and also use for lion-field | ||
*/ | ||
static _getAriaElementsInRightDomOrder(descriptionElements) { | ||
const putPrecedingSiblingsAndLocalParentsFirst = (a, b) => { | ||
// https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition | ||
const pos = a.compareDocumentPosition(b); | ||
if ( | ||
pos === Node.DOCUMENT_POSITION_PRECEDING || | ||
pos === Node.DOCUMENT_POSITION_CONTAINED_BY | ||
) { | ||
return 1; | ||
} | ||
return -1; | ||
}; | ||
const descriptionEls = descriptionElements.filter(el => el); // filter out null references | ||
return descriptionEls.sort(putPrecedingSiblingsAndLocalParentsFirst); | ||
} | ||
// Returns dom references to all elements that should be referred to by field(s) | ||
@@ -513,8 +474,12 @@ _getAriaDescriptionElements() { | ||
* Meant for Application Developers wanting to add to aria-labelledby attribute. | ||
* @param {string} id - should be the id of an element that contains the label for the | ||
* concerned field or fieldset, living in the same shadow root as the host element of field or | ||
* fieldset. | ||
* @param {Element} element | ||
*/ | ||
addToAriaLabel(id) { | ||
this._ariaLabelledby += ` ${id}`; | ||
addToAriaLabelledBy(element, { idPrefix, reorder } = { reorder: true }) { | ||
// eslint-disable-next-line no-param-reassign | ||
element.id = element.id || `${idPrefix}-${this._inputId}`; | ||
if (!this._ariaLabelledNodes.includes(element)) { | ||
this._ariaLabelledNodes = [...this._ariaLabelledNodes, element]; | ||
// This value will be read when we need to reflect to attr | ||
this.__reorderAriaLabelledNodes = Boolean(reorder); | ||
} | ||
} | ||
@@ -524,8 +489,12 @@ | ||
* Meant for Application Developers wanting to add to aria-describedby attribute. | ||
* @param {string} id - should be the id of an element that contains the label for the | ||
* concerned field or fieldset, living in the same shadow root as the host element of field or | ||
* fieldset. | ||
* @param {Element} element | ||
*/ | ||
addToAriaDescription(id) { | ||
this._ariaDescribedby += ` ${id}`; | ||
addToAriaDescribedBy(element, { idPrefix, reorder } = { reorder: true }) { | ||
// eslint-disable-next-line no-param-reassign | ||
element.id = element.id || `${idPrefix}-${this._inputId}`; | ||
if (!this._ariaDescribedNodes.includes(element)) { | ||
this._ariaDescribedNodes = [...this._ariaDescribedNodes, element]; | ||
// This value will be read when we need to reflect to attr | ||
this.__reorderAriaDescribedNodes = Boolean(reorder); | ||
} | ||
} | ||
@@ -532,0 +501,0 @@ |
@@ -18,3 +18,2 @@ import { | ||
const nameSuffix = ''; | ||
const tagString = 'lion-field'; | ||
@@ -201,3 +200,3 @@ const tag = unsafeStatic(tagString); | ||
describe(`A11y${nameSuffix}`, () => { | ||
describe('Accessibility', () => { | ||
it(`by setting corresponding aria-labelledby (for label) and aria-describedby (for helpText, feedback) | ||
@@ -226,5 +225,5 @@ ~~~ | ||
expect(nativeInput.getAttribute('aria-labelledby')).to.equal(` label-${el._inputId}`); | ||
expect(nativeInput.getAttribute('aria-describedby')).to.contain(` help-text-${el._inputId}`); | ||
expect(nativeInput.getAttribute('aria-describedby')).to.contain(` feedback-${el._inputId}`); | ||
expect(nativeInput.getAttribute('aria-labelledby')).to.equal(`label-${el._inputId}`); | ||
expect(nativeInput.getAttribute('aria-describedby')).to.contain(`help-text-${el._inputId}`); | ||
expect(nativeInput.getAttribute('aria-describedby')).to.contain(`feedback-${el._inputId}`); | ||
}); | ||
@@ -245,6 +244,6 @@ | ||
expect(nativeInput.getAttribute('aria-labelledby')).to.contain( | ||
` before-${el._inputId} after-${el._inputId}`, | ||
`before-${el._inputId} after-${el._inputId}`, | ||
); | ||
expect(nativeInput.getAttribute('aria-describedby')).to.contain( | ||
` prefix-${el._inputId} suffix-${el._inputId}`, | ||
`prefix-${el._inputId} suffix-${el._inputId}`, | ||
); | ||
@@ -254,4 +253,4 @@ }); | ||
// TODO: put this test on FormControlMixin test once there | ||
it(`allows to add to aria description or label via addToAriaLabel() and | ||
addToAriaDescription()`, async () => { | ||
it(`allows to add to aria description or label via addToAriaLabelledBy() and | ||
addToAriaDescribedBy()`, async () => { | ||
const wrapper = await fixture(html` | ||
@@ -277,3 +276,3 @@ <div id="wrapper"> | ||
expect(_inputNode.getAttribute('aria-labelledby')).to.contain(`label-${el._inputId}`); | ||
el.addToAriaLabel('additionalLabel'); | ||
el.addToAriaLabelledBy(wrapper.querySelector('#additionalLabel')); | ||
// Now check if ids are added to the end (not overridden) | ||
@@ -290,3 +289,3 @@ expect(_inputNode.getAttribute('aria-labelledby')).to.contain(`label-${el._inputId}`); | ||
expect(_inputNode.getAttribute('aria-describedby')).to.contain(`feedback-${el._inputId}`); | ||
el.addToAriaDescription('additionalDescription'); | ||
el.addToAriaDescribedBy(wrapper.querySelector('#additionalDescription')); | ||
// Now check if ids are added to the end (not overridden) | ||
@@ -302,3 +301,3 @@ expect(_inputNode.getAttribute('aria-describedby')).to.contain(`feedback-${el._inputId}`); | ||
describe(`Validation${nameSuffix}`, () => { | ||
describe(`Validation`, () => { | ||
beforeEach(() => { | ||
@@ -425,3 +424,3 @@ // Reset and preload validation translations | ||
describe(`Content projection${nameSuffix}`, () => { | ||
describe(`Content projection`, () => { | ||
it('renders correctly all slot elements in light DOM', async () => { | ||
@@ -428,0 +427,0 @@ const el = await fixture(html` |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
212703
37
3017