Skip to content

Commit 2477eb2

Browse files
feat: add shadowRoot to internals
1 parent 9664fd6 commit 2477eb2

File tree

4 files changed

+106
-35
lines changed

4 files changed

+106
-35
lines changed

Diff for: src/element-internals.ts

+12-34
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
shadowHostsMap,
77
formElementsMap,
88
refValueMap,
9-
hiddenInputMap
9+
hiddenInputMap,
10+
shadowRootMap
1011
} from './maps';
1112
import { initAom } from './aom';
1213
import { getHostRoot, initRef, initLabels, initForm, findParentForm, createHiddenInput, removeHiddenInputs } from './utils';
@@ -167,6 +168,15 @@ export class ElementInternals implements IElementInternals {
167168
ref.setAttribute('aria-invalid', `${!valid}`);
168169
}
169170

171+
get shadowRoot(): ShadowRoot | null {
172+
const ref = refMap.get(this);
173+
const shadowRoot = shadowRootMap.get(ref);
174+
if (shadowRoot) {
175+
return shadowRootMap.get(ref);
176+
}
177+
return null;
178+
}
179+
170180
/** The element's validation message set during a call to ElementInternals.setValidity */
171181
get validationMessage(): string {
172182
return validationMessageMap.get(this);
@@ -201,6 +211,7 @@ if (!window.ElementInternals) {
201211
function attachShadowObserver(...args) {
202212
const shadowRoot = attachShadow.apply(this, args);
203213
const observer = new MutationObserver(observerCallback);
214+
shadowRootMap.set(this, shadowRoot);
204215
observer.observe(shadowRoot, observerConfig);
205216
shadowHostsMap.set(this, observer);
206217
return shadowRoot;
@@ -226,37 +237,4 @@ if (!window.ElementInternals) {
226237

227238
const documentObserver = new MutationObserver(observerCallback);
228239
documentObserver.observe(document.documentElement, observerConfig);
229-
230-
const FormDataOriginal = window.FormData;
231-
232-
class FormData {
233-
constructor(form?: HTMLFormElement) {
234-
const data = new FormDataOriginal(form);
235-
if (form && formElementsMap.has(form)) {
236-
const refs = formElementsMap.get(form);
237-
refs.forEach(ref => {
238-
if (ref.getAttribute('name')) {
239-
const value = refValueMap.get(ref);
240-
if (typeof value === 'string') {
241-
data.set(ref.getAttribute('name'), value);
242-
} else if (value != null) {
243-
const keysAdded = [];
244-
value.forEach((formDataValue, formDataKey) => {
245-
if (keysAdded.includes(formDataKey)) {
246-
data.append(formDataKey, formDataValue);
247-
} else {
248-
data.set(formDataKey, formDataValue);
249-
keysAdded.push(formDataKey);
250-
}
251-
})
252-
}
253-
}
254-
});
255-
}
256-
return data;
257-
}
258-
}
259-
260-
// @ts-ignore
261-
window.FormData = FormData;
262240
}

Diff for: src/maps.ts

+3
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,6 @@ export const refValueMap = new WeakMap<ICustomElement, any>();
3434

3535
/** Elements that need to be upgraded once added to the DOM */
3636
export const upgradeMap = new WeakMap<Element, IElementInternals>();
37+
38+
/** Save references to shadow roots for inclusion in internals instance */
39+
export const shadowRootMap = new WeakMap<ICustomElement, ShadowRoot>();

Diff for: static/scripts/address.js

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const addressSheet = new CSSStyleSheet();
2+
addressSheet.replace('input { display: block; } fieldset { display: flex; flex-flow: column; gap: 16px; border: 1px solid #ccc}');
3+
4+
class Address extends HTMLElement {
5+
static get formAssociated() {
6+
return true;
7+
}
8+
9+
internals = this.attachInternals();
10+
11+
connectedCallback() {
12+
const root = this.attachShadow({ mode: 'open' });
13+
root.adoptedStyleSheets = [addressSheet];
14+
const form = document.createElement('form');
15+
form.innerHTML = `
16+
<fieldset>
17+
<legend>Address</legend>
18+
<input name="line1" value="1600 Pennsylvania Ave"/>
19+
<input name="line2"/>
20+
<input name="city" value="Washington"/>
21+
<input name="state" value="District of Columbia"/>
22+
<button type="submit">Test</button>
23+
<button type="button">Other</button>
24+
</fieldset>`;
25+
root.append(form);
26+
27+
// this.log();
28+
form.addEventListener('submit', this.log.bind(this));
29+
30+
root.querySelector('[type="button"]')
31+
.addEventListener('click', this._other.bind(this));
32+
}
33+
34+
_other() {
35+
this.shadowRoot.querySelector('input').remove();
36+
}
37+
38+
log(event) {
39+
if (event) {
40+
event.preventDefault();
41+
}
42+
const internalFormData = new FormData(
43+
this.shadowRoot.querySelector('form')
44+
);
45+
this.internals.setFormValue(
46+
internalFormData
47+
);
48+
49+
const allData = new FormData(this.internals.form);
50+
51+
for (let [key, value] of allData.entries()) {
52+
console.log({ [key]: value });
53+
}
54+
}
55+
}
56+
57+
customElements.define('x-address', Address);

Diff for: test/ElementInternals.test.js

+34-1
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,39 @@ describe('The ElementInternals polyfill', () => {
336336
const output = new FormData(form);
337337
expect(Array.from(output.keys()).length).to.equal(1);
338338
expect(output.get('formdata')).to.equal('works');
339-
})
339+
});
340+
341+
it('saves a reference to all shadow roots', () => {
342+
expect(internals.shadowRoot).to.equal(el.shadowRoot);
343+
});
344+
});
345+
346+
describe('closed shadow root element', () => {
347+
let shadowRoot;
348+
let el;
349+
let internals;
350+
351+
class ClosedRoot extends HTMLElement {
352+
constructor() {
353+
super();
354+
shadowRoot = this.attachShadow({ mode: 'closed' });
355+
this.internals = this.attachInternals();
356+
}
357+
}
358+
359+
customElements.define('closed-root', ClosedRoot);
360+
361+
beforeEach(async () => {
362+
el = await fixture(html`<closed-root></closed-root>`);
363+
internals = el.internals;
364+
});
365+
366+
afterEach(async () => {
367+
await fixtureCleanup(el);
368+
});
369+
370+
it('maintains a reference to closed shadow roots', () => {
371+
expect(internals.shadowRoot).to.equal(shadowRoot);
372+
});
340373
});
341374
});

0 commit comments

Comments
 (0)