Skip to content

Commit 5139c70

Browse files
authored
chore(xplat): Add support for nested selectors to Griffel to StyleX converter (#30791)
1 parent 68debe6 commit 5139c70

10 files changed

+171
-92
lines changed

packages/react-components/react-platform-adapter/etc/react-platform-adapter.api.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
```ts
66

7-
import { makeResetStyles } from '@griffel/react';
7+
import type { GriffelStyle } from '@griffel/react';
88
import { makeStyles } from '@griffel/react';
99
import { makeStyles as makeStylesCore } from '@griffel/core';
1010
import { mergeClasses } from '@griffel/react';
@@ -16,12 +16,15 @@ import type { Theme } from '@fluentui/react-theme';
1616
import { useRenderer_unstable } from '@griffel/react';
1717

1818
// @public (undocumented)
19-
export const getStylesFromClassName: (className: string) => undefined;
19+
export const getStylesFromClassName: (className: string) => {
20+
[key: string]: unknown;
21+
}[];
2022

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

24-
export { makeResetStyles }
26+
// @public (undocumented)
27+
export const makeResetStyles: (resetStyles: GriffelStyle) => () => string;
2528

2629
export { makeStyles }
2730

packages/react-components/react-platform-adapter/src/jsx/jsxPlatformAdapter.native.ts

-36
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as React from 'react';
2+
3+
import { html } from 'react-strict-dom';
4+
import { getStylesFromClassName } from '../styling/classNameMap';
5+
import type { JSXRuntime } from './types';
6+
7+
/**
8+
* Create a wrapper for React JSX that creates react-strict-dom elements for intrinsic elements.
9+
*
10+
* @param reactJsx The original JSX function to wrap from react/jsx-runtime
11+
*/
12+
export const jsxPlatformAdapter = (reactJsx: JSXRuntime): JSXRuntime => {
13+
return <P extends {}>(
14+
type: React.ElementType<P>,
15+
props: (P & { children?: React.ReactNode; className?: string; style?: React.CSSProperties }) | null,
16+
key?: React.Key,
17+
source?: unknown,
18+
self?: unknown,
19+
) => {
20+
if (typeof type === 'string' && type in html) {
21+
if (props?.className) {
22+
props = {
23+
...props,
24+
style: [...getStylesFromClassName(props.className), props.style],
25+
};
26+
27+
delete props!.className;
28+
}
29+
30+
// TODO figure out which types need to wrap children in a span.
31+
if (type !== 'span' && props?.children) {
32+
let modifiedChildren = false;
33+
const children = React.Children.map(props.children, child => {
34+
if (typeof child === 'string') {
35+
modifiedChildren = true;
36+
return <html.span>{child}</html.span>;
37+
}
38+
return child;
39+
});
40+
if (modifiedChildren) {
41+
props = { ...props, children };
42+
}
43+
}
44+
45+
// TODO need to figure out proper type for indexing the `html` import.
46+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
47+
return reactJsx((html as any)[type], props, key, source, self);
48+
}
49+
50+
return reactJsx(type, props, key, source, self);
51+
};
52+
};

packages/react-components/react-platform-adapter/src/styling/classNameMap.native.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ export const registerStyles = (styles: Record<string, StyleXStyle>) => {
1212
Object.assign(classNameMap, css.create(styles));
1313
};
1414

15-
export const getStylesFromClassName = (className: string) => {
15+
export const getStylesFromClassName = (className: string): StyleXStyle[] => {
1616
return className.split(' ').map(c => classNameMap[c]);
1717
};
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export const getStylesFromClassName = (className: string) => {
2-
return undefined;
1+
export const getStylesFromClassName = (className: string): { [key: string]: unknown }[] => {
2+
return [];
33
};
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,131 @@
1-
import type { GriffelResetStyle, GriffelStyle } from '@griffel/core';
1+
import type { GriffelStyle } from '@griffel/core';
22
import type { UserAuthoredStyles as StyleXStyle } from '@stylexjs/stylex/lib/StyleXTypes';
33

44
// Modifiable version of StyleXStyle
55
type StyleXStyleBuilder = {
66
-readonly [P in keyof StyleXStyle]: StyleXStyle[P];
77
};
88

9-
type ComplexValueType = {
10-
default?: string | number;
11-
[pseudoClass: `:${string}`]: string | number;
12-
[atRule: `@${string}`]: string | number;
9+
type ValueWithSelectors = {
10+
default?: string | number | ValueWithSelectors | null;
11+
[pseudoClass: `:${string}`]: string | number | ValueWithSelectors;
12+
[atRule: `@${string}`]: string | number | ValueWithSelectors;
1313
};
1414

15-
function isComplexValueKey(key: string): key is keyof ComplexValueType {
15+
type Selector = keyof ValueWithSelectors;
16+
17+
function isSelector(key: string): key is Selector {
1618
return key.startsWith(':') || key.startsWith('@') || key === 'default';
1719
}
1820

19-
function isComplexValue(value: StyleXStyle[keyof StyleXStyle]): value is ComplexValueType {
20-
return typeof value === 'object' && value !== null;
21+
function isValueWithSelectors(value: StyleXStyle[keyof StyleXStyle]): value is ValueWithSelectors {
22+
return typeof value === 'object' && value !== null && !Array.isArray(value);
23+
}
24+
25+
function makeValueWithSelectors(
26+
selectors: Selector[],
27+
value: string | number,
28+
existingValue?: string | number | unknown | null | undefined,
29+
): string | number | ValueWithSelectors {
30+
if (selectors.length === 0) {
31+
return value;
32+
}
33+
34+
const selector = selectors[0];
35+
const nestedSelectors = selectors.slice(1);
36+
37+
if (isValueWithSelectors(existingValue)) {
38+
existingValue[selector] = makeValueWithSelectors(nestedSelectors, value, existingValue[selector]);
39+
return existingValue;
40+
}
41+
42+
return {
43+
default: existingValue ?? null,
44+
[selector]: makeValueWithSelectors(nestedSelectors, value),
45+
} as ValueWithSelectors;
2146
}
2247

23-
function mergeSelectorValues(
24-
selector: keyof ComplexValueType,
25-
style: GriffelStyle | GriffelResetStyle,
26-
result: StyleXStyleBuilder,
27-
) {
28-
for (const key in style) {
29-
if (!Object.prototype.hasOwnProperty.call(style, key)) {
48+
/**
49+
* Converts from Griffel's selector format to StyleX's format.
50+
*
51+
* Griffel nests the values inside of the selectors:
52+
* {
53+
* color: 'red',
54+
* ':hover': {
55+
* color: 'green',
56+
* '@media (forced-colors: active)': {
57+
* color: 'highlighttext',
58+
* },
59+
* },
60+
* }
61+
*
62+
* StyleX nests the selectors inside of the values:
63+
* {
64+
* color: {
65+
* default: 'red',
66+
* ':hover': {
67+
* default: 'green',
68+
* '@media (forced-colors: active)': 'highlighttext',
69+
* },
70+
* },
71+
* }
72+
*
73+
* @param selector The selector, such as ':hover' or '@media ...'
74+
* @param griffelStyle The griffel styles within this selector.
75+
* @param strictDomStyle The react-strict-dom style to add the merged values to.
76+
*/
77+
function mergeSelectorValues(selectors: Selector[], griffelStyle: GriffelStyle, strictDomStyle: StyleXStyleBuilder) {
78+
for (const key in griffelStyle) {
79+
if (!Object.prototype.hasOwnProperty.call(griffelStyle, key)) {
3080
continue;
3181
}
3282

33-
const value = style[key as keyof GriffelStyle];
34-
if (typeof value === 'string' || typeof value === 'number') {
35-
const property = key as keyof GriffelStyle & keyof StyleXStyle;
83+
const property = key as keyof GriffelStyle & keyof StyleXStyle;
84+
const value = griffelStyle[property];
3685

37-
// If there's already a value with another selector, add this to it
38-
const existingValue = result[property];
39-
if (isComplexValue(existingValue)) {
40-
existingValue[selector] = value;
41-
} else {
42-
result[property] = { default: existingValue || null, [selector]: value };
43-
}
86+
if (typeof value === 'string' || typeof value === 'number') {
87+
strictDomStyle[property] = makeValueWithSelectors(selectors, value, strictDomStyle[property]);
88+
} else if (Array.isArray(value)) {
89+
console.warn('Unsupported array value for ', key);
90+
} else if (isSelector(key) && isValueWithSelectors(value)) {
91+
mergeSelectorValues([...selectors, key], value, strictDomStyle);
4492
} else {
45-
throw new Error(`Unsupported nested selector: '${selector}' '${key}'`);
93+
console.warn(`Unsupported selector `, [...selectors, key]);
4694
}
4795
}
4896
}
4997

50-
export const convertGriffelToStyleX = (style: GriffelStyle | GriffelResetStyle): StyleXStyle => {
51-
const result: StyleXStyleBuilder = {};
98+
export const convertGriffelToStyleX = (griffelStyle: GriffelStyle): StyleXStyle => {
99+
const strictDomStyle: StyleXStyleBuilder = {};
52100

53-
for (const key in style) {
54-
if (!Object.prototype.hasOwnProperty.call(style, key)) {
101+
for (const key in griffelStyle) {
102+
if (!Object.prototype.hasOwnProperty.call(griffelStyle, key)) {
55103
continue;
56104
}
57105

58-
const value = style[key as keyof GriffelStyle];
106+
const property = key as keyof GriffelStyle & keyof StyleXStyle;
107+
108+
const value = griffelStyle[property];
59109
if (value === null) {
60110
continue;
61111
}
62112

63113
if (typeof value === 'string' || typeof value === 'number') {
64-
const property = key as keyof GriffelStyle & keyof StyleXStyle;
65-
66114
// If there's already a value with :hover or :active, add a `default` case
67-
const existingValue = result[property];
68-
if (isComplexValue(existingValue)) {
115+
const existingValue = strictDomStyle[property];
116+
if (isValueWithSelectors(existingValue)) {
69117
existingValue.default = value;
70118
} else {
71-
result[property] = value;
119+
strictDomStyle[property] = value;
72120
}
73121
} else if (Array.isArray(value)) {
74-
throw new Error(`Unsupported array value: '${key}'`);
75-
} else if (typeof value === 'object') {
76-
if (isComplexValueKey(key)) {
77-
mergeSelectorValues(key, value, result);
78-
}
122+
console.warn('Unsupported array value for ', key);
123+
} else if (isSelector(key) && isValueWithSelectors(value)) {
124+
mergeSelectorValues([key], value, strictDomStyle);
79125
} else {
80-
throw new Error(`Unsupported value: '${key}'`);
126+
console.warn(`Unsupported selector `, key);
81127
}
82128
}
83129

84-
return result;
130+
return strictDomStyle;
85131
};
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
1-
import type { GriffelResetStyle } from '@griffel/core';
1+
import type { GriffelStyle } from '@griffel/core';
22
import type { UserAuthoredStyles as StyleXStyle } from '@stylexjs/stylex/lib/StyleXTypes';
33
import { getUniqueClassName, registerStyles } from './classNameMap.native';
44
import { convertGriffelToStyleX } from './convertGriffelToStyleX.native';
55

6-
export function makeResetStyles(resetStyles: GriffelResetStyle): () => string {
6+
// Note, the `resetStyles` param is of type `GriffelStyle` and not `GriffelResetStyle`, since
7+
// react-strict-dom does not support all shorthand properties.
8+
export function makeResetStyles(resetStyles: GriffelStyle): () => string {
79
const styles: Record<string, StyleXStyle> = {};
810

911
const className = getUniqueClassName('resetStyles');
1012
styles[className] = convertGriffelToStyleX(resetStyles);
1113

14+
let registered = false;
1215
const useResetStyles = () => {
16+
if (!registered) {
17+
registerStyles(styles);
18+
registered = true;
19+
}
20+
1321
return className;
1422
};
1523

16-
registerStyles(styles);
17-
1824
return useResetStyles;
1925
}
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
export { makeResetStyles } from '@griffel/react';
1+
import type { GriffelStyle } from '@griffel/react';
2+
import { makeResetStyles as makeResetStylesGriffel } from '@griffel/react';
3+
4+
// Note, the `resetStyles` param is of type `GriffelStyle` and not `GriffelResetStyle`, since
5+
// react-strict-dom does not support all shorthand properties.
6+
export const makeResetStyles = makeResetStylesGriffel as (resetStyles: GriffelStyle) => () => string;

packages/react-components/react-platform-adapter/src/styling/makeStyles.native.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ export function makeStyles<Slots extends string | number>(
1919
classNameMap[slotName] = className;
2020
}
2121

22+
let registered = false;
2223
const useStyles = () => {
24+
if (!registered) {
25+
registerStyles(styles);
26+
registered = true;
27+
}
2328
return classNameMap;
2429
};
2530

26-
registerStyles(styles);
27-
2831
return useStyles;
2932
}

0 commit comments

Comments
 (0)