Skip to content

Commit 8aa4faf

Browse files
Elliott Marquezcopybara-github
authored andcommitted
fix: aria polyfill overrides user values and user values override internals values
PiperOrigin-RevId: 565570035
1 parent af171df commit 8aa4faf

2 files changed

Lines changed: 160 additions & 28 deletions

File tree

internal/aria/aria.ts

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -271,68 +271,64 @@ export function polyfillElementInternalsAria(
271271
throw new Error('Missing setupHostAria()');
272272
}
273273

274-
let firstConnectedCallbacks: Array<() => void> = [];
274+
let firstConnectedCallbacks:
275+
Array<{property: ARIAProperty | 'role', callback: () => void}> = [];
275276
let hasBeenConnected = false;
276277

277278
// Add support for Firefox, which has not yet implement ElementInternals aria
278279
for (const ariaProperty of ARIA_PROPERTIES) {
279-
let ariaValueBeforeConnected: string|null = null;
280+
let internalAriaValue: string|null = null;
280281
Object.defineProperty(internals, ariaProperty, {
281282
enumerable: true,
282283
configurable: true,
283284
get() {
284-
if (!hasBeenConnected) {
285-
return ariaValueBeforeConnected;
286-
}
287-
288-
// Dynamic lookup rather than hardcoding all properties.
289-
// tslint:disable-next-line:no-dict-access-on-struct-type
290-
return host[ariaProperty];
285+
return internalAriaValue;
291286
},
292287
set(value: string|null) {
293288
const setValue = () => {
289+
internalAriaValue = value;
290+
if (!hasBeenConnected) {
291+
firstConnectedCallbacks.push(
292+
{property: ariaProperty, callback: setValue});
293+
return;
294+
}
295+
294296
// Dynamic lookup rather than hardcoding all properties.
295297
// tslint:disable-next-line:no-dict-access-on-struct-type
296298
host[ariaProperty] = value;
297299
};
298300

299-
if (!hasBeenConnected) {
300-
ariaValueBeforeConnected = value;
301-
firstConnectedCallbacks.push(setValue);
302-
return;
303-
}
304-
305301
setValue();
306302
},
307303
});
308304
}
309305

310-
let roleValueBeforeConnected: string|null = null;
306+
let internalRoleValue: string|null = null;
311307
Object.defineProperty(internals, 'role', {
312308
enumerable: true,
313309
configurable: true,
314310
get() {
315-
if (!hasBeenConnected) {
316-
return roleValueBeforeConnected;
317-
}
318-
319-
return host.getAttribute('role');
311+
return internalRoleValue;
320312
},
321313
set(value: string|null) {
322314
const setRole = () => {
315+
internalRoleValue = value;
316+
317+
if (!hasBeenConnected) {
318+
firstConnectedCallbacks.push({
319+
property: 'role',
320+
callback: setRole,
321+
});
322+
return;
323+
}
324+
323325
if (value === null) {
324326
host.removeAttribute('role');
325327
} else {
326328
host.setAttribute('role', value);
327329
}
328330
};
329331

330-
if (!hasBeenConnected) {
331-
roleValueBeforeConnected = value;
332-
firstConnectedCallbacks.push(setRole);
333-
return;
334-
}
335-
336332
setRole();
337333
},
338334
});
@@ -344,7 +340,31 @@ export function polyfillElementInternalsAria(
344340
}
345341

346342
hasBeenConnected = true;
347-
for (const callback of firstConnectedCallbacks) {
343+
344+
const propertiesSetByUser = new Set<ARIAProperty|'role'>();
345+
346+
// See which properties were set by the user on host before we apply
347+
// internals values as attributes to host. Needs to be done in another
348+
// for loop because the callbacks set these attributes on host.
349+
for (const {property} of firstConnectedCallbacks) {
350+
const wasSetByUser =
351+
host.getAttribute(ariaPropertyToAttribute(property)) !== null ||
352+
// Dynamic lookup rather than hardcoding all properties.
353+
// tslint:disable-next-line:no-dict-access-on-struct-type
354+
host[property] !== undefined;
355+
356+
if (wasSetByUser) {
357+
propertiesSetByUser.add(property);
358+
}
359+
}
360+
361+
for (const {property, callback} of firstConnectedCallbacks) {
362+
// If the user has set the attribute or property, do not override the
363+
// user's value
364+
if (propertiesSetByUser.has(property)) {
365+
continue;
366+
}
367+
348368
callback();
349369
}
350370

internal/aria/aria_test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,80 @@ describe('aria', () => {
198198
element.remove();
199199
});
200200

201+
it('should not override aria attributes on host when set before connection',
202+
async () => {
203+
const element = new TestElement();
204+
element.setAttribute('aria-label', 'Value set by user');
205+
element.internals.ariaLabel = 'Value set on internals';
206+
document.body.appendChild(element);
207+
await element.updateComplete;
208+
expect(element.getAttribute('aria-label'))
209+
.withContext('aria-label attribute value on host')
210+
.toEqual('Value set by user');
211+
expect(element.internals.ariaLabel)
212+
.withContext('ariaLabel internals property still the same')
213+
.toEqual('Value set on internals');
214+
215+
element.remove();
216+
});
217+
218+
it('should not override aria properties on host when set before connection',
219+
async () => {
220+
const element = new TestElement();
221+
element.ariaLabel = 'Value set by user';
222+
element.internals.ariaLabel = 'Value set on internals';
223+
document.body.appendChild(element);
224+
await element.updateComplete;
225+
expect(element.getAttribute('aria-label'))
226+
.withContext('aria-label attribute value on host')
227+
.toEqual('Value set by user');
228+
expect(element.ariaLabel)
229+
.withContext('ariaLabel property value on host')
230+
.toEqual('Value set by user');
231+
expect(element.internals.ariaLabel)
232+
.withContext('ariaLabel internals property still the same')
233+
.toEqual('Value set on internals');
234+
235+
element.remove();
236+
});
237+
238+
it('should not override role attribute on host when set before connection',
239+
async () => {
240+
const element = new TestElement();
241+
element.setAttribute('role', 'Value set by user');
242+
element.internals.role = 'Value set on internals';
243+
document.body.appendChild(element);
244+
await element.updateComplete;
245+
expect(element.getAttribute('role'))
246+
.withContext('role attribute value on host')
247+
.toEqual('Value set by user');
248+
expect(element.internals.role)
249+
.withContext('role internals property still the same')
250+
.toEqual('Value set on internals');
251+
252+
element.remove();
253+
});
254+
255+
it('should not override role property on host when set before connection',
256+
async () => {
257+
const element = new TestElement();
258+
element.role = 'Value set by user';
259+
element.internals.role = 'Value set on internals';
260+
document.body.appendChild(element);
261+
await element.updateComplete;
262+
expect(element.getAttribute('role'))
263+
.withContext('role attribute value on host')
264+
.toEqual('Value set by user');
265+
expect(element.role)
266+
.withContext('role property value on host')
267+
.toEqual('Value set by user');
268+
expect(element.internals.role)
269+
.withContext('role internals property still the same')
270+
.toEqual('Value set on internals');
271+
272+
element.remove();
273+
});
274+
201275
it('should handle setting role multiple times before connection',
202276
async () => {
203277
const element = new TestElement();
@@ -216,6 +290,25 @@ describe('aria', () => {
216290
element.remove();
217291
});
218292

293+
it('should handle setting role multiple times before connection when property is set on host',
294+
async () => {
295+
const element = new TestElement();
296+
element.role = 'radio';
297+
element.internals.role = 'button';
298+
element.internals.role = 'checkbox';
299+
300+
expect(element.internals.role)
301+
.withContext('internals.role before connection')
302+
.toEqual('checkbox');
303+
document.body.appendChild(element);
304+
await element.updateComplete;
305+
expect(element.internals.role)
306+
.withContext('internals.role after connection')
307+
.toEqual('checkbox');
308+
309+
element.remove();
310+
});
311+
219312
it('should handle setting aria properties multiple times before connection',
220313
async () => {
221314
const element = new TestElement();
@@ -234,6 +327,25 @@ describe('aria', () => {
234327
element.remove();
235328
});
236329

330+
it('should handle setting aria properties multiple times before connection when property is set on host',
331+
async () => {
332+
const element = new TestElement();
333+
element.ariaLabel = 'First';
334+
element.internals.ariaLabel = 'First';
335+
element.internals.ariaLabel = 'Second';
336+
337+
expect(element.internals.ariaLabel)
338+
.withContext('internals.ariaLabel before connection')
339+
.toEqual('Second');
340+
document.body.appendChild(element);
341+
await element.updateComplete;
342+
expect(element.internals.ariaLabel)
343+
.withContext('internals.ariaLabel after connection')
344+
.toEqual('Second');
345+
346+
element.remove();
347+
});
348+
237349
it('should handle setting role after first connection while disconnected',
238350
async () => {
239351
const element = new TestElement();

0 commit comments

Comments
 (0)