Skip to content

Commit ca506cd

Browse files
fix: ensure polyfill generates attributes
1 parent 9372415 commit ca506cd

File tree

4 files changed

+47
-5
lines changed

4 files changed

+47
-5
lines changed

Diff for: src/element-internals.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
validationAnchorMap,
99
validityMap,
1010
validationMessageMap,
11+
validityUpgradeMap,
1112
} from './maps';
1213
import {
1314
createHiddenInput,
@@ -216,9 +217,16 @@ export class ElementInternals implements IElementInternals {
216217
throw new DOMException(`Failed to execute 'setValidity' on 'ElementInternals': The second argument should not be empty if one or more flags in the first argument are true.`);
217218
}
218219
validationMessageMap.set(this, valid ? '' : validationMessage);
219-
ref.toggleAttribute('internals-invalid', !valid);
220-
ref.toggleAttribute('internals-valid', valid);
221-
ref.setAttribute('aria-invalid', `${!valid}`);
220+
221+
// check to make sure the host element is connected before adding attributes
222+
// because safari doesnt allow elements to have attributes added in the constructor
223+
if (ref.isConnected) {
224+
ref.toggleAttribute('internals-invalid', !valid);
225+
ref.toggleAttribute('internals-valid', valid);
226+
ref.setAttribute('aria-invalid', `${!valid}`);
227+
} else {
228+
validityUpgradeMap.set(ref, this);
229+
}
222230
}
223231

224232
get shadowRoot(): ShadowRoot | null {
@@ -262,7 +270,7 @@ export class ElementInternals implements IElementInternals {
262270
declare global {
263271
interface CustomElementConstructor {
264272
formAssociated?: boolean;
265-
}
273+
}
266274

267275
interface Window {
268276
ElementInternals: typeof ElementInternals

Diff for: src/maps.ts

+3
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,6 @@ export const documentFragmentMap = new WeakMap<DocumentFragment, MutationObserve
4646

4747
/** Whether connectedCallback has already been called. */
4848
export const connectedCallbackMap = new WeakMap<ICustomElement, boolean>();
49+
50+
/** Save a reference to validity state for elements that need to upgrade after being connected */
51+
export const validityUpgradeMap = new WeakMap<ICustomElement, IElementInternals>();

Diff for: src/mutation-observers.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { internalsMap, shadowHostsMap, upgradeMap, hiddenInputMap, documentFragmentMap, formElementsMap } from './maps.js';
1+
import { internalsMap, shadowHostsMap, upgradeMap, hiddenInputMap, documentFragmentMap, formElementsMap, validityUpgradeMap } from './maps.js';
22
import { aom } from './aom.js';
33
import { removeHiddenInputs, initForm, initLabels, upgradeInternals, setDisabled } from './utils.js';
44
import { ICustomElement } from './types.js';
@@ -67,6 +67,7 @@ export function observerCallback(mutationList: MutationRecord[]) {
6767
initNode(node);
6868
}
6969

70+
7071
/** Upgrade the accessibility information on any previously connected */
7172
if (upgradeMap.has(node)) {
7273
const internals = upgradeMap.get(node);
@@ -79,6 +80,15 @@ export function observerCallback(mutationList: MutationRecord[]) {
7980
upgradeMap.delete(node);
8081
}
8182

83+
/** Upgrade the validity state when the element is connected */
84+
if (validityUpgradeMap.has(node)) {
85+
const internals = validityUpgradeMap.get(node);
86+
node.setAttribute('internals-valid', internals.validity.valid.toString());
87+
node.setAttribute('internals-invalid', (!internals.validity.valid).toString());
88+
node.setAttribute('aria-invalid', (!internals.validity.valid).toString());
89+
validityUpgradeMap.delete(node);
90+
}
91+
8292
/** If the node that's added is a form, check the validity */
8393
if (node.localName === 'form') {
8494
const formElements = formElementsMap.get(node as unknown as HTMLFormElement);

Diff for: test/polyfilledBrowsers.test.js

+21
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,15 @@ describe('ElementInternals polyfill behavior', () => {
6868
}
6969
}
7070

71+
class ValidateInConstructor extends FormAssociated {
72+
constructor() {
73+
super();
74+
this.internals.setValidity({ valueMissing: true }, 'Test');
75+
}
76+
}
77+
7178
customElements.define('form-associated', FormAssociated);
79+
customElements.define('validate-in-constructor', ValidateInConstructor);
7280

7381
beforeEach(async () => {
7482
form = await fixture(html`<form>
@@ -106,6 +114,19 @@ describe('ElementInternals polyfill behavior', () => {
106114
expect(el.getAttribute('role')).to.equal('button');
107115
}
108116
});
117+
118+
it('will not throw and will upgrade if constructed using document.createElement', async () => {
119+
let el;
120+
expect(() => {
121+
el = document.createElement('validate-in-constructor');
122+
}).not.to.throw();
123+
document.body.append(el);
124+
await aTimeout(0);
125+
if (ElementInternals.isPolyfilled) {
126+
expect(el.getAttribute('internals-valid')).to.equal('false');
127+
expect(el.getAttribute('internals-invalid')).to.equal('true');
128+
}
129+
});
109130
});
110131

111132
describe('CustomStateSet', () => {

0 commit comments

Comments
 (0)