import { concatClassName , stylePropsView } from '@tamagui/helpers' ; import { useForceUpdate } from '@tamagui/use-force-update' ; import React , { Children , Fragment , createElement , forwardRef , useCallback , useContext , useEffect , useRef , useState , } from 'react' ; import { StyleSheet , Text , View , ViewStyle } from 'react-native' ; import { stackDefaultStyles } from './constants/constants' ; import { isTouchDevice , isWeb } from './constants/platform' ; import { rnw } from './constants/rnw' ; import { onConfiguredOnce } from './createTamagui' ; import { addStylesUsingClassname , useStylesAsClassname } from './helpers/addStylesUsingClassname' ; import { extendStaticConfig , parseStaticConfig } from './helpers/extendStaticConfig' ; import { getSplitStyles } from './helpers/getSplitStyles' ; import { useFeatures } from './hooks/useFeatures' ; import { usePressable } from './hooks/usePressable' ; import { useTheme } from './hooks/useTheme' ; import { SpaceTokens , StaticComponent , StaticConfig , StaticConfigParsed , TamaguiInternalConfig , } from './types' ; import { TextAncestorContext } from './views/TextAncestorContext' ; export const mouseUps = new Set < Function > ( ) ; if ( typeof document !== 'undefined' ) { document . addEventListener ( 'mouseup' , ( ) => { mouseUps . forEach ( ( x ) => x ( ) ) ; mouseUps . clear ( ) ; } ) ; } type DefaultProps = { } ; const defaultComponentState = { hover : false , press : false , pressIn : false , } ; type ComponentState = typeof defaultComponentState ; function createShallowUpdate ( setter ) { return ( next ) => { setter ( ( prev ) => { for ( const key in next ) { if ( prev [ key ] !== next [ key ] ) { return { ... prev , ... next } ; } } return prev ; } ) ; } ; } export function createComponent < A extends Object = DefaultProps > ( configIn ) { let staticConfig ; if ( 'parsed' in configIn ) { staticConfig = configIn ; } else { staticConfig = parseStaticConfig ( configIn ) ; } const { Component , validStyles , isText , isZStack } = staticConfig ; const validStyleProps = validStyles ?? stylePropsView ; let defaultProps = null as any ; let tamaguiConfig ; let defaultsStyle ; let defaultsClassName = '' ; let initialTheme ; const component = forwardRef < View , A > ( ( props , forwardedRef ) => { const forceUpdate = useForceUpdate ( ) ; const features = useFeatures ( props , { forceUpdate } ) ; const theme = useTheme ( ) ; const [ state , set_ ] = useState < ComponentState > ( defaultComponentState ) ; const set = createShallowUpdate ( set_ ) ; if ( process . env . NODE_ENV === 'development' && ! isText && isWeb ) { Children . toArray ( props . children ) . forEach ( ( item ) => { if ( typeof item === 'string' ) { console . error ( `Unexpected text node: ${ item }. A text node cannot be a child of a <View>.` ) ; } } ) ; } const hasTextAncestor = isWeb ? useContext ( TextAncestorContext ) : false ; const hostRef = useRef ( null ) ; const { viewProps : viewPropsIn , pseudos , style , classNames , } = getSplitStyles ( props , staticConfig , theme ) ; const { tag , hitSlop , children , pointerEvents , onPress , onPressIn , onPressOut , onHoverIn , onHoverOut , space , disabled , onMouseEnter , onMouseLeave , hrefAttrs , onMoveShouldSetResponder , onMoveShouldSetResponderCapture , onResponderEnd , onResponderGrant , onResponderMove , onResponderReject , onResponderRelease , onResponderStart , onResponderTerminate , onResponderTerminationRequest , onScrollShouldSetResponder , onScrollShouldSetResponderCapture , onSelectionChangeShouldSetResponder , onSelectionChangeShouldSetResponderCapture , onStartShouldSetResponder , onStartShouldSetResponderCapture , onMouseDown , onClick , accessible , accessibilityRole , onLayout , ... viewProps } = viewPropsIn ; if ( ! isWeb ) { if ( accessible ) viewProps . accessible = accessible ; if ( accessibilityRole ) viewProps . accessibilityRole = accessibilityRole ; } if ( isWeb ) { rnw . useResponderEvents ( hostRef , { onMoveShouldSetResponder , onMoveShouldSetResponderCapture , onResponderEnd , onResponderGrant , onResponderMove , onResponderReject , onResponderRelease , onResponderStart , onResponderTerminate , onResponderTerminationRequest , onScrollShouldSetResponder , onScrollShouldSetResponderCapture , onSelectionChangeShouldSetResponder , onSelectionChangeShouldSetResponderCapture , onStartShouldSetResponder , onStartShouldSetResponderCapture , } ) ; } const internal = useRef < { isMounted : boolean ; hasEverHadEvents ? : boolean ; } > ( ) ; if ( ! internal . current ) { internal . current = { isMounted : true , } ; } useEffect ( ( ) => { internal . current ! . isMounted = true ; return ( ) => { mouseUps . delete ( unPress ) ; internal . current ! . isMounted = false ; } ; } , [ ] ) ; const element = isWeb ? ! Component || typeof Component === 'string' ? tag || Component : Component : Component ; const ReactView = ! isWeb ? View : element || ( hasTextAncestor ? 'span' : 'div' ) ; const ReactText = ! isWeb ? Text : element || 'span' ; const ViewComponent = isText ? ReactText : ReactView ; const isHovering = ! isTouchDevice && ! disabled && pseudos && state . hover ; const isPressing = ! disabled && pseudos && state . press ; const styles = [ style , isHovering ? pseudos . hoverStyle || null : null , isPressing ? pseudos . pressStyle || null : null , ] ; if ( isWeb ) { const stylesClassNames = useStylesAsClassname ( styles ) ; const classList = isText ? [ defaultsClassName , classNames , stylesClassNames , props . className , ] : [ defaultsClassName , props . className , classNames , stylesClassNames ] ; const className = concatClassName ( ... classList ) ; if ( process . env . NODE_ENV === 'development' ) { if ( props [ 'debug' ] ) { console . log ( '🥚 className' , { style , styles , classList , stylesClassNames , className } ) ; } } viewProps . className = className ; } else { viewProps . style = styles ; } if ( pointerEvents ) { viewProps . pointerEvents = pointerEvents ; } if ( isWeb ) { const platformMethodsRef = rnw . usePlatformMethods ( viewProps ) ; const setRef = rnw . useMergeRefs ( hostRef , platformMethodsRef , forwardedRef ) ; viewProps . ref = setRef ; if ( props . href != null && hrefAttrs != null ) { const { download , rel , target } = hrefAttrs ; if ( download != null ) { viewProps . download = download ; } if ( rel != null ) { viewProps . rel = rel ; } if ( typeof target === 'string' ) { viewProps . target = target . charAt ( 0 ) !== '_' ? '_' + target : target ; } } } else { if ( forwardedRef ) { viewProps . ref = forwardedRef ; } } const attachPress = ! ! ( ( pseudos && pseudos . pressStyle ) || onPress || onPressOut || onPressIn || onClick ) ; const attachHover = isWeb && ! isTouchDevice && ! ! ( ( pseudos && pseudos . hoverStyle ) || onHoverIn || onHoverOut || onMouseEnter || onMouseLeave ) ; const shouldAttach = internal . current . hasEverHadEvents || attachPress || attachHover || 'pressStyle' in props || 'onPress' in props || 'onPressIn' in props || 'onPressOut' in props || ( isWeb && ( 'hoverStyle' in props || 'onClick' in props || 'onHoverIn' in props || 'onHoverOut' in props || 'onMouseEnter' in props || 'onMouseLeave' in props ) ) ; if ( shouldAttach ) { internal . current . hasEverHadEvents = true ; } const unPress = useCallback ( ( ) => { if ( ! internal . current ! . isMounted ) return ; set ( { press : false , pressIn : false , } ) ; } , [ ] ) ; const events = shouldAttach ? { ... ( ! isWeb && { onPressOut : ( e ) => { unPress ( ) ; onPressOut ?. ( e ) ; } , } ) , ... ( isWeb && ! isTouchDevice && { onMouseEnter : attachHover || attachPress ? ( e ) => { let next = { } ; if ( attachHover ) { next . hover = true ; } if ( state . pressIn ) { next . press = true ; } if ( Object . keys ( next ) . length ) { set ( next ) ; } onHoverIn ?. ( e ) ; onMouseEnter ?. ( e ) ; } : undefined , onMouseLeave : attachHover || attachPress ? ( e ) => { let next = { } ; mouseUps . add ( unPress ) ; if ( attachHover ) { next . hover = false ; } if ( state . pressIn ) { next . press = false ; next . pressIn = false ; } if ( Object . keys ( next ) . length ) { set ( next ) ; } onHoverOut ?. ( e ) ; onMouseLeave ?. ( e ) ; } : undefined , } ) , onMouseDown : attachPress ? ( e ) => { set ( { press : true , pressIn : true , } ) ; onPressIn ?. ( e ) ; onMouseDown ?. ( e ) ; } : ( onPressIn as any ) , onClick : attachPress ? ( e ) => { onPress ?. ( e ) ; onClick ?. ( e ) ; unPress ( ) ; onPressOut ?. ( e ) ; } : ( onPressOut as any ) , } : null ; if ( events ) { if ( typeof ViewComponent !== 'string' ) { const [ pressProps ] = usePressable ( { disabled , hitSlop , onPressIn : events . onMouseDown , onPressOut : events . onPressOut , onPress : events . onClick , } ) ; Object . assign ( viewProps , pressProps ) ; } else { Object . assign ( viewProps , events ) ; } } const childEls = spacedChildren ( { children , space , flexDirection : props . flexDirection || defaultProps ?. flexDirection , isZStack : isZStack , } ) ; let content ; if ( isWeb && rnw ) { const domProps = rnw . createDOMProps ( viewProps ) ; const className = domProps . className && domProps . className !== viewProps . className ? `${ domProps . className } ${ viewProps . className }` : viewProps . className ; Object . assign ( viewProps , domProps ) ; viewProps . className = className ; content = createElement ( ViewComponent , viewProps , childEls ) ; } else { content = createElement ( ViewComponent , viewProps , childEls ) ; } const shouldWrapTextAncestor = isWeb && isText && ! hasTextAncestor ; if ( shouldWrapTextAncestor ) { content = createElement ( TextAncestorContext . Provider , { value : true } , content ) ; } if ( process . env . NODE_ENV === 'development' ) { if ( props [ 'debug' ] ) { viewProps [ 'debug' ] = true ; console . log ( '🥚 props in:' , props ) ; console . log ( '🥚 props out:' , viewProps ) ; console . log ( props . onClick ?. toString ( ) ) ; console . log ( '🥚 etc:' , { shouldAttach , ViewComponent , viewProps , styles , pseudos , content , childEls } ) ; if ( isWeb ) { console . log ( '🥚 component info' , { staticConfig , tamaguiConfig } ) ; } } } return ( < > { features } { content } < />); } ) ; onConfiguredOnce ( ( conf ) => { tamaguiConfig = conf ; initialTheme = conf . themes [ conf . defaultTheme || Object . keys ( conf . themes ) [ 0 ] ] ; const next = getSplitStyles ( staticConfig . defaultProps , staticConfig , initialTheme , true ) ; if ( isWeb ) { if ( next . classNames ) { defaultsClassName += next . classNames + ' ' ; } defaultsClassName += addStylesUsingClassname ( [ next . style , next . pseudos ] ) ; } else { if ( ! hasWarnedOnce ) { hasWarnedOnce = true ; console . log ( '⚠️ TODO account for default styles for media queries' ) ; } } component . defaultProps = { ... next . viewProps , ... component . defaultProps , } ; if ( process . env . NODE_ENV === 'development' ) { if ( staticConfig . defaultProps ?. debug ) { console . log ( 'tamagui debug component:' , next , defaultsClassName , component . defaultProps ) ; } } } ) ; component [ 'staticConfig' ] = { validStyles : validStyleProps , ... staticConfig , } ; const res = component as any as StaticComponent < A , void > ; res [ 'extractable' ] = ( Component , conf ? ) => { Component [ 'staticConfig' ] = extendStaticConfig ( res , { ... conf , neverFlatten : true , defaultProps : { ... Component . defaultProps , ... conf ?. defaultProps , } , } ) ; return Component ; } ; return res ; } let hasWarnedOnce = false ; export const Spacer = createComponent < { size ? : number | SpaceTokens ; flex ? : boolean | number ; } > ( { defaultProps : { ... stackDefaultStyles , size : true , } , variants : { size : { '...size' : ( size , { tokens } ) => { size = size == true ? '$true' : size ; return { width : tokens . size [ size ] ?? size , height : tokens . size [ size ] ?? size , } ; } , } , direction : { horizontal : { height : 0 , } , vertical : { width : 0 , } , } , } , } ) ; export function spacedChildren ( { isZStack , children , space , flexDirection , } ) { if ( ! space && ! isZStack ) { return children ; } const childrenList = Children . toArray ( children ) ; const len = childrenList . length ; if ( len === 1 ) { return children ; } const next = [ ] ; for ( const [ index , child ] of childrenList . entries ( ) ) { if ( child === null || child === undefined ) { continue ; } const key = `${ child ?. [ 'key' ] ?? index }` ; next . push ( < Fragment key = { key } > { isZStack ? < Absolute > { child } < /Absolute> : child}</Fragment > ) ; if ( child ?. [ 'type' ] ?. [ 'isVisuallyHidden' ] ) { continue ; } if ( index !== len - 1 ) { if ( space ) { next . push ( < Spacer key = { `${ key }_spacer` } direction = { flexDirection === 'row' || flexDirection === 'row-reverse' ? 'horizontal' : 'vertical' } size = { space } / > ) ; } } } return next ; } const Absolute = ( { children } ) => < View style = { sheet . fullscreen } > { children } < /View>; const sheet = StyleSheet . create ( { fullscreen : { position : 'absolute' , top : 0 , left : 0 , right : 0 , bottom : 0 , } , } ) ;