fix(form-core): order aria-labelledby and aria-describedby based on slot order instead of dom order

This commit is contained in:
gerjanvangeest 2024-02-06 09:21:37 +01:00 committed by Thijs Louisse
parent 5073ea4760
commit 659cbff18c
3 changed files with 53 additions and 32 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/ui': patch
---
[form-core] order aria-labelledby and aria-describedby based on slot order instead of dom order

View file

@ -390,8 +390,20 @@ const FormControlMixinImplementation = superclass =>
const insideNodes = nodes.filter(n => this.contains(n)); const insideNodes = nodes.filter(n => this.contains(n));
const outsideNodes = nodes.filter(n => !this.contains(n)); const outsideNodes = nodes.filter(n => !this.contains(n));
const insideSlots = insideNodes.map(n => n.assignedSlot || n);
const orderedInsideSlots = [...getAriaElementsInRightDomOrder(insideSlots)];
/** @type {Element[]} */
const orderedInsideNodes = [];
orderedInsideSlots.forEach(assignedNode => {
insideNodes.forEach(node => {
// @ts-ignore
if (assignedNode.name === node.slot) {
orderedInsideNodes.push(node);
}
});
});
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
nodes = [...getAriaElementsInRightDomOrder(insideNodes), ...outsideNodes]; nodes = [...orderedInsideNodes, ...outsideNodes];
} }
const string = nodes.map(n => n.id).join(' '); const string = nodes.map(n => n.id).join(' ');
this._inputNode.setAttribute(attrName, string); this._inputNode.setAttribute(attrName, string);

View file

@ -349,19 +349,18 @@ describe('FormControlMixin', () => {
); );
}); });
it('sorts internal elements, and allows opt-out', async () => { it('sorts internal elements based on assigned slots, and allows opt-out', async () => {
const wrapper = await fixture(html` const wrapper = await fixture(html`
<div id="wrapper"> <div id="wrapper">
<${tag}> <${tag}>
<input slot="input" id="myInput" /> <input slot="input" id="myInput" />
<label slot="label" id="internalLabel">Added to label by default</label> <label slot="label" id="internalLabel">Added to label by default</label>
<div slot="help-text" id="internalDescription"> <div slot="help-text" id="internalDescription">
Added to description by default Added to description by default
</div> </div>
</${tag}> </${tag}>
<div id="externalLabelB">should go after input internals</div> </div>
<div id="externalDescriptionB">should go after input internals</div> `);
</div>`);
const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString)); const el = /** @type {FormControlMixinClass} */ (wrapper.querySelector(tagString));
const { _inputNode } = getFormControlMembers(el); const { _inputNode } = getFormControlMembers(el);
@ -370,36 +369,41 @@ describe('FormControlMixin', () => {
// A real life scenario would be for instance when // A real life scenario would be for instance when
// a Field or FormGroup would be extended and an extra slot would be added in the template // a Field or FormGroup would be extended and an extra slot would be added in the template
const myInput = /** @type {HTMLElement} */ (wrapper.querySelector('#myInput')); const myInput = /** @type {HTMLElement} */ (wrapper.querySelector('#myInput'));
const internalLabel = /** @type {HTMLElement} */ (wrapper.querySelector('#internalLabel'));
const internalDescription = /** @type {HTMLElement} */ (
wrapper.querySelector('#internalDescription')
);
el.addToAriaLabelledBy(myInput); el.addToAriaLabelledBy(myInput);
await el.updateComplete; await el.updateComplete;
el.addToAriaDescribedBy(myInput); el.addToAriaDescribedBy(myInput);
await el.updateComplete; await el.updateComplete;
expect(
/** @type {string} */ (_inputNode.getAttribute('aria-labelledby')).split(' '),
).to.eql(['myInput', 'internalLabel']);
expect(
/** @type {string} */ (_inputNode.getAttribute('aria-describedby')).split(' '),
).to.eql(['myInput', 'internalDescription']);
// cleanup
el.removeFromAriaLabelledBy(myInput);
await el.updateComplete;
el.removeFromAriaDescribedBy(myInput);
await el.updateComplete;
// opt-out of reorder
el.addToAriaLabelledBy(myInput, { reorder: false });
await el.updateComplete;
el.addToAriaDescribedBy(myInput, { reorder: false });
await el.updateComplete;
expect( expect(
/** @type {string} */ (_inputNode.getAttribute('aria-labelledby')).split(' '), /** @type {string} */ (_inputNode.getAttribute('aria-labelledby')).split(' '),
).to.eql(['internalLabel', 'myInput']); ).to.eql(['internalLabel', 'myInput']);
expect( expect(
/** @type {string} */ (_inputNode.getAttribute('aria-describedby')).split(' '), /** @type {string} */ (_inputNode.getAttribute('aria-describedby')).split(' '),
).to.eql(['internalDescription', 'myInput']); ).to.eql(['internalDescription', 'myInput']);
// cleanup
el.removeFromAriaLabelledBy(internalLabel);
await el.updateComplete;
el.removeFromAriaDescribedBy(internalDescription);
await el.updateComplete;
// opt-out of reorder
el.addToAriaLabelledBy(internalLabel, { reorder: false });
await el.updateComplete;
el.addToAriaDescribedBy(internalDescription, { reorder: false });
await el.updateComplete;
expect(
/** @type {string} */ (_inputNode.getAttribute('aria-labelledby')).split(' '),
).to.eql(['myInput', 'internalLabel']);
expect(
/** @type {string} */ (_inputNode.getAttribute('aria-describedby')).split(' '),
).to.eql(['myInput', 'internalDescription']);
}); });
it('respects provided order for external elements', async () => { it('respects provided order for external elements', async () => {