-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
chore(xplat): Add support for nested selectors to Griffel to StyleX converter #30791
Changes from all commits
fd5a166
c5dc4ed
86e54eb
008faf1
af8489d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import * as React from 'react'; | ||
|
||
import { html } from 'react-strict-dom'; | ||
import { getStylesFromClassName } from '../styling/classNameMap'; | ||
import type { JSXRuntime } from './types'; | ||
|
||
/** | ||
* Create a wrapper for React JSX that creates react-strict-dom elements for intrinsic elements. | ||
* | ||
* @param reactJsx The original JSX function to wrap from react/jsx-runtime | ||
*/ | ||
export const jsxPlatformAdapter = (reactJsx: JSXRuntime): JSXRuntime => { | ||
return <P extends {}>( | ||
type: React.ElementType<P>, | ||
props: (P & { children?: React.ReactNode; className?: string; style?: React.CSSProperties }) | null, | ||
key?: React.Key, | ||
source?: unknown, | ||
self?: unknown, | ||
) => { | ||
if (typeof type === 'string' && type in html) { | ||
if (props?.className) { | ||
props = { | ||
...props, | ||
style: [...getStylesFromClassName(props.className), props.style], | ||
}; | ||
|
||
delete props!.className; | ||
} | ||
|
||
// TODO figure out which types need to wrap children in a span. | ||
if (type !== 'span' && props?.children) { | ||
let modifiedChildren = false; | ||
const children = React.Children.map(props.children, child => { | ||
if (typeof child === 'string') { | ||
modifiedChildren = true; | ||
return <html.span>{child}</html.span>; | ||
} | ||
return child; | ||
}); | ||
if (modifiedChildren) { | ||
props = { ...props, children }; | ||
} | ||
} | ||
|
||
// TODO need to figure out proper type for indexing the `html` import. | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
return reactJsx((html as any)[type], props, key, source, self); | ||
} | ||
|
||
return reactJsx(type, props, key, source, self); | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
export const getStylesFromClassName = (className: string) => { | ||
return undefined; | ||
export const getStylesFromClassName = (className: string): { [key: string]: unknown }[] => { | ||
return []; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,85 +1,131 @@ | ||
import type { GriffelResetStyle, GriffelStyle } from '@griffel/core'; | ||
import type { GriffelStyle } from '@griffel/core'; | ||
import type { UserAuthoredStyles as StyleXStyle } from '@stylexjs/stylex/lib/StyleXTypes'; | ||
|
||
// Modifiable version of StyleXStyle | ||
type StyleXStyleBuilder = { | ||
-readonly [P in keyof StyleXStyle]: StyleXStyle[P]; | ||
}; | ||
|
||
type ComplexValueType = { | ||
default?: string | number; | ||
[pseudoClass: `:${string}`]: string | number; | ||
[atRule: `@${string}`]: string | number; | ||
type ValueWithSelectors = { | ||
default?: string | number | ValueWithSelectors | null; | ||
[pseudoClass: `:${string}`]: string | number | ValueWithSelectors; | ||
[atRule: `@${string}`]: string | number | ValueWithSelectors; | ||
}; | ||
|
||
function isComplexValueKey(key: string): key is keyof ComplexValueType { | ||
type Selector = keyof ValueWithSelectors; | ||
|
||
function isSelector(key: string): key is Selector { | ||
return key.startsWith(':') || key.startsWith('@') || key === 'default'; | ||
} | ||
|
||
function isComplexValue(value: StyleXStyle[keyof StyleXStyle]): value is ComplexValueType { | ||
return typeof value === 'object' && value !== null; | ||
function isValueWithSelectors(value: StyleXStyle[keyof StyleXStyle]): value is ValueWithSelectors { | ||
return typeof value === 'object' && value !== null && !Array.isArray(value); | ||
} | ||
|
||
function makeValueWithSelectors( | ||
selectors: Selector[], | ||
value: string | number, | ||
existingValue?: string | number | unknown | null | undefined, | ||
): string | number | ValueWithSelectors { | ||
if (selectors.length === 0) { | ||
return value; | ||
} | ||
|
||
const selector = selectors[0]; | ||
const nestedSelectors = selectors.slice(1); | ||
|
||
if (isValueWithSelectors(existingValue)) { | ||
existingValue[selector] = makeValueWithSelectors(nestedSelectors, value, existingValue[selector]); | ||
return existingValue; | ||
} | ||
|
||
return { | ||
default: existingValue ?? null, | ||
[selector]: makeValueWithSelectors(nestedSelectors, value), | ||
} as ValueWithSelectors; | ||
} | ||
|
||
function mergeSelectorValues( | ||
selector: keyof ComplexValueType, | ||
style: GriffelStyle | GriffelResetStyle, | ||
result: StyleXStyleBuilder, | ||
) { | ||
for (const key in style) { | ||
if (!Object.prototype.hasOwnProperty.call(style, key)) { | ||
/** | ||
* Converts from Griffel's selector format to StyleX's format. | ||
* | ||
* Griffel nests the values inside of the selectors: | ||
* { | ||
* color: 'red', | ||
* ':hover': { | ||
* color: 'green', | ||
* '@media (forced-colors: active)': { | ||
* color: 'highlighttext', | ||
* }, | ||
* }, | ||
* } | ||
* | ||
* StyleX nests the selectors inside of the values: | ||
* { | ||
* color: { | ||
* default: 'red', | ||
* ':hover': { | ||
* default: 'green', | ||
* '@media (forced-colors: active)': 'highlighttext', | ||
* }, | ||
* }, | ||
* } | ||
* | ||
* @param selector The selector, such as ':hover' or '@media ...' | ||
* @param griffelStyle The griffel styles within this selector. | ||
* @param strictDomStyle The react-strict-dom style to add the merged values to. | ||
*/ | ||
function mergeSelectorValues(selectors: Selector[], griffelStyle: GriffelStyle, strictDomStyle: StyleXStyleBuilder) { | ||
for (const key in griffelStyle) { | ||
if (!Object.prototype.hasOwnProperty.call(griffelStyle, key)) { | ||
continue; | ||
} | ||
|
||
const value = style[key as keyof GriffelStyle]; | ||
if (typeof value === 'string' || typeof value === 'number') { | ||
const property = key as keyof GriffelStyle & keyof StyleXStyle; | ||
const property = key as keyof GriffelStyle & keyof StyleXStyle; | ||
const value = griffelStyle[property]; | ||
|
||
// If there's already a value with another selector, add this to it | ||
const existingValue = result[property]; | ||
if (isComplexValue(existingValue)) { | ||
existingValue[selector] = value; | ||
} else { | ||
result[property] = { default: existingValue || null, [selector]: value }; | ||
} | ||
if (typeof value === 'string' || typeof value === 'number') { | ||
strictDomStyle[property] = makeValueWithSelectors(selectors, value, strictDomStyle[property]); | ||
} else if (Array.isArray(value)) { | ||
console.warn('Unsupported array value for ', key); | ||
} else if (isSelector(key) && isValueWithSelectors(value)) { | ||
mergeSelectorValues([...selectors, key], value, strictDomStyle); | ||
} else { | ||
throw new Error(`Unsupported nested selector: '${selector}' '${key}'`); | ||
console.warn(`Unsupported selector `, [...selectors, key]); | ||
} | ||
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. If I follow the logic now, we will emit warnings with unsupported selectors but not end up passing them to stylex. I think that is better than having the hard errors. 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. Yeah, we'll need a longer-term solution to catch and fix these cases at development time, like a lint rule or something. But for now, this at least makes it so it doesn't crash with an invalid selector. |
||
} | ||
} | ||
|
||
export const convertGriffelToStyleX = (style: GriffelStyle | GriffelResetStyle): StyleXStyle => { | ||
const result: StyleXStyleBuilder = {}; | ||
export const convertGriffelToStyleX = (griffelStyle: GriffelStyle): StyleXStyle => { | ||
const strictDomStyle: StyleXStyleBuilder = {}; | ||
|
||
for (const key in style) { | ||
if (!Object.prototype.hasOwnProperty.call(style, key)) { | ||
for (const key in griffelStyle) { | ||
if (!Object.prototype.hasOwnProperty.call(griffelStyle, key)) { | ||
continue; | ||
} | ||
|
||
const value = style[key as keyof GriffelStyle]; | ||
const property = key as keyof GriffelStyle & keyof StyleXStyle; | ||
|
||
const value = griffelStyle[property]; | ||
if (value === null) { | ||
continue; | ||
} | ||
|
||
if (typeof value === 'string' || typeof value === 'number') { | ||
const property = key as keyof GriffelStyle & keyof StyleXStyle; | ||
|
||
// If there's already a value with :hover or :active, add a `default` case | ||
const existingValue = result[property]; | ||
if (isComplexValue(existingValue)) { | ||
const existingValue = strictDomStyle[property]; | ||
if (isValueWithSelectors(existingValue)) { | ||
existingValue.default = value; | ||
} else { | ||
result[property] = value; | ||
strictDomStyle[property] = value; | ||
} | ||
} else if (Array.isArray(value)) { | ||
throw new Error(`Unsupported array value: '${key}'`); | ||
} else if (typeof value === 'object') { | ||
if (isComplexValueKey(key)) { | ||
mergeSelectorValues(key, value, result); | ||
} | ||
console.warn('Unsupported array value for ', key); | ||
} else if (isSelector(key) && isValueWithSelectors(value)) { | ||
mergeSelectorValues([key], value, strictDomStyle); | ||
} else { | ||
throw new Error(`Unsupported value: '${key}'`); | ||
console.warn(`Unsupported selector `, key); | ||
} | ||
} | ||
|
||
return result; | ||
return strictDomStyle; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,25 @@ | ||
import type { GriffelResetStyle } from '@griffel/core'; | ||
import type { GriffelStyle } from '@griffel/core'; | ||
import type { UserAuthoredStyles as StyleXStyle } from '@stylexjs/stylex/lib/StyleXTypes'; | ||
import { getUniqueClassName, registerStyles } from './classNameMap.native'; | ||
import { convertGriffelToStyleX } from './convertGriffelToStyleX.native'; | ||
|
||
export function makeResetStyles(resetStyles: GriffelResetStyle): () => string { | ||
// Note, the `resetStyles` param is of type `GriffelStyle` and not `GriffelResetStyle`, since | ||
// react-strict-dom does not support all shorthand properties. | ||
export function makeResetStyles(resetStyles: GriffelStyle): () => string { | ||
const styles: Record<string, StyleXStyle> = {}; | ||
|
||
const className = getUniqueClassName('resetStyles'); | ||
styles[className] = convertGriffelToStyleX(resetStyles); | ||
|
||
let registered = false; | ||
const useResetStyles = () => { | ||
if (!registered) { | ||
registerStyles(styles); | ||
registered = true; | ||
} | ||
|
||
return className; | ||
}; | ||
|
||
registerStyles(styles); | ||
|
||
return useResetStyles; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,6 @@ | ||
export { makeResetStyles } from '@griffel/react'; | ||
import type { GriffelStyle } from '@griffel/react'; | ||
import { makeResetStyles as makeResetStylesGriffel } from '@griffel/react'; | ||
|
||
// Note, the `resetStyles` param is of type `GriffelStyle` and not `GriffelResetStyle`, since | ||
// react-strict-dom does not support all shorthand properties. | ||
export const makeResetStyles = makeResetStylesGriffel as (resetStyles: GriffelStyle) => () => string; |
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.
Interesting, didn't realize index signatures could be added this way.