import { isWeb } from '@tamagui/constants' import type { MutableRefObject } from 'react' import React, { Children, cloneElement, forwardRef, isValidElement, useRef } from 'react' import { getSetting } from '../config' import { variableToString } from '../createVariable' import { useThemeWithState } from '../hooks/useTheme' import { getThemeState, hasThemeUpdatingProps, ThemeStateContext, } from '../hooks/useThemeState' import type { ThemeProps, ThemeState } from '../types' import { ThemeDebug } from './ThemeDebug' type ThemeComponentPropsOnly = ThemeProps & { passThrough?: boolean; contain?: boolean } export const Theme = forwardRef(function Theme(props: ThemeComponentPropsOnly, ref) { 'use no memo' // @ts-expect-error only for internal views if (props.disable) { return props.children } const { passThrough } = props const isRoot = !!props['_isRoot'] const [_, themeState] = useThemeWithState(props, isRoot) const disableDirectChildTheme = props['disable-child-theme'] let finalChildren = disableDirectChildTheme ? Children.map(props.children, (child) => passThrough || !isValidElement(child) ? child : cloneElement(child, { ['data-disable-theme']: true } as any) ) : props.children if (ref) { try { React.Children.only(finalChildren) // TODO deprecate react 18 and then avoid clone here and just pass prop finalChildren = cloneElement(finalChildren, { ref }) } catch { //ok } } const stateRef = useRef({ hasEverThemed: false, }) return getThemedChildren( themeState, finalChildren, props, isRoot, stateRef, passThrough ) }) Theme['avoidForwardRef'] = true export function getThemedChildren( themeState: ThemeState, children: any, props: ThemeComponentPropsOnly, isRoot = false, stateRef: MutableRefObject<{ hasEverThemed?: boolean | 'wrapped' }>, passThrough = false ) { const { shallow, forceClassName } = props // always be true if ever themed so we avoid re-parenting const state = stateRef.current let hasEverThemed = state.hasEverThemed let shouldRenderChildrenWithTheme = hasEverThemed || themeState.isNew || isRoot || hasThemeUpdatingProps(props) if (process.env.NODE_ENV === 'development' && props.debug === 'visualize') { children = ( {children} ) } if (!shouldRenderChildrenWithTheme) { return children } // from here on out we have to be careful not to re-parent children = ( {children} ) const { isInverse, name } = themeState const requiresExtraWrapper = isInverse || forceClassName // it only ever progresses from false => true => 'wrapped' if (!state.hasEverThemed) { state.hasEverThemed = true } if ( requiresExtraWrapper || // if the theme is exactly dark or light, its likely to change between dark/light // and that would require wrapping which would re-parent, so to avoid re-parenting do this themeState.name === 'dark' || themeState.name === 'light' ) { state.hasEverThemed = 'wrapped' } // each children of these children wont get the theme if (shallow) { if (!themeState.parentId) { // they are doing shallow but didnt change actually change a theme theme? } else { const parentState = getThemeState( themeState.isNew ? themeState.id : themeState.parentId ) if (!parentState) throw new Error(`‼️010`) children = Children.toArray(children).map((child) => { return isValidElement(child) ? passThrough ? child : cloneElement( child, undefined, {(child as any).props.children} ) : child }) } } if (process.env.NODE_ENV === 'development') { if (!passThrough && props.debug) { console.warn(` getThemedChildren`, { requiresExtraWrapper, forceClassName, themeState, state, themeSpanProps: getThemeClassNameAndColor(themeState, props, isRoot), }) } } // this has to be after a few of the above items so it properly sets context (even if shallow set) if (forceClassName === false) { return children } if (isWeb) { const baseStyle = props.contain ? inertContainedStyle : inertStyle const { className = '', color } = passThrough ? {} : getThemeClassNameAndColor(themeState, props, isRoot) children = ( {children} ) // to prevent tree structure changes always render this if inverse is true or false if (state.hasEverThemed === 'wrapped') { // but still calculate if we need the classnames const className = requiresExtraWrapper ? `${ name.startsWith('light') ? 't_light' : name.startsWith('dark') ? 't_dark' : '' } _dsp_contents` : `_dsp_contents` children = {children} } return children } return children } const inertStyle = { display: 'contents', } const inertContainedStyle = { display: 'contents', contain: 'strict', } const empty = { className: '', color: undefined } function getThemeClassNameAndColor( themeState: ThemeState, props: ThemeProps, isRoot = false ) { if (!themeState.isNew && !props.forceClassName) { return empty } // in order to provide currentColor, set color by default const themeColor = themeState?.theme && themeState.isNew ? variableToString(themeState.theme.color) : '' const style = themeColor ? { color: themeColor, } : undefined const themeClassName = themeState.name.replace(schemePrefix, '') // Build full hierarchy of theme classes for CSS variable inheritance // Examples: // - "red_surface1" → "t_red t_red_surface1" // - "green_active_Button" → "t_green t_green_active t_green_active_Button" const themeNameParts = themeClassName.split('_') let themeClasses = `t_${themeClassName}` if (themeNameParts.length > 1) { // Build full hierarchy for all multi-part themes (sub-themes, component themes, etc.) // This enables CSS variable inheritance through all levels const hierarchyClasses: string[] = [] for (let i = 1; i <= themeNameParts.length; i++) { hierarchyClasses.push(`t_${themeNameParts.slice(0, i).join('_')}`) } themeClasses = hierarchyClasses.join(' ') } const className = `${isRoot ? '' : 't_sub_theme'} ${themeClasses}` return { color: themeColor, className } } const schemePrefix = /^(dark|light)_/