Skip to content

Commit f7a5dcf

Browse files
refactor: reuse UI5 Web Components Styling (#4858)
- reuse UI5 Web Components adopted stylesheets instead of react-jss - migrate AnalyticalCard as example --------- Co-authored-by: Lukas Harbarth <[email protected]>
1 parent 9d97e9e commit f7a5dcf

File tree

12 files changed

+345
-27
lines changed

12 files changed

+345
-27
lines changed

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.module.css.ts

package.json

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
"private": true,
77
"license": "Apache-2.0",
88
"scripts": {
9-
"start": "lerna run build:i18n && yarn create-cypress-commands-docs && storybook dev -p 6006",
10-
"build:prepare": "lerna run build:i18n && rimraf node_modules/@types/mocha",
11-
"build": "yarn build:prepare && tsc --build && lerna run build:client && lerna run build:wrapper && lerna run build:ssr",
9+
"start": "yarn prepare && yarn create-cypress-commands-docs && npm-run-all -p start:watcher start:storybook",
10+
"start:watcher": "lerna run watch:css",
11+
"start:storybook": "storybook dev -p 6006",
12+
"prepare": "lerna run build:i18n && lerna run build:css && rimraf node_modules/@types/mocha",
13+
"build": "yarn prepare && tsc --build && lerna run build:client && lerna run build:wrapper && lerna run build:ssr",
1214
"build:storybook": "lerna run build:i18n && yarn create-cypress-commands-docs && storybook build -o .out",
1315
"test:prepare": "rimraf temp && lerna run build",
1416
"test:cypress": "cypress run --component --browser chrome --spec 'packages/main/src/components/*/**',packages/base,packages/charts,packages/cypress-commands",
@@ -25,7 +27,7 @@
2527
"examples:recreate-seed": "rimraf examples/seed-test && npx create-react-app examples/seed-test --template file:./packages/cra-template-seed",
2628
"examples:start-seed": "cd examples/seed-test && SKIP_PREFLIGHT_CHECK=true yarn start",
2729
"chromatic": "cross-env STORYBOOK_ENV=chromatic npx chromatic --build-script-name build:storybook",
28-
"postinstall": "rimraf node_modules/@types/mocha && husky install && lerna run build:i18n",
30+
"postinstall": "husky install && yarn prepare",
2931
"create-cypress-commands-docs": "typedoc && rimraf temp/typedoc"
3032
},
3133
"dependencies": {
@@ -60,6 +62,7 @@
6062
"@ui5/webcomponents-tools": "1.15.1",
6163
"@vitejs/plugin-react": "^4.0.0",
6264
"chromatic": "^6.5.3",
65+
"cssnano": "^6.0.1",
6366
"cypress": "^12.1.0",
6467
"dedent": "^0.7.0",
6568
"eslint": "^8.35.0",
@@ -78,6 +81,10 @@
7881
"lerna": "^7.0.0",
7982
"lint-staged": "^13.0.0",
8083
"npm-run-all": "^4.1.5",
84+
"postcss": "^8.4.25",
85+
"postcss-cli": "^10.1.0",
86+
"postcss-import": "^15.1.0",
87+
"postcss-modules": "^6.0.0",
8188
"prettier": "^2.8.4",
8289
"rimraf": "^5.0.0",
8390
"storybook": "7.0.26",

packages/base/src/hooks/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useIsomorphicId } from './useIsomorphicId.js';
44
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect.js';
55
import { useIsRTL } from './useIsRTL.js';
66
import { useResponsiveContentPadding } from './useResponsiveContentPadding.js';
7+
import { useStylesheet } from './useStylesheet.js';
78
import { useSyncRef } from './useSyncRef.js';
89
import { useViewportRange } from './useViewportRange.js';
910

@@ -15,5 +16,6 @@ export {
1516
useSyncRef,
1617
useViewportRange,
1718
useIsomorphicId,
19+
useStylesheet,
1820
useCurrentTheme
1921
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { StyleDataCSP } from '@ui5/webcomponents-base/dist/ManagedStyles.js';
2+
import { createOrUpdateStyle, removeStyle } from '@ui5/webcomponents-base/dist/ManagedStyles.js';
3+
import * as React from 'react';
4+
5+
function getUseInsertionEffect(isSSR: boolean) {
6+
return isSSR ? React.useEffect : Reflect.get(React, 'useInsertionEffect') || React.useLayoutEffect;
7+
}
8+
9+
export function useStylesheet(styles: StyleDataCSP, componentName: string) {
10+
getUseInsertionEffect(typeof window === 'undefined')(() => {
11+
createOrUpdateStyle(styles, styles.packageName, componentName);
12+
13+
return () => {
14+
removeStyle(styles.packageName, componentName);
15+
};
16+
}, [styles]);
17+
}

packages/main/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@
1111
/wrappers
1212
/ssr
1313
src/i18n/i18n-defaults.ts
14+
src/**/*.css.ts

packages/main/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@
4747
"build:i18n": "node scripts/generateI18n.mjs",
4848
"build:client": "babel src --out-dir dist --extensions .ts,.tsx --env-name client && tsc --declarationDir dist",
4949
"build:ssr": "babel src --out-dir ssr --extensions .ts,.tsx --env-name ssr && tsc --declarationDir ssr",
50-
"build:wrapper": "babel src --out-dir wrappers --extensions .ts,.tsx --env-name wrapper && tsc --declarationDir wrappers"
50+
"build:wrapper": "babel src --out-dir wrappers --extensions .ts,.tsx --env-name wrapper && tsc --declarationDir wrappers",
51+
"build:css": "postcss --dir ../../temp src/**/*.css",
52+
"watch:css": "postcss --watch --dir ../../temp src/**/*.css"
5153
},
5254
"dependencies": {
5355
"@tanstack/react-virtual": "3.0.0-beta.18",

packages/main/postcss.config.cjs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const fs = require('node:fs');
2+
const cssnano = require('cssnano');
3+
const postcssImport = require('postcss-import');
4+
const postcssModules = require('postcss-modules');
5+
const postcssCSStoESM = require('../../scripts/postcss-css-to-esm.cjs');
6+
7+
const packageName = JSON.parse(fs.readFileSync('./package.json').toString()).name;
8+
9+
module.exports = {
10+
plugins: [
11+
postcssImport(),
12+
postcssModules({
13+
// generateScopedName: '[name]__[local]___[hash:base64:5]',
14+
getJSON: (cssFileName, json) => {
15+
return null;
16+
},
17+
globalModulePaths: [/\/\w+(?!\.module)\.css$/]
18+
}),
19+
cssnano({
20+
preset: [
21+
'default',
22+
{
23+
mergeLonghand: false, // https://github.com/cssnano/cssnano/issues/675
24+
mergeRules: false // https://github.com/cssnano/cssnano/issues/730
25+
}
26+
]
27+
}),
28+
postcssCSStoESM({ toReplace: 'src', packageName })
29+
]
30+
};

packages/main/src/components/AnalyticalCard/AnalyticalCard.jss.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.content {
2+
display: flex;
3+
flex: 1 1 auto;
4+
overflow: hidden;
5+
position: relative;
6+
}

packages/main/src/components/AnalyticalCard/index.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
'use client';
22

3+
import { useStylesheet } from '@ui5/webcomponents-react-base';
34
import type { ReactNode } from 'react';
45
import React, { forwardRef } from 'react';
5-
import { createUseStyles } from 'react-jss';
66
import type { CommonProps } from '../../interfaces/index.js';
77
import type { CardDomRef } from '../../webComponents/index.js';
88
import { Card } from '../../webComponents/index.js';
9-
import styles from './AnalyticalCard.jss.js';
9+
import { classNames, styleData } from './AnalyticalCard.module.css.js';
1010

1111
export interface AnalyticalCardPropTypes extends CommonProps {
1212
/**
@@ -19,18 +19,18 @@ export interface AnalyticalCardPropTypes extends CommonProps {
1919
children: ReactNode | ReactNode[];
2020
}
2121

22-
const useStyles = createUseStyles(styles, { name: 'AnalyticalCard' });
2322
/**
2423
* The `AnalyticalCard` is mainly used for data visualization. It consists of two areas – a header area and a chart area with a visual representation of the data.<br />
2524
*/
2625
const AnalyticalCard = forwardRef<CardDomRef, AnalyticalCardPropTypes>((props, ref) => {
2726
const { children, header, ...rest } = props;
28-
const classes = useStyles();
27+
28+
useStylesheet(styleData, AnalyticalCard.displayName);
2929

3030
return (
3131
<Card ref={ref} {...rest}>
3232
{header}
33-
<div className={classes.content} role="group">
33+
<div className={classNames.content} role="group">
3434
{children}
3535
</div>
3636
</Card>

scripts/postcss-css-to-esm.cjs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// copied from https://github.com/SAP/ui5-webcomponents/blob/main/packages/tools/lib/postcss-css-to-esm/index.js
2+
const fs = require('node:fs');
3+
const path = require('node:path');
4+
const { basename } = require('node:path');
5+
6+
const getTSContent = (targetFile, packageName, css, exportTokens) => {
7+
const typeImport = `import type { StyleDataCSP } from '@ui5/webcomponents-base/dist/types.js';`;
8+
9+
// tabs are intentionally mixed to have proper identation in the produced file
10+
return `${typeImport}
11+
export const styleData: StyleDataCSP = {packageName:'${packageName}',fileName:'${basename(targetFile)}',content:${css}};
12+
13+
export const classNames = ${JSON.stringify(exportTokens)} as const;
14+
`;
15+
};
16+
17+
const proccessCSS = (css) => {
18+
css = css.replace(/\.sapThemeMeta[\s\S]*?:root/, ':root');
19+
css = css.replace(/\.background-image.*{.*}/, '');
20+
css = css.replace(/\.sapContrast[ ]*:root[\s\S]*?}/, '');
21+
css = css.replace(/--sapFontUrl.*\);?/, '');
22+
return JSON.stringify(css);
23+
};
24+
25+
module.exports = function (opts) {
26+
opts = opts || {};
27+
28+
const packageName = opts.packageName;
29+
const toReplace = opts.toReplace;
30+
31+
return {
32+
postcssPlugin: 'postcss-css-to-esm',
33+
OnceExit(root, { result }) {
34+
const css = proccessCSS(root.toString());
35+
const { exportTokens } = result.messages.find(
36+
(message) => message.type === 'export' && message.plugin === 'postcss-modules'
37+
);
38+
39+
const targetFile = root.source.input.from
40+
.replace(`/${toReplace}/`, '/src/')
41+
.replace(`\\${toReplace}\\`, '\\src\\');
42+
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
43+
44+
const filePath = `${targetFile}.ts`;
45+
46+
// it seems slower to read the old content, but writing the same content with no real changes
47+
// (as in initial build and then watch mode) will cause an unnecessary dev server refresh
48+
let oldContent = '';
49+
if (fs.existsSync(filePath)) {
50+
oldContent = fs.readFileSync(filePath).toString();
51+
}
52+
53+
const content = getTSContent(targetFile, packageName, css, exportTokens);
54+
if (content !== oldContent) {
55+
fs.writeFileSync(filePath, content);
56+
}
57+
}
58+
};
59+
};
60+
module.exports.postcss = true;

0 commit comments

Comments
 (0)