diff --git a/change/@griffel-core-514d4515-c4ba-47ce-81d3-9c6c3426e82e.json b/change/@griffel-core-514d4515-c4ba-47ce-81d3-9c6c3426e82e.json new file mode 100644 index 000000000..606743461 --- /dev/null +++ b/change/@griffel-core-514d4515-c4ba-47ce-81d3-9c6c3426e82e.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix(core): trim \">\" selector", + "packageName": "@griffel/core", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/core/src/runtime/resolveStyleRules.test.ts b/packages/core/src/runtime/resolveStyleRules.test.ts index 64a35bd5b..153b3e01e 100644 --- a/packages/core/src/runtime/resolveStyleRules.test.ts +++ b/packages/core/src/runtime/resolveStyleRules.test.ts @@ -121,6 +121,29 @@ describe('resolveStyleRules', () => { `); }); + it('trims ">" selectors to generate the same classes', () => { + const resultA = resolveStyleRules({ '> div': { color: 'blue' } }); + const resultB = resolveStyleRules({ '>div': { color: 'blue' } }); + + expect(resultA[0]).toEqual(resultB[0]); + expect(resultA[0]).toMatchInlineSnapshot(` + Object { + "B9q554f": "f1plvi8r", + } + `); + + expect(resultA).toMatchInlineSnapshot(` + .f1plvi8r > div { + color: blue; + } + `); + expect(resultB).toMatchInlineSnapshot(` + .f1plvi8r > div { + color: blue; + } + `); + }); + it('hyphenates camelcased CSS properties', () => { expect( resolveStyleRules({ @@ -362,7 +385,7 @@ describe('resolveStyleRules', () => { `); expect(resolveStyleRules({ '> div': { color: 'green' } })).toMatchInlineSnapshot(` - .f18wx08q > div { + .f1fdorc0 > div { color: green; } `); @@ -392,7 +415,7 @@ describe('resolveStyleRules', () => { it('handles complex nested selectors', () => { expect(resolveStyleRules({ '& > :first-child': { '& svg': { color: 'red' } } })).toMatchInlineSnapshot(` - .fxfx2ih > :first-child svg { + .fkngkdt > :first-child svg { color: red; } `); diff --git a/packages/core/src/runtime/resolveStyleRules.ts b/packages/core/src/runtime/resolveStyleRules.ts index cb32aa70c..21dabbce9 100644 --- a/packages/core/src/runtime/resolveStyleRules.ts +++ b/packages/core/src/runtime/resolveStyleRules.ts @@ -17,6 +17,7 @@ import { isObject } from './utils/isObject'; import { getStyleBucketName } from './getStyleBucketName'; import { hashClassName } from './utils/hashClassName'; import { hashPropertyKey } from './utils/hashPropertyKey'; +import { trimSelector } from './utils/trimSelector'; import { warnAboutUnresolvedRule } from './warnings/warnAboutUnresolvedRule'; import { warnAboutUnsupportedProperties } from './warnings/warnAboutUnsupportedProperties'; @@ -92,15 +93,17 @@ export function resolveStyleRules( } if (typeof value === 'string' || typeof value === 'number') { + const selector = trimSelector(selectors.join('')); + // uniq key based on a hash of property & selector, used for merging later - const key = hashPropertyKey(selectors, container, media, support, property); + const key = hashPropertyKey(selector, container, media, support, property); const className = hashClassName({ container, media, layer, value: value.toString(), support, - selectors, + selector, property, }); @@ -112,7 +115,7 @@ export function resolveStyleRules( container, value: rtlDefinition.value.toString(), property: rtlDefinition.key, - selectors, + selector, media, layer, support, @@ -202,14 +205,16 @@ export function resolveStyleRules( continue; } - const key = hashPropertyKey(selectors, container, media, support, property); + const selector = trimSelector(selectors.join('')); + + const key = hashPropertyKey(selector, container, media, support, property); const className = hashClassName({ container, media, layer, value: value.map(v => (v ?? '').toString()).join(';'), support, - selectors, + selector, property, }); @@ -233,7 +238,7 @@ export function resolveStyleRules( container, value: rtlDefinitions.map(v => (v?.value ?? '').toString()).join(';'), property: rtlDefinitions[0].key, - selectors, + selector, layer, media, support, diff --git a/packages/core/src/runtime/utils/hashClassName.ts b/packages/core/src/runtime/utils/hashClassName.ts index 1866859ca..2675e6da0 100644 --- a/packages/core/src/runtime/utils/hashClassName.ts +++ b/packages/core/src/runtime/utils/hashClassName.ts @@ -4,7 +4,7 @@ import { HASH_PREFIX } from '../../constants'; interface HashedClassNameParts { property: string; value: string; - selectors: string[]; + selector: string; media: string; layer: string; support: string; @@ -16,12 +16,20 @@ export function hashClassName({ media, layer, property, - selectors, + selector, support, value, }: HashedClassNameParts): string { - // Trimming of value is required to generate consistent hashes - const classNameHash = hashString(selectors.join('') + container + media + layer + support + property + value.trim()); + const classNameHash = hashString( + selector + + container + + media + + layer + + support + + property + + // Trimming of value is required to generate consistent hashes + value.trim(), + ); return HASH_PREFIX + classNameHash; } diff --git a/packages/core/src/runtime/utils/hashPropertyKey.test.ts b/packages/core/src/runtime/utils/hashPropertyKey.test.ts index 173b0d3f6..f54f0b501 100644 --- a/packages/core/src/runtime/utils/hashPropertyKey.test.ts +++ b/packages/core/src/runtime/utils/hashPropertyKey.test.ts @@ -2,10 +2,10 @@ import { hashPropertyKey } from './hashPropertyKey'; describe('hashPropertyKey', () => { it('generates hashes that always start with letters', () => { - expect(hashPropertyKey([''], '', '', '', 'color')).toBe('sj55zd'); - expect(hashPropertyKey([''], '', '', '', 'display')).toBe('mc9l5x'); + expect(hashPropertyKey('', '', '', '', 'color')).toBe('sj55zd'); + expect(hashPropertyKey('', '', '', '', 'display')).toBe('mc9l5x'); - expect(hashPropertyKey([''], '', '', '', 'backgroundColor')).toBe('De3pzq'); - expect(hashPropertyKey([':hover'], '', '', '', 'color')).toBe('Bi91k9c'); + expect(hashPropertyKey('', '', '', '', 'backgroundColor')).toBe('De3pzq'); + expect(hashPropertyKey(':hover', '', '', '', 'color')).toBe('Bi91k9c'); }); }); diff --git a/packages/core/src/runtime/utils/hashPropertyKey.ts b/packages/core/src/runtime/utils/hashPropertyKey.ts index 22d60b505..c114c9353 100644 --- a/packages/core/src/runtime/utils/hashPropertyKey.ts +++ b/packages/core/src/runtime/utils/hashPropertyKey.ts @@ -2,14 +2,14 @@ import hash from '@emotion/hash'; import { PropertyHash } from '../../types'; export function hashPropertyKey( - selectors: string[], + selector: string, container: string, media: string, support: string, property: string, ): PropertyHash { // uniq key based on property & selector, used for merging later - const computedKey = selectors.join('') + container + media + support + property; + const computedKey = selector + container + media + support + property; // "key" can be really long as it includes selectors, we use hashes to reduce sizes of keys // ".foo :hover" => "abcd" @@ -25,7 +25,7 @@ export function hashPropertyKey( const startsWithNumber = firstCharCode >= 48 && firstCharCode <= 57; if (startsWithNumber) { - return String.fromCharCode(firstCharCode + 17) + hashedKey.substr(1); + return String.fromCharCode(firstCharCode + 17) + hashedKey.slice(1); } return hashedKey; diff --git a/packages/core/src/runtime/utils/trimSelector.test.ts b/packages/core/src/runtime/utils/trimSelector.test.ts new file mode 100644 index 000000000..a46f81006 --- /dev/null +++ b/packages/core/src/runtime/utils/trimSelector.test.ts @@ -0,0 +1,11 @@ +import { trimSelector } from './trimSelector'; + +describe('trimSelector', () => { + it('trims ">"', () => { + expect(trimSelector('>.foo')).toBe('>.foo'); + expect(trimSelector('> .foo')).toBe('>.foo'); + expect(trimSelector('> .foo')).toBe('>.foo'); + + expect(trimSelector('> .foo > .bar')).toBe('>.foo >.bar'); + }); +}); diff --git a/packages/core/src/runtime/utils/trimSelector.ts b/packages/core/src/runtime/utils/trimSelector.ts new file mode 100644 index 000000000..c69ad41fd --- /dev/null +++ b/packages/core/src/runtime/utils/trimSelector.ts @@ -0,0 +1,6 @@ +/** + * Trims selectors to generate consistent hashes. + */ +export function trimSelector(selector: string): string { + return selector.replace(/>\s+/g, '>'); +}