fix: stop leaking CSS rules from dynamic UI styles (#1084) (#1103)

* fix: stop leaking CSS rules from dynamic position styles

* Fix floating surfaces on web

* Stop diff controls growing Unistyles rules

* Use inline style escape hatch in diff pane

* Centralize dynamic style escape hatches

* Restore startup workspace navigation

* Upgrade Unistyles for web cleanup fix

* Update review style test for inline styles

* Use redirect for startup workspace restore

* Update startup redirect test
This commit is contained in:
Mohamed Boudra
2026-05-20 16:55:00 +08:00
committed by GitHub
parent 905a0985f9
commit f933bf6b3a
18 changed files with 366 additions and 186 deletions
+12 -9
View File
@@ -20,6 +20,8 @@ import {
useWindowDimensions,
type LayoutChangeEvent,
type PressableStateCallbackType,
type StyleProp,
type ViewStyle,
} from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useIsCompactFormFactor } from "@/constants/layout";
@@ -56,6 +58,7 @@ import {
SheetHeaderView,
type SheetHeader,
} from "@/components/adaptive-modal-sheet";
import { FloatingSurface } from "@/components/ui/floating";
const IS_WEB = isWeb;
@@ -858,7 +861,7 @@ interface DesktopContainerStyleInput {
availableHeight: number | undefined;
}
function buildDesktopContainerStyle(input: DesktopContainerStyleInput) {
function buildDesktopFrameStyle(input: DesktopContainerStyleInput): StyleProp<ViewStyle> {
const {
desktopMinWidth,
referenceWidth,
@@ -877,7 +880,6 @@ function buildDesktopContainerStyle(input: DesktopContainerStyleInput) {
? { maxHeight: Math.min(availableHeight, desktopFixedHeight ?? 400) }
: null;
return [
styles.desktopContainer,
{
position: "absolute" as const,
minWidth: desktopMinWidth ?? referenceWidth ?? 200,
@@ -1070,7 +1072,7 @@ interface DesktopBodyProps {
handleClose: () => void;
refs: ReturnType<typeof useFloating>["refs"];
shouldUseDesktopFade: boolean;
desktopContainerStyle: unknown;
desktopFrameStyle: StyleProp<ViewStyle>;
handleDesktopContentLayout: (event: LayoutChangeEvent) => void;
header: SheetHeader | undefined;
stickyHeader: ReactNode;
@@ -1191,11 +1193,12 @@ function DesktopComboboxBody(props: DesktopBodyProps): ReactElement {
>
<View ref={props.refs.setOffsetParent} collapsable={false} style={styles.desktopOverlay}>
<Pressable style={styles.desktopBackdrop} onPress={props.handleClose} />
<Animated.View
<FloatingSurface
testID="combobox-desktop-container"
entering={props.shouldUseDesktopFade ? FadeIn.duration(100) : undefined}
exiting={props.shouldUseDesktopFade ? FadeOut.duration(100) : undefined}
style={props.desktopContainerStyle as never}
style={styles.desktopContainer}
frameStyle={props.desktopFrameStyle}
ref={props.refs.setFloating}
collapsable={false}
onLayout={props.handleDesktopContentLayout}
@@ -1227,7 +1230,7 @@ function DesktopComboboxBody(props: DesktopBodyProps): ReactElement {
renderOption={props.renderOption}
/>
)}
</Animated.View>
</FloatingSurface>
</View>
</Modal>
);
@@ -1492,9 +1495,9 @@ export function Combobox({
[theme.colors.palette.zinc],
);
const desktopContainerStyle = useMemo(
const desktopFrameStyle = useMemo(
() =>
buildDesktopContainerStyle({
buildDesktopFrameStyle({
desktopMinWidth,
referenceWidth,
desktopFixedHeight,
@@ -1561,7 +1564,7 @@ export function Combobox({
handleClose={handleClose}
refs={refs}
shouldUseDesktopFade={shouldUseDesktopFade}
desktopContainerStyle={desktopContainerStyle}
desktopFrameStyle={desktopFrameStyle}
handleDesktopContentLayout={handleDesktopContentLayout}
header={header}
stickyHeader={stickyHeader}
@@ -18,7 +18,6 @@ import {
Modal,
Platform,
Pressable,
ScrollView,
StatusBar,
Text,
View,
@@ -28,7 +27,7 @@ import {
type StyleProp,
type ViewStyle,
} from "react-native";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import { FadeIn, FadeOut } from "react-native-reanimated";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useIsCompactFormFactor } from "@/constants/layout";
import { Check, CheckCircle } from "lucide-react-native";
@@ -38,6 +37,7 @@ import {
IsolatedBottomSheetModal,
useIsolatedBottomSheetVisibility,
} from "@/components/ui/isolated-bottom-sheet-modal";
import { FloatingScrollView, FloatingSurface } from "@/components/ui/floating";
import { isWeb, isNative } from "@/constants/platform";
import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style";
@@ -498,7 +498,7 @@ export function ContextMenuContent({
[],
);
const animatedContentStyle = useMemo(() => {
const frameStyle = useMemo(() => {
const { width: screenWidth } = Dimensions.get("window");
const resolvedWidthStyle: ViewStyle = fullWidth
? { width: screenWidth - horizontalPadding * 2 }
@@ -508,7 +508,6 @@ export function ContextMenuContent({
...(typeof maxWidth === "number" ? { maxWidth } : null),
};
return [
styles.content,
resolvedWidthStyle,
{
position: "absolute" as const,
@@ -566,23 +565,24 @@ export function ContextMenuContent({
onPress={handleClose}
testID={testID ? `${testID}-backdrop` : undefined}
/>
<Animated.View
<FloatingSurface
entering={FadeIn.duration(100)}
exiting={FadeOut.duration(100)}
collapsable={false}
testID={testID}
onLayout={handleContentLayout}
style={animatedContentStyle}
style={styles.content}
frameStyle={frameStyle}
>
<ScrollView
<FloatingScrollView
bounces={false}
showsVerticalScrollIndicator
style={webScrollbarStyle}
contentContainerStyle={SCROLL_CONTENT_CONTAINER_STYLE}
>
{children}
</ScrollView>
</Animated.View>
</FloatingScrollView>
</FloatingSurface>
</View>
</Modal>
);
@@ -14,7 +14,6 @@ import {
ActivityIndicator,
Modal,
Pressable,
ScrollView,
Text,
View,
Dimensions,
@@ -25,10 +24,10 @@ import {
type ViewStyle,
type StyleProp,
} from "react-native";
import Animated, { Keyframe, runOnJS } from "react-native-reanimated";
import { Keyframe, runOnJS } from "react-native-reanimated";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { Check, CheckCircle } from "lucide-react-native";
import { isWeb } from "@/constants/platform";
import { FloatingScrollView, FloatingSurface } from "@/components/ui/floating";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style";
@@ -172,42 +171,37 @@ function computePosition({
return { x, y, actualPlacement };
}
interface SharedDropdownContentProps {
collapsable: false;
testID?: string;
style: StyleProp<ViewStyle>;
}
function renderDropdownSurface(input: {
isWebSurface: boolean;
sharedProps: SharedDropdownContentProps;
frameStyle: StyleProp<ViewStyle>;
testID?: string;
surfaceStyle: StyleProp<ViewStyle>;
scrollable: boolean;
scrollViewportStyle: StyleProp<ViewStyle>;
content: ReactElement;
onExited: () => void;
}): ReactElement {
const { isWebSurface, sharedProps, scrollable, scrollViewportStyle, content, onExited } = input;
const { frameStyle, testID, surfaceStyle, scrollable, scrollViewportStyle, content, onExited } =
input;
const body = scrollable ? (
<ScrollView
<FloatingScrollView
bounces={false}
showsVerticalScrollIndicator
style={scrollViewportStyle}
contentContainerStyle={DROPDOWN_SCROLL_CONTENT_STYLE}
>
{content}
</ScrollView>
</FloatingScrollView>
) : (
content
);
if (isWebSurface) {
return <View {...sharedProps}>{body}</View>;
}
return (
<Animated.View
{...sharedProps}
<FloatingSurface
collapsable={false}
testID={testID}
style={surfaceStyle}
frameStyle={frameStyle}
entering={contentEntering}
exiting={contentExiting.withCallback((finished) => {
"worklet";
@@ -217,7 +211,7 @@ function renderDropdownSurface(input: {
})}
>
{body}
</Animated.View>
</FloatingSurface>
);
}
@@ -513,7 +507,8 @@ export function DropdownMenuContent({
[],
);
const contentStyle = useMemo(() => {
const surfaceStyle = styles.content;
const frameStyle = useMemo(() => {
const { width: screenWidth } = Dimensions.get("window");
const resolvedWidthStyle: ViewStyle = fullWidth
? { width: screenWidth - horizontalPadding * 2 }
@@ -523,7 +518,6 @@ export function DropdownMenuContent({
...(typeof maxWidth === "number" ? { maxWidth } : null),
};
return [
styles.content,
resolvedWidthStyle,
{
position: "absolute" as const,
@@ -543,14 +537,6 @@ export function DropdownMenuContent({
actualPlacement,
align,
]);
const sharedContentProps = useMemo(
() => ({
collapsable: false as const,
testID,
style: contentStyle,
}),
[testID, contentStyle],
);
const scrollViewportStyle = useMemo(
() => [webScrollbarStyle, visibleContentSize ? { height: visibleContentSize.height } : null],
[visibleContentSize, webScrollbarStyle],
@@ -583,8 +569,9 @@ export function DropdownMenuContent({
/>
{!closing
? renderDropdownSurface({
isWebSurface: isWeb,
sharedProps: sharedContentProps,
frameStyle,
testID,
surfaceStyle,
scrollable,
scrollViewportStyle,
content,
@@ -0,0 +1,63 @@
import { forwardRef, useMemo, type ComponentProps, type ReactElement, type ReactNode } from "react";
import {
ScrollView,
StyleSheet,
type ScrollViewProps,
type StyleProp,
type View,
type ViewStyle,
} from "react-native";
import Animated from "react-native-reanimated";
import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
export interface FloatingSurfaceProps extends Omit<ComponentProps<typeof Animated.View>, "style"> {
frameStyle?: StyleProp<ViewStyle>;
style?: StyleProp<ViewStyle>;
}
export const FloatingSurface = forwardRef<View, FloatingSurfaceProps>(function FloatingSurface(
{ frameStyle, style, ...props },
ref,
): ReactElement {
const inlineFrameStyle = useMemo(() => {
const flattened = StyleSheet.flatten(frameStyle);
return flattened ? inlineUnistylesStyle(flattened) : undefined;
}, [frameStyle]);
const surfaceStyle = useMemo(() => [style, inlineFrameStyle], [inlineFrameStyle, style]);
return <Animated.View {...props} ref={ref} style={surfaceStyle} />;
});
export interface FloatingScrollViewProps {
bounces?: boolean;
children: ReactNode;
contentContainerStyle?: StyleProp<ViewStyle>;
keyboardShouldPersistTaps?: ScrollViewProps["keyboardShouldPersistTaps"];
showsVerticalScrollIndicator?: boolean;
style?: StyleProp<ViewStyle>;
}
export function FloatingScrollView({
bounces,
children,
contentContainerStyle,
keyboardShouldPersistTaps,
showsVerticalScrollIndicator,
style,
}: FloatingScrollViewProps): ReactElement {
const inlineStyle = useMemo(() => {
const flattened = StyleSheet.flatten(style);
return flattened ? inlineUnistylesStyle(flattened) : undefined;
}, [style]);
return (
<ScrollView
bounces={bounces}
contentContainerStyle={contentContainerStyle}
keyboardShouldPersistTaps={keyboardShouldPersistTaps}
showsVerticalScrollIndicator={showsVerticalScrollIndicator}
style={inlineStyle}
>
{children}
</ScrollView>
);
}
+12 -10
View File
@@ -25,9 +25,10 @@ import {
} from "react-native";
import { Portal } from "@gorhom/portal";
import { useBottomSheetModalInternal } from "@gorhom/bottom-sheet";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import { FadeIn, FadeOut } from "react-native-reanimated";
import { StyleSheet } from "react-native-unistyles";
import { useIsCompactFormFactor } from "@/constants/layout";
import { FloatingSurface } from "@/components/ui/floating";
import { isWeb } from "@/constants/platform";
type Side = "top" | "bottom" | "left" | "right";
@@ -494,19 +495,18 @@ export function TooltipContent({
[],
);
const contentStyle = useMemo(
const frameStyle = useMemo(
() => [
styles.content,
{ maxWidth },
style,
{
position: "absolute" as const,
top: position?.y ?? -9999,
left: position?.x ?? -9999,
maxWidth,
},
],
[maxWidth, style, position?.x, position?.y],
[maxWidth, position?.x, position?.y],
);
const contentStyle = useMemo(() => [styles.content, style], [style]);
const handleDismiss = useCallback(() => ctx.setOpen(false), [ctx]);
@@ -519,7 +519,7 @@ export function TooltipContent({
return (
<Portal hostName={bottomSheetInternal?.hostName}>
<View pointerEvents="none" style={styles.portalOverlay}>
<Animated.View
<FloatingSurface
pointerEvents="none"
entering={FadeIn.duration(80)}
exiting={FadeOut.duration(80)}
@@ -527,9 +527,10 @@ export function TooltipContent({
testID={testID}
onLayout={handleLayout}
style={contentStyle}
frameStyle={frameStyle}
>
{children}
</Animated.View>
</FloatingSurface>
</View>
</Portal>
);
@@ -544,7 +545,7 @@ export function TooltipContent({
onRequestClose={handleDismiss}
>
<Pressable style={styles.overlay} onPress={handleDismiss}>
<Animated.View
<FloatingSurface
pointerEvents="none"
entering={FadeIn.duration(80)}
exiting={FadeOut.duration(80)}
@@ -552,9 +553,10 @@ export function TooltipContent({
testID={testID}
onLayout={handleLayout}
style={contentStyle}
frameStyle={frameStyle}
>
{children}
</Animated.View>
</FloatingSurface>
</Pressable>
</Modal>
);