Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: enable hydration for declarative shadow roots #10519

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
4 changes: 3 additions & 1 deletion packages/base/src/UI5Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,9 @@ abstract class UI5Element extends HTMLElement {
const ctor = this.constructor as typeof UI5Element;
if (ctor._needsShadowDOM()) {
const defaultOptions = { mode: "open" } as ShadowRootInit;
this.attachShadow({ ...defaultOptions, ...ctor.getMetadata().getShadowRootOptions() });
if (!this.shadowRoot) { // if there is already a declarative shadow root, do not destroy it
this.attachShadow({ ...defaultOptions, ...ctor.getMetadata().getShadowRootOptions() });
}

const slotsAreManaged = ctor.getMetadata().slotsAreManaged();
if (slotsAreManaged) {
Expand Down
2 changes: 1 addition & 1 deletion packages/base/src/util/toLowercaseEnumValue.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
type LowercaseEnum<T> = T extends string ? Lowercase<T> : never;

export default function toLowercaseEnumValue<T extends string>(value: T): LowercaseEnum<T> {
return value.toLowerCase() as LowercaseEnum<T>;
return value?.toLowerCase() as LowercaseEnum<T>;
}
66 changes: 66 additions & 0 deletions packages/main/test/pages/DeclarativeShadowDOM.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="utf-8">

<title>Declarative Shadow DOM</title>

<script src="%VITE_BUNDLE_PATH%" type="module"></script>

<style>
*:not(:defined) {
display: none;
}
</style>

</head>

<body>

<h1>Declarative</h1>

<ui5-button icon="add">
<template shadowrootmode="open" shadowrootdelegatesfocus="true">
<button type="button" class="ui5-button-root" data-sap-focus-ref="true" tabindex="0" part="button" role="button" title="Add">
<ui5-icon class="ui5-button-icon" part="icon" ui5-icon design="Default" name="add" mode="Decorative" desktop>
<template shadowrootmode="open">
<svg class="ui5-icon-root" part="root" viewBox="0 0 512 512" role="presentation" focusable="false" preserveAspectRatio="xMidYMid meet" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" aria-label="Add"><g role="presentation"><path d="M454 230q11 0 18.5 7.5T480 256t-7.5 18.5T454 282H282v172q0 11-7.5 18.5T256 480t-18.5-7.5T230 454V282H58q-11 0-18.5-7.5T32 256t7.5-18.5T58 230h172V58q0-11 7.5-18.5T256 32t18.5 7.5T282 58v172h172z"></path></g></svg>
</template>
</ui5-icon>
<span id="ui5wc_1-content" class="ui5-button-text"><bdi><slot></slot></bdi></span>
</button>
</template>
Button
</ui5-button>

<ui5-avatar initials="XL" color-scheme="Accent5" size="XL">
<template shadowrootmode="open">
<div class="ui5-avatar-root" data-sap-focus-ref="true" role="img" aria-label="Avatar"><span class="ui5-avatar-initials">XL</span>
<ui5-icon class="ui5-avatar-icon ui5-avatar-icon-fallback ui5-avatar-fallback-icon-hidden" ui5-icon="" design="Default" name="employee" mode="Image" desktop="">
<template shadowrootmode="open">
<svg class="ui5-icon-root" part="root" viewBox="0 0 512 512" role="img" focusable="false" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg"><g role="presentation"><path d="M326 352q11 0 18.5 7.5T352 378t-7.5 18-18.5 7h-12q-11 0-18.5-7t-7.5-18 7.5-18.5T314 352h12zm16-97q48 23 77 67.5t29 99.5v32q0 11-7.5 18.5T422 480H90q-11 0-18.5-7.5T64 454v-32q0-55 29-99.5t77-67.5l-5-4q-18-18-27.5-41.5T128 160q0-27 10-50t27.5-40.5 41-27.5T256 32t49.5 10.5 41 28T374 111t10 49q0 27-11 52t-31 43zm-112 86l-25-15q-13-7-13-19v-6q-34 17-55.5 49T115 422v7h115v-88zm-51-181q0 32 22.5 54.5T256 237t54.5-22.5T333 160t-22.5-54.5T256 83t-54.5 22.5T179 160zm218 262q0-40-21.5-72T320 301v6q0 12-13 19l-26 15v88h116v-7z"></path></g></svg>
</template>
</ui5-icon>
<slot name="badge"></slot>
</div>
</template>
</ui5-avatar>

<br>

<h1>Imperative</h1>

<ui5-button icon="add">Button</ui5-button>
<ui5-avatar initials="XL" color-scheme="Accent5" size="XL"></ui5-avatar>

<script>
[...document.querySelectorAll("ui5-button")].forEach(button => {
button.addEventListener("click", () => {
console.log("Button clicked");
});
});
</script>

</body>
</html>
85 changes: 85 additions & 0 deletions packages/tools/lib/ssr/transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
const fs = require('fs');
const puppeteer = require('puppeteer');
const prettier = require('prettier');

const args = process.argv.slice(2); // Skip the first two elements
const packageName = args[0] || "main";
const pageName = args[1] || "Button";

const projectRoot = __dirname.split("/packages/tools/lib/ssr")[0];
const serverRoot = `http://localhost:8080`;

const testPageURL = `${serverRoot}/packages/${packageName}/test/pages/${pageName}.html`;
const outputPath = `${projectRoot}/packages/${packageName}/test/pages/${pageName}-SSR.html`;

const generateSSRPage = async (testPageURL, outputPath) => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(testPageURL);

let html = await page.evaluate(() => {
function processNode(node) {
const skipList = ["vite-plugin-checker-error-overlay", "ui5-announcement-area"];

if (skipList.includes(node.localName)) {
return document.createTextNode("");
}

if (node.nodeType === Node.ELEMENT_NODE) {
const clone = document.createElement(node.tagName.toLowerCase());

// Copy attributes
for (let attr of node.attributes) {
clone.setAttribute(attr.name, attr.value);
}

// Process shadow DOM if it's a custom element
if (customElements.get(node.tagName.toLowerCase())) {
const shadowRoot = node.shadowRoot;
if (shadowRoot) {
const template = document.createElement('template');
template.shadowRootMode = 'open';
if (shadowRoot.delegatesFocus) {
template.shadowRootDelegatesFocus = true;
}
for (let child of shadowRoot.children) {
template.content.appendChild(processNode(child));
}
clone.appendChild(template);
}
}
// Process children
for (let child of node.childNodes) {
clone.appendChild(processNode(child));
}

return clone;
} else{
return node.cloneNode();
}
}

const bodyClone = processNode(document.documentElement);
return bodyClone.outerHTML;
});

html = await prettier.format(html, { parser: 'html' });

// Remove vite-injected scripts
html = html.replace(`<script type="module">
import { inject } from "/@vite-plugin-checker-runtime";
inject({
overlayConfig: {},
base: "/",
});
</script>`, '');
html = html.replace(`<script type="module" src="/@vite-plugin-checker-client"></script>`, '');
html = html.replace(`<script type="module" src="/@vite/client"></script>`, '');

fs.writeFileSync(outputPath, html);
console.log("Done");

await browser.close();
};

generateSSRPage(testPageURL, outputPath);
Loading