The header size calculation weren't taking into account cases where the value was a CSS variable. This could lead to situation where the outputted property was invalid. This update resolves the CSS property and then tries to calculate again using that value

Signed-off-by: Alex Lorenzi <alorenzi@spotify.com>
This commit is contained in:
Alex Lorenzi
2024-07-30 14:52:09 -04:00
parent 7b0f0224f9
commit dd0ce5a890
3 changed files with 156 additions and 57 deletions
@@ -30,6 +30,8 @@ type TypographyHeadings = Pick<
type TypographyHeadingsKeys = keyof TypographyHeadings;
const headings: TypographyHeadingsKeys[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
const relativeLengthUnit: RegExp = /(em)|(rem)/gi;
const cssVariable: RegExp = /var\(|\)/gi;
export default ({ theme }: RuleOptions) => `
/*================== Typeset ==================*/
@@ -43,17 +45,27 @@ ${headings.reduce<string>((style, heading) => {
(theme.typography as BackstageTypography).htmlFontSize ?? 16;
const styles = theme.typography[heading];
const { lineHeight, fontFamily, fontWeight, fontSize } = styles;
const calculate = (value: typeof fontSize) => {
let factor: number | string = 1;
const calculate = (value: typeof fontSize): string | undefined => {
if (typeof value === 'number') {
// convert px to rem
// 60% of the size defined because it is too big
factor = (value / htmlFontSize) * 0.6;
// Convert px to rem and apply 60% factor
return calculate(`${(value / htmlFontSize) * 0.6}rem`);
} else if (typeof value === 'string') {
if (value.match(cssVariable)) {
// Resolve css variable and calculate recursively
const resolvedValue = window
.getComputedStyle(document.body)
.getPropertyValue(value.replaceAll(cssVariable, ''));
if (resolvedValue !== '') {
return calculate(resolvedValue);
}
} else if (value.match(relativeLengthUnit)) {
// Use relative size as factor
const factor = value.replace(relativeLengthUnit, '');
return `calc(${factor} * var(--md-typeset-font-size))`;
}
}
if (typeof value === 'string') {
factor = value.replace('rem', '');
}
return `calc(${factor} * var(--md-typeset-font-size))`;
// Value is not a number, relative length unit, or CSS variable, return as is
return value;
};
return style.concat(`
.md-typeset ${heading} {
@@ -1,48 +0,0 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { renderHook } from '@testing-library/react';
import { useStylesTransformer } from './transformer';
describe('Transformers > Styles', () => {
it('should return a function that injects all styles into a given dom element', () => {
const { result } = renderHook(() => useStylesTransformer());
const dom = document.createElement('html');
dom.innerHTML = '<head></head>';
result.current(dom); // calling styles transformer
const style = dom.querySelector('head > style');
expect(style).toHaveTextContent(
'/*================== Variables ==================*/',
);
expect(style).toHaveTextContent(
'/*================== Reset ==================*/',
);
expect(style).toHaveTextContent(
'/*================== Layout ==================*/',
);
expect(style).toHaveTextContent(
'/*================== Typeset ==================*/',
);
expect(style).toHaveTextContent(
'/*================== Animations ==================*/',
);
expect(style).toHaveTextContent(
'/*================== Extensions ==================*/',
);
});
});
@@ -0,0 +1,135 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { renderHook } from '@testing-library/react';
import { useStylesTransformer } from './transformer';
import { createTheme, ThemeProvider } from '@material-ui/core/styles';
import React from 'react';
describe('Transformers > Styles', () => {
it('should return a function that injects all styles into a given dom element', () => {
const { result } = renderHook(() => useStylesTransformer());
const dom = document.createElement('html');
dom.innerHTML = '<head></head>';
result.current(dom); // calling styles transformer
const style = dom.querySelector('head > style');
expect(style).toHaveTextContent(
'/*================== Variables ==================*/',
);
expect(style).toHaveTextContent(
'/*================== Reset ==================*/',
);
expect(style).toHaveTextContent(
'/*================== Layout ==================*/',
);
expect(style).toHaveTextContent(
'/*================== Typeset ==================*/',
);
expect(style).toHaveTextContent(
'/*================== Animations ==================*/',
);
expect(style).toHaveTextContent(
'/*================== Extensions ==================*/',
);
});
it('should use relative header sizes as the factor the md-typeset variable', () => {
const theme = createTheme({
typography: {
h1: {
fontSize: '20rem',
},
},
});
const wrapper = ({ children }: { children: React.ReactNode }) => (
<ThemeProvider theme={theme}>{children}</ThemeProvider>
);
const { result } = renderHook(() => useStylesTransformer(), { wrapper });
const dom = document.createElement('html');
dom.innerHTML = `<head></head>`;
result.current(dom); // calling styles transformer
const style = dom.querySelector('head > style');
expect(style).not.toBeNull();
const h1 = style!.textContent?.match(/\.md-typeset h1 {.*?}/s);
expect(h1).toHaveLength(1);
expect(h1![0]).toContain(
'font-size: calc(20 * var(--md-typeset-font-size));',
);
});
it('should resolve header sizes that are variables', () => {
document.body.style.setProperty('--font-size-h1', '20rem');
const theme = createTheme({
typography: {
h1: {
fontSize: 'var(--font-size-h1)',
},
},
});
const wrapper = ({ children }: { children: React.ReactNode }) => (
<ThemeProvider theme={theme}>{children}</ThemeProvider>
);
const { result } = renderHook(() => useStylesTransformer(), { wrapper });
const dom = document.createElement('html');
dom.innerHTML = `<head></head>`;
result.current(dom); // calling styles transformer
const style = dom.querySelector('head > style');
expect(style).not.toBeNull();
const h1 = style!.textContent?.match(/\.md-typeset h1 {.*?}/s);
expect(h1).toHaveLength(1);
expect(h1![0]).toContain(
'font-size: calc(20 * var(--md-typeset-font-size));',
);
});
it('should convert pixel header sizes to REM and reduce by 60%', () => {
const theme = createTheme({
typography: {
htmlFontSize: 16,
h1: {
fontSize: 100,
},
},
});
const wrapper = ({ children }: { children: React.ReactNode }) => (
<ThemeProvider theme={theme}>{children}</ThemeProvider>
);
const { result } = renderHook(() => useStylesTransformer(), { wrapper });
const dom = document.createElement('html');
dom.innerHTML = `<head></head>`;
result.current(dom); // calling styles transformer
const style = dom.querySelector('head > style');
expect(style).not.toBeNull();
const h1 = style!.textContent?.match(/\.md-typeset h1 {.*?}/s);
expect(h1).toHaveLength(1);
expect(h1![0]).toContain(
// 100px / 16px * 0.6 = 3.75rem
'font-size: calc(3.75 * var(--md-typeset-font-size));',
);
});
});