Skip to content
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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

```ts

import { makeResetStyles } from '@griffel/react';
import type { GriffelStyle } from '@griffel/react';
import { makeStyles } from '@griffel/react';
import { makeStyles as makeStylesCore } from '@griffel/core';
import { mergeClasses } from '@griffel/react';
Expand All @@ -16,12 +16,15 @@ import type { Theme } from '@fluentui/react-theme';
import { useRenderer_unstable } from '@griffel/react';

// @public (undocumented)
export const getStylesFromClassName: (className: string) => undefined;
export const getStylesFromClassName: (className: string) => {
[key: string]: unknown;
}[];

// @public (undocumented)
export const jsxPlatformAdapter: (reactJsx: JSXRuntime) => JSXRuntime;

export { makeResetStyles }
// @public (undocumented)
export const makeResetStyles: (resetStyles: GriffelStyle) => () => string;

export { makeStyles }

Expand Down

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
Expand Up @@ -12,6 +12,6 @@ export const registerStyles = (styles: Record<string, StyleXStyle>) => {
Object.assign(classNameMap, css.create(styles));
};

export const getStylesFromClassName = (className: string) => {
export const getStylesFromClassName = (className: string): StyleXStyle[] => {
return className.split(' ').map(c => classNameMap[c]);
};
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;

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.

};

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]);
}

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ export function makeStyles<Slots extends string | number>(
classNameMap[slotName] = className;
}

let registered = false;
const useStyles = () => {
if (!registered) {
registerStyles(styles);
registered = true;
}
return classNameMap;
};

registerStyles(styles);

return useStyles;
}
Loading