diff --git a/package-lock.json b/package-lock.json
index 5862d038101..c2a22cd18c1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1157,15 +1157,15 @@
}
},
"node_modules/@es-joy/jsdoccomment": {
- "version": "0.46.0",
- "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.46.0.tgz",
- "integrity": "sha512-C3Axuq1xd/9VqFZpW4YAzOx5O9q/LP46uIQy/iNDpHG3fmPa6TBtvfglMCs3RBiBxAIi0Go97r8+jvTt55XMyQ==",
+ "version": "0.48.0",
+ "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.48.0.tgz",
+ "integrity": "sha512-G6QUWIcC+KvSwXNsJyDTHvqUdNoAVJPPgkc3+Uk4WBKqZvoXhlvazOgm9aL0HwihJLQf0l+tOE2UFzXBqCqgDw==",
"dev": true,
"license": "MIT",
"dependencies": {
"comment-parser": "1.4.1",
"esquery": "^1.6.0",
- "jsdoc-type-pratt-parser": "~4.0.0"
+ "jsdoc-type-pratt-parser": "~4.1.0"
},
"engines": {
"node": ">=16"
@@ -5429,16 +5429,16 @@
}
},
"node_modules/eslint-plugin-jsdoc": {
- "version": "50.0.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.0.1.tgz",
- "integrity": "sha512-UayhAysIk1Du8InV27WMbV4AMSJSu60+bekmeuGK2OUy4QJSFPr1srYT6AInykGkmMdRuHfDX6Q0tJEr8BtDtg==",
+ "version": "50.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.2.2.tgz",
+ "integrity": "sha512-i0ZMWA199DG7sjxlzXn5AeYZxpRfMJjDPUl7lL9eJJX8TPRoIaxJU4ys/joP5faM5AXE1eqW/dslCj3uj4Nqpg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
- "@es-joy/jsdoccomment": "~0.46.0",
+ "@es-joy/jsdoccomment": "~0.48.0",
"are-docs-informative": "^0.0.2",
"comment-parser": "1.4.1",
- "debug": "^4.3.5",
+ "debug": "^4.3.6",
"escape-string-regexp": "^4.0.0",
"espree": "^10.1.0",
"esquery": "^1.6.0",
@@ -8638,7 +8638,9 @@
"license": "MIT"
},
"node_modules/jsdoc-type-pratt-parser": {
- "version": "4.0.0",
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz",
+ "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/src/app-data/index.ts b/src/app-data/index.ts
index 33c39e13853..7cb73f320c8 100644
--- a/src/app-data/index.ts
+++ b/src/app-data/index.ts
@@ -105,7 +105,7 @@ export const BUILD: BuildConditionals = {
devTools: false,
shadowDelegatesFocus: true,
initializeNextTick: false,
- asyncLoading: false,
+ asyncLoading: true,
asyncQueue: false,
transformTagName: false,
attachStyles: true,
diff --git a/src/compiler/transformers/static-to-meta/component.ts b/src/compiler/transformers/static-to-meta/component.ts
index 15dcf6cb143..5abeaf7c8b1 100644
--- a/src/compiler/transformers/static-to-meta/component.ts
+++ b/src/compiler/transformers/static-to-meta/component.ts
@@ -201,6 +201,7 @@ const validateComponentMembers = (node: ts.Node) => {
/**
* the parent node is a class declaration
*/
+ node.parent &&
ts.isClassDeclaration(node.parent)
) {
const propName = node.name.escapedText;
diff --git a/src/compiler/types/generate-app-types.ts b/src/compiler/types/generate-app-types.ts
index bff21008d3c..09643f214ee 100644
--- a/src/compiler/types/generate-app-types.ts
+++ b/src/compiler/types/generate-app-types.ts
@@ -108,7 +108,9 @@ const generateComponentTypesFile = (
return `{ ${typeData
.sort(sortImportNames)
.map((td) => {
- if (td.originalName === td.importName) {
+ if (td.originalName === '') {
+ return `${td.localName}`;
+ } else if (td.originalName === td.importName) {
return `${td.originalName}`;
} else {
return `${td.originalName} as ${td.importName}`;
diff --git a/src/compiler/types/tests/generate-app-types.spec.ts b/src/compiler/types/tests/generate-app-types.spec.ts
index 234202f6dd9..813fb5c7298 100644
--- a/src/compiler/types/tests/generate-app-types.spec.ts
+++ b/src/compiler/types/tests/generate-app-types.spec.ts
@@ -1747,6 +1747,11 @@ declare module "@stencil/core" {
location: 'import',
path: '@utils',
},
+ Fragment: {
+ location: 'import',
+ path: '@stencil/core',
+ id: '',
+ },
},
},
}),
@@ -1767,7 +1772,9 @@ declare module "@stencil/core" {
*/
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
import { MyType as UserImplementedPropType } from "@utils";
+import { Fragment } from "@stencil/core";
export { MyType as UserImplementedPropType } from "@utils";
+export { Fragment } from "@stencil/core";
export namespace Components {
/**
* docs
diff --git a/src/declarations/stencil-public-runtime.ts b/src/declarations/stencil-public-runtime.ts
index 85caf89d29e..5916ea13c37 100644
--- a/src/declarations/stencil-public-runtime.ts
+++ b/src/declarations/stencil-public-runtime.ts
@@ -619,6 +619,8 @@ export declare namespace h {
export function h(sel: any, data: VNodeData | null, children: VNode): VNode;
export namespace JSX {
+ interface Element extends LocalJSX.Element {}
+
interface IntrinsicElements extends LocalJSX.IntrinsicElements, JSXBase.IntrinsicElements {
[tagName: string]: any;
}
diff --git a/src/mock-doc/element.ts b/src/mock-doc/element.ts
index 81576b18ac4..4d4cf8a38eb 100644
--- a/src/mock-doc/element.ts
+++ b/src/mock-doc/element.ts
@@ -126,6 +126,12 @@ patchPropAttributes(
},
);
+Object.defineProperty(MockButtonElement.prototype, 'form', {
+ get(this: MockElement) {
+ return this.hasAttribute('form') ? this.getAttribute('form') : null;
+ },
+});
+
export class MockImageElement extends MockHTMLElement {
constructor(ownerDocument: any) {
super(ownerDocument, 'img');
diff --git a/src/runtime/styles.ts b/src/runtime/styles.ts
index b705d7a0ee3..cedf3559d0a 100644
--- a/src/runtime/styles.ts
+++ b/src/runtime/styles.ts
@@ -92,10 +92,29 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
}
/**
- * attach styles at the end of the head tag if we render shadow components
+ * attach styles at the end of the head tag if we render scoped components
*/
if (!(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation)) {
- styleContainerNode.append(styleElm);
+ if (styleContainerNode.nodeName === 'HEAD') {
+ /**
+ * if the page contains preconnect links, we want to insert the styles
+ * after the last preconnect link to ensure the styles are preloaded
+ */
+ const preconnectLinks = styleContainerNode.querySelectorAll('link[rel=preconnect]');
+ const referenceNode =
+ preconnectLinks.length > 0
+ ? preconnectLinks[preconnectLinks.length - 1].nextSibling
+ : document.querySelector('style');
+ (styleContainerNode as HTMLElement).insertBefore(styleElm, referenceNode);
+ } else if ('host' in styleContainerNode) {
+ /**
+ * if a scoped component is used within a shadow root, we want to insert the styles
+ * at the beginning of the shadow root node
+ */
+ (styleContainerNode as HTMLElement).prepend(styleElm);
+ } else {
+ styleContainerNode.append(styleElm);
+ }
}
/**
diff --git a/src/runtime/vdom/set-accessor.ts b/src/runtime/vdom/set-accessor.ts
index 950fad94fe2..c4e0aa6f20c 100644
--- a/src/runtime/vdom/set-accessor.ts
+++ b/src/runtime/vdom/set-accessor.ts
@@ -136,7 +136,11 @@ export const setAccessor = (
if (memberName === 'list') {
isProp = false;
} else if (oldValue == null || (elm as any)[memberName] != n) {
- (elm as any)[memberName] = n;
+ if (typeof (elm as any).__lookupSetter__(memberName) === 'function') {
+ (elm as any)[memberName] = n;
+ } else {
+ elm.setAttribute(memberName, n);
+ }
}
} else {
(elm as any)[memberName] = newValue;
diff --git a/src/runtime/vdom/test/set-accessor.spec.ts b/src/runtime/vdom/test/set-accessor.spec.ts
index ff99235244b..9f9bb4529f3 100644
--- a/src/runtime/vdom/test/set-accessor.spec.ts
+++ b/src/runtime/vdom/test/set-accessor.spec.ts
@@ -886,4 +886,16 @@ describe('setAccessor for standard html elements', () => {
expect(elm.style.cssText).toEqual('margin: 30px; color: orange;');
});
});
+
+ it('uses setAttribute if element has not setter', () => {
+ const elm = document.createElement('button');
+ const spy = jest.spyOn(elm, 'setAttribute');
+ setAccessor(elm, 'form', undefined, 'some-form', false, 0);
+ expect(spy.mock.calls).toEqual([['form', 'some-form']]);
+
+ const elm2 = document.createElement('button');
+ const spy2 = jest.spyOn(elm2, 'setAttribute');
+ setAccessor(elm2, 'textContent', undefined, 'some-content', false, 0);
+ expect(spy2.mock.calls).toEqual([]);
+ });
});
diff --git a/test/end-to-end/src/miscellaneous/renderToString.e2e.ts b/test/end-to-end/src/miscellaneous/renderToString.e2e.ts
index f6601be948c..1125850d033 100644
--- a/test/end-to-end/src/miscellaneous/renderToString.e2e.ts
+++ b/test/end-to-end/src/miscellaneous/renderToString.e2e.ts
@@ -49,11 +49,16 @@ describe('renderToString', () => {
);
});
- it('puts style last in the head tag', async () => {
+ it('puts style after preconnect links in the head tag', async () => {
const { html } = await renderToString(
`
+
@@ -72,11 +77,52 @@ describe('renderToString', () => {
{ fullDocument: true, serializeShadowRoot: false },
);
+ /**
+ * expect the scoped component styles to be injected after the preconnect link
+ */
expect(html).toContain(
- ' `,
+ );
+ });
+
+ it('puts styles before any custom styles', async () => {
+ const { html } = await renderToString(
+ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ { fullDocument: true, serializeShadowRoot: false },
+ );
+
+ /**
+ * expect the scoped component styles to be injected before custom styles
+ */
+ expect(html.replaceAll(/\n[ ]*/g, '')).toContain(
+ '.selected.sc-scoped-car-list{font-weight:bold;background:rgb(255, 255, 210)} ',
);
});
@@ -109,8 +155,8 @@ describe('renderToString', () => {
/**
* renders hydration styles and custom link tag within the head tag
*/
- expect(html).toContain(
- ' ',
);
});
});
diff --git a/test/end-to-end/tsconfig.json b/test/end-to-end/tsconfig.json
index c67ece8da0c..83de5031222 100644
--- a/test/end-to-end/tsconfig.json
+++ b/test/end-to-end/tsconfig.json
@@ -12,7 +12,7 @@
"jsxFragmentFactory": "Fragment",
"lib": [
"dom",
- "es2017"
+ "es2021"
],
"module": "esnext",
"moduleResolution": "node",
diff --git a/test/wdio/custom-elements-hierarchy-lifecycle/cmp-child.tsx b/test/wdio/custom-elements-hierarchy-lifecycle/cmp-child.tsx
new file mode 100644
index 00000000000..cf4fb3ce233
--- /dev/null
+++ b/test/wdio/custom-elements-hierarchy-lifecycle/cmp-child.tsx
@@ -0,0 +1,18 @@
+import { Component, h } from '@stencil/core';
+
+import { createAndAppendElement } from './cmp-util.js';
+
+@Component({
+ tag: 'custom-elements-hierarchy-lifecycle-child',
+ shadow: true,
+})
+export class CustomElementsHierarchyLifecycleChild {
+ async componentDidLoad(): Promise {
+ createAndAppendElement('DID LOAD CHILD');
+ return Promise.resolve();
+ }
+
+ render() {
+ return CHILD CONTENT
;
+ }
+}
diff --git a/test/wdio/custom-elements-hierarchy-lifecycle/cmp-parent.tsx b/test/wdio/custom-elements-hierarchy-lifecycle/cmp-parent.tsx
new file mode 100644
index 00000000000..d9131aa1646
--- /dev/null
+++ b/test/wdio/custom-elements-hierarchy-lifecycle/cmp-parent.tsx
@@ -0,0 +1,18 @@
+import { Component, h } from '@stencil/core';
+
+import { createAndAppendElement } from './cmp-util.js';
+
+@Component({
+ tag: 'custom-elements-hierarchy-lifecycle-parent',
+ shadow: true,
+})
+export class CustomElementsHierarchyLifecycleParent {
+ async componentDidLoad(): Promise {
+ createAndAppendElement('DID LOAD PARENT');
+ return Promise.resolve();
+ }
+
+ render() {
+ return ;
+ }
+}
diff --git a/test/wdio/custom-elements-hierarchy-lifecycle/cmp-util.ts b/test/wdio/custom-elements-hierarchy-lifecycle/cmp-util.ts
new file mode 100644
index 00000000000..e47bb48baf1
--- /dev/null
+++ b/test/wdio/custom-elements-hierarchy-lifecycle/cmp-util.ts
@@ -0,0 +1,6 @@
+export const createAndAppendElement = (text: string) => {
+ const p = document.createElement('p');
+ p.textContent = text;
+
+ document.body.appendChild(p);
+};
diff --git a/test/wdio/custom-elements-hierarchy-lifecycle/cmp.test.tsx b/test/wdio/custom-elements-hierarchy-lifecycle/cmp.test.tsx
new file mode 100644
index 00000000000..c864a91dd92
--- /dev/null
+++ b/test/wdio/custom-elements-hierarchy-lifecycle/cmp.test.tsx
@@ -0,0 +1,35 @@
+import { Fragment, h } from '@stencil/core';
+import { render } from '@wdio/browser-runner/stencil';
+
+import { defineCustomElement as defineCustomElementChildCmp } from '../test-components/custom-elements-hierarchy-lifecycle-child.js';
+import { defineCustomElement as defineCustomElementParentCmp } from '../test-components/custom-elements-hierarchy-lifecycle-parent.js';
+
+describe('custom-elements-hierarchy-lifecycle', () => {
+ before(() => {
+ defineCustomElementChildCmp();
+ defineCustomElementParentCmp();
+ });
+
+ it('should call componentDidLoad in the child before the parent', async () => {
+ expect(customElements.get('custom-elements-hierarchy-lifecycle-child')).toBeDefined();
+ expect(customElements.get('custom-elements-hierarchy-lifecycle-parent')).toBeDefined();
+
+ render({
+ template: () => (
+ <>
+
+ >
+ ),
+ });
+
+ const elm = document.querySelector('custom-elements-hierarchy-lifecycle-parent');
+ expect(elm.shadowRoot).toBeDefined();
+
+ await browser.waitUntil(() => Boolean(elm.shadowRoot.querySelector('custom-elements-hierarchy-lifecycle-child')));
+
+ expect(Array.from(document.querySelectorAll('p')).map((r) => r.textContent)).toEqual([
+ 'DID LOAD CHILD',
+ 'DID LOAD PARENT',
+ ]);
+ });
+});