import { isAndroid, isClient, isIos, isWeb, useIsomorphicLayoutEffect, } from '@tamagui/constants' import { StyleObjectIdentifier, StyleObjectProperty, StyleObjectPseudo, StyleObjectRules, nonAnimatableStyleProps, stylePropsText, stylePropsTransform, tokenCategories, validPseudoKeys, validStyles as validStylesView, } from '@tamagui/helpers' import React from 'react' import { getConfig, getFont, getSetting } from '../config' import { isDevTools } from '../constants/isDevTools' import { getMediaImportanceIfMoreImportant, getMediaKey, getMediaKeyImportance, mediaKeyMatch, } from '../hooks/useMedia' import { mediaState as globalMediaState, mediaQueryConfig } from './mediaState' import type { AllGroupContexts, AnimationDriver, ClassNamesObject, ComponentContextI, DebugProp, GetStyleResult, GetStyleState, PseudoStyles, RulesToInsert, SpaceTokens, SplitStyleProps, StaticConfig, StyleObject, TamaguiComponentState, TamaguiInternalConfig, TextStyle, ThemeParsed, ViewStyleWithPseudos, } from '../types' import { createMediaStyle } from './createMediaStyle' import { fixStyles } from './expandStyles' import { getCSSStylesAtomic, getStyleAtomic, styleToCSS } from './getCSSStylesAtomic' import { getDefaultProps } from './getDefaultProps' import { extractValueFromDynamic, getDynamicVal, getOppositeScheme, isColorStyleKey, } from './getDynamicVal' import { getGroupPropParts } from './getGroupPropParts' import { insertStyleRules, shouldInsertStyleRules, updateRules } from './insertStyleRule' import { isActivePlatform, getPlatformSpecificityBump } from './isActivePlatform' import { isActiveTheme } from './isActiveTheme' import { log } from './log' import { normalizeValueWithProperty } from './normalizeValueWithProperty' import { propMapper } from './propMapper' import { type PseudoDescriptorKey, pseudoDescriptors, pseudoPriorities, defaultMediaImportance, } from './pseudoDescriptors' import { skipProps } from './skipProps' import { sortString } from './sortString' import { styleOriginalValues } from './styleOriginalValues' import { transformsToString } from './transformsToString' export { styleOriginalValues } export type SplitStyles = ReturnType export type SplitStyleResult = ReturnType let conf: TamaguiInternalConfig type StyleSplitter = ( props: { [key: string]: any }, staticConfig: StaticConfig, theme: ThemeParsed, themeName: string, componentState: TamaguiComponentState, styleProps: SplitStyleProps, parentSplitStyles?: GetStyleResult | null, context?: ComponentContextI, groupContext?: AllGroupContexts | null, // web-only elementType?: string, startedUnhydrated?: boolean, debug?: DebugProp, // resolved animation driver (respects animatedBy prop) animationDriver?: AnimationDriver | null ) => null | GetStyleResult export const PROP_SPLIT = '-' // Normalize group keys like $group-press to $group-true-press when the group name // doesn't exist in context (defaults to the unnamed 'true' group) function normalizeGroupKey( key: string, groupContext: AllGroupContexts | null | undefined ): string { const parts = key.split('-') const plen = parts.length if ( // check if its actually a simple group selector to avoid breaking selectors plen === 2 || (plen === 3 && pseudoPriorities[parts[parts.length - 1]]) ) { const name = parts[1] if (name !== 'true' && groupContext && !groupContext[name]) { return key.replace('$group-', '$group-true-') } } return key } // if you need and easier way to test performance, you can do something like this // add this early return somewhere in this file and you can see roughly where it slows down: // return { // space, // hasMedia, // fontFamily: styleState.fontFamily, // viewProps: { // children: props.children, // }, // style: { // borderColor: props.borderColor, // borderWidth: props.borderWidth, // padding: props.padding, // }, // pseudos, // classNames, // rulesToInsert, // dynamicThemeAccess, // } function isValidStyleKey( key: string, validStyles: Record, accept?: Record ) { return key in validStyles ? true : accept && key in accept } export const getSplitStyles: StyleSplitter = ( props, staticConfig, theme, themeName, componentState, styleProps, parentSplitStyles, componentContext, groupContext, elementType, startedUnhydrated, debug, animationDriver ) => { conf = conf || getConfig() // use passed animationDriver or fall back to context/config const driver = animationDriver || componentContext?.animationDriver || (conf.animations as AnimationDriver) if (props.passThrough) { return null } // a bit icky, we need no normalize but not fully if ( isWeb && styleProps.isAnimated && driver?.isReactNative && !styleProps.noNormalize ) { styleProps.noNormalize = 'values' } const { shorthands } = conf const { isHOC, isText, isInput, variants, isReactNative, inlineProps, inlineWhenUnflattened, parentStaticConfig, acceptsClassName, } = staticConfig const viewProps: GetStyleResult['viewProps'] = {} const mediaState = styleProps.mediaState || globalMediaState const shouldDoClasses = acceptsClassName && isWeb && !styleProps.noClass const rulesToInsert: RulesToInsert = process.env.TAMAGUI_TARGET === 'native' ? (undefined as any) : {} const classNames: ClassNamesObject = {} let space: SpaceTokens | null = props.space let pseudos: PseudoStyles | null = null let hasMedia: boolean | Set = false let dynamicThemeAccess: boolean | undefined let pseudoGroups: Set | undefined let mediaGroups: Set | undefined let className = (props.className as string) || '' // existing classNames let mediaStylesSeen = 0 const validStyles = staticConfig.validStyles || (staticConfig.isText || staticConfig.isInput ? stylePropsText : validStylesView) if ( process.env.NODE_ENV === 'development' && (debug === 'profile' || (globalThis as any).time) ) { // @ts-expect-error time`split-styles-setup` } /** * Not the biggest fan of creating an object but it is a nice API */ const styleState: GetStyleState = { classNames, conf, props, styleProps, componentState, staticConfig, style: null, theme, usedKeys: {}, viewProps, context: componentContext, debug, // resolved animation driver (respects animatedBy prop) animationDriver: driver, } // only used by compiler if (process.env.IS_STATIC === 'is_static') { const { fallbackProps } = styleProps if (fallbackProps) { styleState.props = new Proxy(props, { get(_, key, val) { if (!Reflect.has(props, key)) { return Reflect.get(fallbackProps, key) } return Reflect.get(props, key) }, }) } } if ( process.env.NODE_ENV === 'development' && (debug === 'profile' || (globalThis as any).time) ) { // @ts-expect-error time`style-state` } if (process.env.NODE_ENV === 'development' && debug === 'verbose' && isClient) { if (isDevTools) { console.groupCollapsed('🔹 getSplitStyles 👇') log({ props, staticConfig, shouldDoClasses, styleProps, rulesToInsert, componentState, styleState, theme: { ...theme }, }) } } const { asChild } = props const { accept } = staticConfig const { noSkip, disableExpandShorthands, noExpand, styledContext } = styleProps const { webContainerType } = conf.settings const parentVariants = parentStaticConfig?.variants for (const keyOg in props) { let keyInit = keyOg let valInit = props[keyInit] if (keyInit === 'children') { viewProps[keyInit] = valInit continue } if ( process.env.NODE_ENV === 'development' && (debug === 'profile' || (globalThis as any).time) ) { // @ts-expect-error time`before-prop-${keyInit}` } if (process.env.NODE_ENV === 'test' && keyInit === 'jestAnimatedStyle') { continue } // for custom accept sub-styles if (accept) { const accepted = accept[keyInit] if ( (accepted === 'style' || accepted === 'textStyle') && valInit && typeof valInit === 'object' ) { viewProps[keyInit] = getSubStyle(styleState, keyInit, valInit, styleProps.noClass) continue } } // normalize shorthands up front if (!disableExpandShorthands) { if (keyInit in shorthands) { keyInit = shorthands[keyInit] } } if (keyInit === 'className') continue // handled above first // when asChild, skip default props - they shouldn't be passed down to children if (asChild) { const defaults = getDefaultProps(staticConfig) if (defaults) { // check both original key and expanded key (after shorthand expansion) const defaultVal = defaults[keyOg] ?? defaults[keyInit] if (defaultVal !== undefined && valInit === defaultVal) { continue } } } // keyInit === 'style' is handled in skipProps if (keyInit in skipProps && !noSkip && !isHOC) { if (keyInit === 'group') { if (process.env.TAMAGUI_TARGET === 'web') { // add container style const identifier = `t_group_${valInit}` const containerType = webContainerType || 'inline-size' const containerCSS = [ 'container', undefined, identifier, undefined, [ `.${identifier} { container-name: ${valInit}; container-type: ${containerType}; }`, ], ] satisfies StyleObject addStyleToInsertRules(rulesToInsert, containerCSS) } } // transition prop is skipped when it's a named animation (e.g. 'quick') // but raw CSS values (from $platform-web) should pass through as style if ( keyInit === 'transition' && typeof valInit === 'string' && !driver?.animations?.[valInit] ) { // not a known animation name, treat as raw CSS } else { continue } } let isValidStyleKeyInit = isValidStyleKey(keyInit, validStyles, accept) // this is all for partially optimized (not flattened)... maybe worth removing? if (process.env.TAMAGUI_TARGET === 'web') { // react-native-web ignores data-* attributes, fixes passing them to animated views if (staticConfig.isReactNative && keyInit.startsWith('data-')) { keyInit = keyInit.replace('data-', '') viewProps['dataSet'] ||= {} viewProps['dataSet'][keyInit] = valInit continue } } if (process.env.TAMAGUI_TARGET === 'native') { if (!isValidStyleKeyInit) { if (!isAndroid) { // only works in android if (keyInit === 'elevationAndroid') continue } // map userSelect to native prop if (keyInit === 'userSelect') { keyInit = 'selectable' valInit = valInit !== 'none' } else if (keyInit.startsWith('data-')) { continue } } } if (process.env.TAMAGUI_TARGET === 'web') { if (!noExpand) { /** * Copying in the accessibility/prop handling from react-native-web here * Keeps it in a single loop, avoids dup de-structuring to avoid bundle size */ if (keyInit === 'disabled' && valInit === true) { viewProps['aria-disabled'] = true // isInput: Input/TextArea wrap the real /