Skip to content
855 changes: 618 additions & 237 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"test:watch": "npm run test -- --watch",
"test:coverage": "npm run test -- --coverage",
"test:jsdom": "node ./jsdom-tests/jsdom.test.js",
"pretest:jsdom": "rollup -i dist/index.js -o jsdom-tests/polyfill.js -f iife",
"pretest:jsdom": "rollup -i dist/index.js -o jsdom-tests/polyfill.js -f iife -n polyfill --context window",
Comment thread
Cliffback marked this conversation as resolved.
"start": "rollup -c --watch --environment BUILD:dev",
"build": "tsc",
"prerelease": "npm run build",
Expand Down Expand Up @@ -48,9 +48,10 @@
"homepage": "https://github.com/calebdwilliams/element-internals-polyfill#readme",
"devDependencies": {
"@changesets/cli": "^2.27.8",
"@lit-labs/testing": "^0.2.1",
"@lit-labs/testing": "^0.2.7",
"@open-wc/testing": "^3.1.7",
"@open-wc/testing-helpers": "^1.7.1",
"@playwright/test": "^1.45.0",
"@rollup/plugin-node-resolve": "^7.1.3",
"@rollup/plugin-typescript": "^6.0.0",
"@types/mocha": "^10.0.1",
Expand All @@ -69,7 +70,7 @@
"karma-rollup-preprocessor": "^7.0.5",
"karma-safari-launcher": "^1.0.0",
"karma-safarinative-launcher": "^1.1.0",
"lit": "^2.7.2",
"lit": "^3.3.0",
"lit-html": "^1.2.1",
"rollup": "^2.46.0",
"rollup-plugin-cleanup": "^3.2.1",
Expand All @@ -80,6 +81,5 @@
"standard-version": "^9.0.0",
"tslib": "^2.1.0",
"typescript": "^5.5.4"
},
"dependencies": {}
}
}
15 changes: 11 additions & 4 deletions src/element-internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,23 @@ import {
setAttribute,
createHiddenInput,
findParentForm,
initRef,
mutationObserverExists,
removeHiddenInputs,
setDisabled,
throwIfNotFormAssociated,
upgradeInternals,
} from "./utils.js";
import {
initRef,
} from "./mutation-observers.js";
import { initAom } from "./aom.js";
import { ValidityState, reconcileValidity, setValid } from "./ValidityState.js";
import {
deferUpgrade,
observerCallback,
observerConfig,
} from "./mutation-observers.js";
import { ICustomElement, LabelsList } from "./types.js";
import { LabelsList } from "./types.js";
import { CustomStateSet } from "./CustomStateSet.js";
import { patchFormPrototype } from "./patch-form-prototype.js";

Expand Down Expand Up @@ -403,6 +405,11 @@ export function forceCustomStateSetPolyfill(
* polyfill as well.
*/
export function forceElementInternalsPolyfill(forceCustomStateSet = true) {
/**
* This is a flag to prevent a DOMException from being thrown when
* attachInternals is called in upgradeInternals.
*/
let attachedFlag = false;
Comment thread
Cliffback marked this conversation as resolved.
if (hasElementInternalsPolyfillBeenApplied) {
return;
}
Expand Down Expand Up @@ -436,7 +443,7 @@ export function forceElementInternalsPolyfill(forceCustomStateSet = true) {
connectedCallback.apply(this);
}
// always upgradeInternals in connectedCallback instead of constructor
upgradeInternals(this);
attachedFlag = upgradeInternals(this);
};
}

Expand All @@ -458,7 +465,7 @@ export function forceElementInternalsPolyfill(forceCustomStateSet = true) {
`Failed to execute 'attachInternals' on 'HTMLElement': Unable to attach ElementInternals to non-custom elements.`
);
}
if (internalsMap.has(this)) {
if (internalsMap.has(this) && !attachedFlag) {
throw new DOMException(
`DOMException: Failed to execute 'attachInternals' on 'HTMLElement': ElementInternals for the specified element was already attached.`
);
Expand Down
17 changes: 17 additions & 0 deletions src/mutation-observers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@ import { aom } from './aom.js';
import { setAttribute, removeHiddenInputs, initForm, initLabels, upgradeInternals, setDisabled, mutationObserverExists } from './utils.js';
import { ICustomElement } from './types.js';


/**
* Initialize a ref by setting up an attribute observe on it
* looking for changes to disabled
* @param {HTMLElement} ref - The element to watch
* @param {ElementInternals} internals - The element internals instance for the ref
* @return {void}
*/
export const initRef = (
ref: HTMLElement,
internals: ElementInternals
): void => {
hiddenInputMap.set(internals, []);
disabledOrNameObserver.observe?.(ref, disabledOrNameObserverConfig);
};


Comment thread
Cliffback marked this conversation as resolved.
function initNode(node: ICustomElement): void {
const internals = internalsMap.get(node);
const { form } = internals;
Expand Down
27 changes: 10 additions & 17 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,21 +92,6 @@ export const createHiddenInput = (
return input;
};

/**
* Initialize a ref by setting up an attribute observe on it
* looking for changes to disabled
* @param {HTMLElement} ref - The element to watch
* @param {ElementInternals} internals - The element internals instance for the ref
* @return {void}
*/
export const initRef = (
ref: HTMLElement,
internals: ElementInternals
): void => {
hiddenInputMap.set(internals, []);
disabledOrNameObserver.observe?.(ref, disabledOrNameObserverConfig);
};

/**
* Set up labels for the ref
* @param {HTMLElement} ref - The ref to add labels to
Expand Down Expand Up @@ -360,13 +345,21 @@ export const overrideFormMethod = (
* either constructed or appended from a DocumentFragment
* @param ref {HTMLElement} - The custom element to upgrade
*/
export const upgradeInternals = (ref: FormAssociatedCustomElement) => {
export const upgradeInternals = (ref: FormAssociatedCustomElement): boolean => {
let attached = false;
if (ref.constructor["formAssociated"]) {
const internals = internalsMap.get(ref);
let internals = internalsMap.get(ref);
// we might have cases where the internals are not set
if (internals === undefined) {
ref.attachInternals();
internals = internalsMap.get(ref);
attached = true;
}
const { labels, form } = internals;
initLabels(ref, labels);
initForm(ref, form, internals);
}
return attached;
};

/**
Expand Down
6 changes: 3 additions & 3 deletions test/CustomStateSet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ describe('CustomStateSet polyfill', () => {

if (window.CustomStateSet.isPolyfilled) {
await aTimeout(100);
expect(el.matches('[state--foo]')).to.be.true;
expect(el.internals.states.has('--foo')).to.be.true;
Comment thread
Cliffback marked this conversation as resolved.
} else {
expect(el.matches(`:--foo`)).to.be.true;
    expect(el.internals.states.has('--foo')).to.be.true;
}
});

Expand All @@ -107,7 +107,7 @@ describe('CustomStateSet polyfill', () => {
await aTimeout(100);
expect(el.matches('[state--foo]')).to.be.false;
} else {
expect(el.matches(`:--foo`)).to.be.false;
    expect(el.internals.states.has('--foo')).to.be.false;
}
});
});
10 changes: 8 additions & 2 deletions test/FormElements.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { expect, fixture, html } from '@open-wc/testing';
import '../dist/index.js';

class TestInput extends HTMLElement {
static formAssociated = true;
internals = this.attachInternals();
Expand All @@ -11,7 +10,12 @@ customElements.define('test-input', TestInput);
class TestDummy extends HTMLElement {
}


customElements.define('test-dummy', TestDummy);
class TestFormAssociatedNoAttachInternals extends HTMLElement {
static formAssociated = true;
}
customElements.define('test-no-attach-internals', TestFormAssociatedNoAttachInternals);

async function createForm(): Promise<HTMLFormElement> {
return await fixture<HTMLFormElement>(html`
Expand All @@ -22,18 +26,20 @@ async function createForm(): Promise<HTMLFormElement> {
<test-input name="second" id="ti2"></test-input>
<test-input name="third" id="ti3"></test-input>
<button type="submit">Submit</button>
<test-no-attach-internals></test-no-attach-internals>
</form>`);
}

it('must contains the custom elements associated to the current form, in the correct order', async () => {
const form = await createForm();
expect(form.elements).to.have.length(5);
expect(form.elements).to.have.length(6);

expect(form.elements[0]).to.be.an.instanceof(HTMLInputElement);
expect(form.elements[1]).to.be.an.instanceof(TestInput);
expect(form.elements[2]).to.be.an.instanceof(TestInput);
expect(form.elements[3]).to.be.an.instanceof(TestInput);
expect(form.elements[4]).to.be.an.instanceof(HTMLButtonElement);
expect(form.elements[5]).to.be.an.instanceof(TestFormAssociatedNoAttachInternals);

expect(form.elements[0].id).to.equal('foo');
expect(form.elements[1].id).to.equal('ti1');
Expand Down