Skip to content

Commit

Permalink
fix: ensure that S2 style macro produces different class names for di…
Browse files Browse the repository at this point in the history
…fferent S2 themes
  • Loading branch information
bhovhannes committed Feb 24, 2025
1 parent a792c1a commit 0ab887a
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 5 deletions.
24 changes: 24 additions & 0 deletions packages/@react-spectrum/s2/style/__tests__/style-macro.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,30 @@ function testStyle(...args) {
}

describe('style-macro', () => {
it('should prefix rules with the hash of theme version', () => {
let {css, js} = testStyle({
alignItems: 'center',
color: 'red-400'
});
expect(css).toMatchInlineSnapshot(`
"@layer _.a, _.b;
@layer _.a {
.chc_2c {
align-items: center;
}
.chcaJ {
color: light-dark(rgb(255, 188, 180), rgb(115, 24, 11));
}
}
"
`);
expect(js).toMatchInlineSnapshot('" chc_2c chcaJ"');
});

it('should handle nested css conditions', () => {
let {css, js} = testStyle({
marginTop: {
Expand Down
2 changes: 2 additions & 0 deletions packages/@react-spectrum/s2/style/spectrum-theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {ArbitraryValue, CSSProperties, CSSValue, PropertyValueMap} from './types
import {autoStaticColor, colorScale, colorToken, fontSizeToken, generateOverlayColorScale, getToken, simpleColorScale, weirdColorToken} from './tokens' with {type: 'macro'};
import {Color, createArbitraryProperty, createColorProperty, createMappedProperty, createRenamedProperty, createSizingProperty, createTheme, parseArbitraryValue} from './style-macro';
import type * as CSS from 'csstype';
import {getThemeVersion} from './version';

interface MacroContext {
addAsset(asset: {type: string, content: string}): void
Expand Down Expand Up @@ -453,6 +454,7 @@ const fontSize = {
} as const;

export const style = createTheme({
version: getThemeVersion(),
properties: {
// colors
color: createColorProperty({
Expand Down
17 changes: 13 additions & 4 deletions packages/@react-spectrum/s2/style/style-macro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,11 @@ function mapConditionalShorthand<T, C extends string, R extends RenderProps<stri
}
}

function createValueLookup(values: Array<CSSValue>, atStart = false) {
function createValueLookup(values: Array<CSSValue>, atStart = false, prefix = '') {
let map = new Map<CSSValue, string>();
for (let value of values) {
if (!map.has(value)) {
map.set(value, generateName(map.size, atStart));
map.set(value, prefix + generateName(map.size, atStart));
}
}
return map;
Expand All @@ -138,8 +138,9 @@ interface MacroContext {
}

export function createTheme<T extends Theme>(theme: T): StyleFunction<ThemeProperties<T>, 'default' | Extract<keyof T['conditions'], string>> {
let themePropertyMap = createValueLookup(Object.keys(theme.properties), true);
let themeConditionMap = createValueLookup(Object.keys(theme.conditions), true);
let themePropertyMap = createValueLookup(Object.keys(theme.properties), true, theme.version);
let themeConditionMap = createValueLookup(Object.keys(theme.conditions), true, theme.version);

let propertyFunctions = new Map(Object.entries(theme.properties).map(([k, v]) => {
if (typeof v === 'function') {
return [k, v];
Expand Down Expand Up @@ -425,6 +426,14 @@ export function createTheme<T extends Theme>(theme: T): StyleFunction<ThemePrope
// 1. Property. For custom properties we use a hash. For theme properties, we use the index within the theme.
// 2. Conditions. This uses the index within the theme.
// 3. Value. The index in the theme, or a hash for arbitrary values.
// let propertyPrefix = property.startsWith('--') ? generateName(themePropertyMap.get(themeProperty)!.length, true) : themePropertyMap.get(themeProperty)!;
// let conditionsString = Array.from(conditions).sort().join(',');
// let valueString = typeof value === 'string' ? value : JSON.stringify(value);
// let hashInput = `${conditionsString}-${valueString}`;
// let hashValue = hash(hashInput).toString(36);
// let className = `${propertyPrefix}-${hashValue}`;
// let prelude = `.${className}`;

let prelude = '.';
if (property.startsWith('--')) {
// Include both custom property name and theme property in case the same var is reused between multiple theme properties.
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-spectrum/s2/style/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type PropertyFunction<T> = (value: T, property: string) => PropertyValueD
export type ShorthandProperty<T> = (value: T) => {[name: string]: Value};

export interface Theme {
version?: string,
properties: {
[name: string]: PropertyValueMap | PropertyFunction<any> | string[]
},
Expand All @@ -53,7 +54,7 @@ type PropertyValue<T> =
export type ArbitraryValue = CustomProperty | `[${string}]`;
type PropertyValue2<T> = PropertyValue<T> | ArbitraryValue;
type Merge<T> = T extends any ? T : never;
type ShorthandValue<T extends Theme, P> =
type ShorthandValue<T extends Theme, P> =
P extends string[]
? PropertyValue2<T['properties'][P[0]]>
: P extends ShorthandProperty<infer V>
Expand Down
32 changes: 32 additions & 0 deletions packages/@react-spectrum/s2/style/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import fs from 'node:fs';
import path from 'node:path';

export function getThemeVersion() {
let s2PkgPath = require.resolve('@react-spectrum/s2');
while (s2PkgPath.length > 2) {
const filePath = path.join(s2PkgPath, 'package.json');
try {
const stats = fs.statSync(filePath, {throwIfNoEntry: false});
if (stats?.isFile()) {
break;
}
} catch { /* empty */ }
s2PkgPath = path.dirname(s2PkgPath);
}
const s2PkgMetadata = require(path.join(s2PkgPath, 'package.json'));
return hashSemVer(s2PkgMetadata.version);
}

function hashSemVer(version: string) {
const alphabet = 'abcdefghijklmnopqrstuvwxyz';
const base = alphabet.length;
const [major, minor, patch] = version.split('.').map(Number);
const combined = (major << 16) | (minor << 8) | patch;
let hashStr = '';
let num = combined;
while (num > 0) {
hashStr = alphabet[num % base] + hashStr;
num = Math.floor(num / base);
}
return hashStr;
}

0 comments on commit 0ab887a

Please sign in to comment.