Skip to content

Commit 478d5be

Browse files
committed
Move ListNestingStyles to adoptedStyleSheets for CSP compatibility
1 parent f7b92fe commit 478d5be

File tree

3 files changed

+105
-3
lines changed

3 files changed

+105
-3
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React from "react";
2+
import { render, cleanup } from "@testing-library/react";
3+
import ListNestingStyles from "./ListNestingStyles";
4+
5+
// Mock getListNestingStyles
6+
jest.mock("draftjs-conductor", () => ({
7+
getListNestingStyles: (max: number) => `.depth-${max} { color: red; }`,
8+
}));
9+
10+
describe("ListNestingStyles", () => {
11+
let originalAdoptedStyleSheets: CSSStyleSheet[];
12+
let mockSheet: CSSStyleSheet;
13+
14+
beforeEach(() => {
15+
// Save original adoptedStyleSheets
16+
originalAdoptedStyleSheets = [...document.adoptedStyleSheets];
17+
// Mock adoptedStyleSheets as a mutable array
18+
Object.defineProperty(document, "adoptedStyleSheets", {
19+
configurable: true,
20+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
21+
// @ts-ignore
22+
get: () => global.adoptedStyleSheets || [],
23+
set: (sheets) => {
24+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
25+
// @ts-ignore
26+
global.adoptedStyleSheets = sheets;
27+
},
28+
});
29+
document.adoptedStyleSheets = [];
30+
// Mock CSSStyleSheet
31+
mockSheet = {
32+
replaceSync: jest.fn(),
33+
} as unknown as CSSStyleSheet;
34+
window.CSSStyleSheet = jest.fn(() => mockSheet);
35+
});
36+
37+
afterEach(() => {
38+
// Restore adoptedStyleSheets
39+
Object.defineProperty(document, "adoptedStyleSheets", {
40+
configurable: true,
41+
value: originalAdoptedStyleSheets,
42+
writable: true,
43+
});
44+
cleanup();
45+
});
46+
47+
it("injects a stylesheet when mounted with max", () => {
48+
render(<ListNestingStyles max={3} />);
49+
expect(window.CSSStyleSheet).toHaveBeenCalled();
50+
expect(mockSheet.replaceSync).toHaveBeenCalledWith(
51+
".depth-3 { color: red; }",
52+
);
53+
expect(document.adoptedStyleSheets).toContain(mockSheet);
54+
});
55+
56+
it("removes the stylesheet on unmount", () => {
57+
const { unmount } = render(<ListNestingStyles max={2} />);
58+
expect(document.adoptedStyleSheets).toContain(mockSheet);
59+
unmount();
60+
expect(document.adoptedStyleSheets).not.toContain(mockSheet);
61+
});
62+
63+
it("does nothing if max is not provided", () => {
64+
render(<ListNestingStyles />);
65+
expect(window.CSSStyleSheet).not.toHaveBeenCalled();
66+
expect(document.adoptedStyleSheets).toHaveLength(0);
67+
});
68+
});

src/components/ListNestingStyles.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,37 @@
11
import { getListNestingStyles } from "draftjs-conductor";
2-
import React from "react";
2+
import React, { useEffect, useRef } from "react";
33

44
interface ListNestingStylesProps {
55
max?: number;
66
}
77

88
/**
9-
* Generates CSS styles for list items, for a given depth.
9+
* Injects CSS styles for list items using adoptedStyleSheets API.
1010
*/
1111
function Styles({ max }: ListNestingStylesProps) {
12-
return max ? <style>{getListNestingStyles(max)}</style> : null;
12+
const sheetRef = useRef<CSSStyleSheet | null>(null);
13+
14+
useEffect(() => {
15+
if (!max) return;
16+
if (!sheetRef.current) {
17+
sheetRef.current = new CSSStyleSheet();
18+
sheetRef.current.replaceSync(getListNestingStyles(max));
19+
}
20+
21+
if (!document.adoptedStyleSheets.includes(sheetRef.current)) {
22+
document.adoptedStyleSheets.push(sheetRef.current);
23+
}
24+
25+
// eslint-disable-next-line consistent-return
26+
return () => {
27+
// Remove the sheet when unmounting or max changes
28+
document.adoptedStyleSheets = document.adoptedStyleSheets.filter(
29+
(s) => s !== sheetRef.current,
30+
);
31+
};
32+
}, [max]);
33+
34+
return null;
1335
}
1436

1537
const ListNestingStyles = React.memo(Styles);

tests/environment.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,15 @@ global.PKG_VERSION = "";
77

88
// stats-js relies on canvas, which does not work with jsdom in Jest.
99
window.DISABLE_STATSGRAPH = true;
10+
11+
// Shim CSSStyleSheet and replaceSync
12+
// See https://github.com/jsdom/jsdom/issues/3766
13+
class CSSStyleSheet {
14+
// eslint-disable-next-line class-methods-use-this
15+
replaceSync() {
16+
// No-op
17+
}
18+
}
19+
window.CSSStyleSheet = CSSStyleSheet;
20+
21+
document.adoptedStyleSheets = [];

0 commit comments

Comments
 (0)