Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions packages/base/src/Theming.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { addCustomCSS } from "./theming/CustomStyle.js";
import { addCustomCSS, removeCustomCSS } from "./theming/CustomStyle.js";
import { attachThemeLoaded, detachThemeLoaded } from "./theming/ThemeLoaded.js";

export { addCustomCSS, attachThemeLoaded, detachThemeLoaded };
export {
addCustomCSS,
removeCustomCSS,
attachThemeLoaded,
detachThemeLoaded,
};
8 changes: 7 additions & 1 deletion packages/base/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,12 @@ import {
} from "./Render.js";

// Theming.ts
import { addCustomCSS, attachThemeLoaded, detachThemeLoaded } from "./Theming.js";
import {
addCustomCSS,
removeCustomCSS,
attachThemeLoaded,
detachThemeLoaded,
} from "./Theming.js";

// UI5Element.ts
import UI5Element from "./UI5Element.js";
Expand Down Expand Up @@ -208,6 +213,7 @@ export {

// Theming.ts
addCustomCSS,
removeCustomCSS,
attachThemeLoaded,
detachThemeLoaded,

Expand Down
49 changes: 40 additions & 9 deletions packages/base/src/theming/CustomStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ import getSharedResource from "../getSharedResource.js";
import EventProvider from "../EventProvider.js";

type CustomCSSChangeCallback = (tag: string) => void;
type CustomCSSEntry = { id: string, css: string };

const getEventProvider = () => getSharedResource("CustomStyle.eventProvider", new EventProvider<string, void>());
const CUSTOM_CSS_CHANGE = "CustomCSSChange";

let currentId = 0;
const nextId = () => {
currentId++;
return `custom-css-${currentId}`;
};

const attachCustomCSSChange = (listener: CustomCSSChangeCallback) => {
getEventProvider().attachEvent(CUSTOM_CSS_CHANGE, listener);
};
Expand All @@ -19,7 +26,7 @@ const fireCustomCSSChange = (tag: string) => {
return getEventProvider().fireEvent(CUSTOM_CSS_CHANGE, tag);
};

const getCustomCSSFor = () => getSharedResource<Record<string, Array<string>>>("CustomStyle.customCSSFor", {});
const getCustomCSSFor = () => getSharedResource<Record<string, Array<CustomCSSEntry>>>("CustomStyle.customCSSFor", {});

// Listen to the eventProvider, in case other copies of this CustomStyle module fire this
// event, and this copy would therefore need to reRender the ui5 webcomponents; but
Expand All @@ -31,13 +38,7 @@ attachCustomCSSChange((tag: string) => {
}
});

const addCustomCSS = (tag: string, css: string) => {
const customCSSFor = getCustomCSSFor();
if (!customCSSFor[tag]) {
customCSSFor[tag] = [];
}
customCSSFor[tag].push(css);

const fireChangeAndRerender = (tag: string) => {
skipRerender = true;
try {
// The event is fired and the attached event listeners are all called synchronously
Expand All @@ -51,13 +52,43 @@ const addCustomCSS = (tag: string, css: string) => {
return reRenderAllUI5Elements({ tag });
};

const addCustomCSS = (tag: string, css: string): string => {
const customCSSFor = getCustomCSSFor();
if (!customCSSFor[tag]) {
customCSSFor[tag] = [];
}

const id = nextId();
customCSSFor[tag].push({ id, css });
fireChangeAndRerender(tag);

return id;
};

const removeCustomCSS = (tag: string, id: string) => {
const customCSSFor = getCustomCSSFor();
const entries = customCSSFor[tag];
if (!entries) {
return;
}

const index = entries.findIndex(entry => entry.id === id);
if (index === -1) {
return;
}

entries.splice(index, 1);
fireChangeAndRerender(tag);
};

const getCustomCSS = (tag: string) => {
const customCSSFor = getCustomCSSFor();
return customCSSFor[tag] ? customCSSFor[tag].join("") : "";
return customCSSFor[tag] ? customCSSFor[tag].map(entry => entry.css).join("") : "";
};

export {
addCustomCSS,
removeCustomCSS,
getCustomCSS,
attachCustomCSSChange,
detachCustomCSSChange,
Expand Down
145 changes: 145 additions & 0 deletions packages/main/cypress/specs/CustomCSS.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { addCustomCSS, removeCustomCSS } from "@ui5/webcomponents-base/dist/Theming.js";
import Select from "../../src/Select.js";
import Option from "../../src/Option.js";

describe("CustomCSS", () => {
it("addCustomCSS applies custom styles", () => {
cy.mount(
<Select id="sel">
<Option>Option 1</Option>
</Select>
);

cy.get("[ui5-select]")
.shadow()
.find(".ui5-select-root")
.then($el => {
addCustomCSS("ui5-select", ".ui5-select-root { background-color: rgb(255, 0, 0) !important; }");
});

cy.get("[ui5-select]")
.shadow()
.find(".ui5-select-root")
.should("have.css", "background-color", "rgb(255, 0, 0)");
});

it("addCustomCSS returns a string id", () => {
const id = addCustomCSS("ui5-select", ":host { /* noop */ }");

expect(id).to.be.a("string");
expect(id).to.include("custom-css-");
});

it("removeCustomCSS removes the applied custom styles", () => {
cy.mount(
<Select id="sel">
<Option>Option 1</Option>
</Select>
);

let cssId: string;

cy.get("[ui5-select]")
.shadow()
.find(".ui5-select-root")
.then(() => {
cssId = addCustomCSS("ui5-select", ".ui5-select-root { outline: 5px solid rgb(0, 128, 0); }");
});

cy.get("[ui5-select]")
.shadow()
.find(".ui5-select-root")
.should("have.css", "outline-color", "rgb(0, 128, 0)")
.then(() => {
removeCustomCSS("ui5-select", cssId);
});

cy.get("[ui5-select]")
.shadow()
.find(".ui5-select-root")
.should("not.have.css", "outline-color", "rgb(0, 128, 0)");
});

it("removeCustomCSS with invalid id does nothing", () => {
cy.mount(
<Select id="sel">
<Option>Option 1</Option>
</Select>
);

let cssId: string;

cy.get("[ui5-select]")
.shadow()
.find(".ui5-select-root")
.then(() => {
cssId = addCustomCSS("ui5-select", ".ui5-select-root { outline: 5px solid rgb(0, 0, 255); }");
});

cy.get("[ui5-select]")
.shadow()
.find(".ui5-select-root")
.should("have.css", "outline-color", "rgb(0, 0, 255)")
.then(() => {
removeCustomCSS("ui5-select", "non-existent-id");
});

// Style should still be applied
cy.get("[ui5-select]")
.shadow()
.find(".ui5-select-root")
.should("have.css", "outline-color", "rgb(0, 0, 255)")
.then(() => {
removeCustomCSS("ui5-select", cssId);
});
});

it("removeCustomCSS with non-existent tag does nothing", () => {
// Should not throw
removeCustomCSS("ui5-nonexistent", "some-id");
});

it("multiple addCustomCSS calls can be individually removed", () => {
cy.mount(
<Select id="sel">
<Option>Option 1</Option>
</Select>
);

let id1: string;
let id2: string;

cy.get("[ui5-select]")
.shadow()
.find(".ui5-select-root")
.then(() => {
id1 = addCustomCSS("ui5-select", ".ui5-select-root { outline: 3px solid rgb(255, 0, 0); }");
id2 = addCustomCSS("ui5-select", ".ui5-select-root { border: 3px solid rgb(0, 0, 255); }");
});

cy.get("[ui5-select]")
.shadow()
.find(".ui5-select-root")
.should("have.css", "outline-color", "rgb(255, 0, 0)")
.should("have.css", "border-color", "rgb(0, 0, 255)")
.then(() => {
removeCustomCSS("ui5-select", id1);
});

// First style removed, second still present
cy.get("[ui5-select]")
.shadow()
.find(".ui5-select-root")
.should("not.have.css", "outline-color", "rgb(255, 0, 0)")
.should("have.css", "border-color", "rgb(0, 0, 255)")
.then(() => {
removeCustomCSS("ui5-select", id2);
});

// Both removed
cy.get("[ui5-select]")
.shadow()
.find(".ui5-select-root")
.should("not.have.css", "border-color", "rgb(0, 0, 255)");
});
});
8 changes: 7 additions & 1 deletion packages/main/src/bundle.common.bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ import "@ui5/webcomponents-icons-business-suite/dist/Assets.js";
import "@ui5/webcomponents-icons-business-suite/dist/AllIcons.js";

import "@ui5/webcomponents-base/dist/features/F6Navigation.js";
import { addCustomCSS, attachThemeLoaded, detachThemeLoaded } from "@ui5/webcomponents-base/dist/Theming.js";
import {
addCustomCSS,
removeCustomCSS,
attachThemeLoaded,
detachThemeLoaded,
} from "@ui5/webcomponents-base/dist/Theming.js";
// import "./customI18n.js";

// Calendars
Expand Down Expand Up @@ -119,6 +124,7 @@ const testAssets = {
detachLanguageChange,
ResizeHandler,
addCustomCSS,
removeCustomCSS,
attachThemeLoaded,
detachThemeLoaded,
getIconNames,
Expand Down
20 changes: 19 additions & 1 deletion packages/main/test/pages/CustomCSS.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,33 @@
<br />
<br />

<ui5-button id="btn-remove">Remove custom css and rerender</ui5-button>

<br />
<br />

<script>
const customCSSIds = [];

const applyCustomCSSAndRerender = function() {
window["sap-ui-webcomponents-bundle"].addCustomCSS("ui5-select", ".ui5-select-root { background-color: red; } ");
const id = window["sap-ui-webcomponents-bundle"].addCustomCSS("ui5-select", ".ui5-select-root { background-color: red; } ");
customCSSIds.push(id);
};

const removeCustomCSSAndRerender = function() {
while (customCSSIds.length) {
window["sap-ui-webcomponents-bundle"].removeCustomCSS("ui5-select", customCSSIds.pop());
}
};

document
.getElementById("btn")
.addEventListener("click", applyCustomCSSAndRerender);

document
.getElementById("btn-remove")
.addEventListener("click", removeCustomCSSAndRerender);

</script>

</body>
Expand Down
Loading