-
Notifications
You must be signed in to change notification settings - Fork 858
[POC] chore(eui): flag dynamic styles in emotion #8797
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { patchCacheForDynamicStyles } from './emotion_dynamic_patch'; | ||
|
||
describe('patchCacheForDynamicStyles', () => { | ||
function createMockCache() { | ||
const inserted: any[] = []; | ||
return { | ||
insert: jest.fn(function (selector, serialized, sheet, shouldCache) { | ||
inserted.push({ selector, serialized, sheet, shouldCache }); | ||
return undefined; | ||
}), | ||
inserted, | ||
}; | ||
} | ||
|
||
it('should not warn for static styles', () => { | ||
const cache: any = createMockCache(); | ||
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); | ||
patchCacheForDynamicStyles(cache); | ||
|
||
// Simulate two static style insertions with the same base class and hash | ||
cache.insert('.css-aaaaaa-euiPanel', { | ||
name: 'aaaaaa-euiPanel', | ||
styles: 'color:red;', | ||
}); | ||
cache.insert('.css-aaaaaa-euiPanel', { | ||
name: 'aaaaaa-euiPanel', | ||
styles: 'color:red;', | ||
}); | ||
|
||
expect(warnSpy).not.toHaveBeenCalled(); | ||
warnSpy.mockRestore(); | ||
}); | ||
|
||
it('should warn for dynamic styles (different hashes for same base)', () => { | ||
const cache: any = createMockCache(); | ||
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); | ||
patchCacheForDynamicStyles(cache); | ||
|
||
// Simulate two dynamic style insertions with different hashes but same base class | ||
cache.insert('.css-aaaaaa-euiPanel', { | ||
name: 'aaaaaa-euiPanel', | ||
styles: 'color:red;', | ||
}); | ||
cache.insert('.css-bbbbbb-euiPanel', { | ||
name: 'bbbbbb-euiPanel', | ||
styles: 'color:blue;', | ||
}); | ||
|
||
expect(warnSpy).toHaveBeenCalledWith( | ||
expect.stringContaining( | ||
'Dynamic style detected for base selector: euiPanel' | ||
) | ||
); | ||
warnSpy.mockRestore(); | ||
}); | ||
|
||
it('should handle selectors that do not match the pattern', () => { | ||
const cache: any = createMockCache(); | ||
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); | ||
patchCacheForDynamicStyles(cache); | ||
|
||
// Insert a selector that doesn't match the .css-xxxxxx-base pattern | ||
cache.insert('.not-css-selector', { | ||
name: 'notcssselector', | ||
styles: 'color:green;', | ||
}); | ||
|
||
expect(warnSpy).not.toHaveBeenCalled(); | ||
warnSpy.mockRestore(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { EmotionCache } from '@emotion/react'; | ||
|
||
/** | ||
* Applies a monkey patch to the given Emotion cache to flag dynamic styles. | ||
* This is modular and can be reused in other places. | ||
*/ | ||
export function patchCacheForDynamicStyles(cache: EmotionCache) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be a good place to use the decorator pattern |
||
if (!cache || typeof cache !== 'object' || (cache as any).__patched) return; | ||
|
||
const baseClassMap = new Map<string, Set<string>>(); | ||
|
||
/** | ||
* A utility to normalize selectors by removing the dynamic part | ||
* of the class name generated by Emotion. | ||
*/ | ||
function getBaseClass(selector: string): string | null { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please note that this implementation is configuration-sensitive and may break when a custom There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💭 The regex only works with the base cache setup, which is by default named as starting with "css". If a cache is created with createCache which requires a custom key (code) and that key is not "css" then this matching will not work. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, it is mentioned in one of the limitations in the description. The list of cache names could be limiting but is an idea we can explore. |
||
const match = selector.match(/\.css-[a-zA-Z0-9]+-(.+)/); | ||
return match ? match[1] : null; | ||
} | ||
|
||
/** | ||
* Checks for dynamic styles by tracking hashes for each base class. | ||
* If multiple hashes are seen for the same base class, a warning is logged. | ||
* Returns true if a dynamic style was flagged. | ||
*/ | ||
function flagDynamicStyles(selector: string, serialized: any): boolean { | ||
const baseClass = getBaseClass(selector); | ||
|
||
if (baseClass) { | ||
const hashes = baseClassMap.get(baseClass) || new Set<string>(); | ||
const hash = | ||
serialized && serialized.name ? serialized.name.split('-')[0] : null; | ||
|
||
if (hash) { | ||
hashes.add(hash); | ||
if (hashes.size > 1) { | ||
console.warn( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The warning should likely be emitted only once per style to reduce console spam in cases of frequently updated styles (e.g., related to scroll positions). |
||
`[EuiCacheProvider] Dynamic style detected for base selector: ${baseClass}. ` + | ||
'This usually means you are passing dynamic values to the `css` prop. ' + | ||
'Consider using the `style` prop for dynamic values instead.' | ||
); | ||
baseClassMap.set(baseClass, hashes); | ||
|
||
return true; | ||
} | ||
|
||
baseClassMap.set(baseClass, hashes); | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
const originalInsert = cache.insert; | ||
cache.insert = function (selector, serialized, sheet, shouldCache) { | ||
flagDynamicStyles(selector, serialized); | ||
return originalInsert.call(this, selector, serialized, sheet, shouldCache); | ||
}; | ||
(cache as any).__patched = true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need this flag at all? |
||
(cache as any).__baseClassMap = baseClassMap; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good example of a technically dynamic style that may or may not be harmful. If it changes too frequently, it might, but overall, this example is okay. It's more of a question of how we want users to define styles specific to component props (or other flags and settings). I usually prefer to follow BEM-like structure and treat props as modifiers to primary component styles, but that's not always handy - it all depends on the structure of the styles (e.g., if you need to style child classes you don't control, this might be your only solid solution).
Generally speaking, we can recommend that Kibana should use BEM-like modifier classes whenever dealing with props affecting component styles, but that's just internal. I wonder if it makes a lot of a difference considering the switch to
speedy
being enabled by default there (and possibly becoming the default for EUI soon).Do you know how does this detection work with Emotion's
cssPropOptimization
?[Slightly off-topic]
We need to benchmark if there's a difference between:
style
the normal waystyle
orsetProperty
to pass a local CSS variable to use in the regular styles and letting browsers optimize it to reduce the number of style calculations and paints (I don't think this will be as performant as it sounds, though)Without any data, I'd say that:
style
attribute updates will cause a rerender and repaint, similar toelement.style.setProperty