From 0cdf350b688c6c551e3aada9399f8b29e520a565 Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Wed, 28 Jan 2026 15:30:16 -0800 Subject: [PATCH 01/15] update phasedComponent pattern and switch direct stagedComponent callers to use it --- packages/components/Icon/package.json | 1 + .../components/Icon/src/FontIcon/FontIcon.tsx | 4 +- .../components/Icon/src/SvgIcon/SvgIcon.tsx | 4 +- packages/components/Icon/src/legacy/Icon.tsx | 4 +- packages/components/Menu/src/Menu/Menu.tsx | 8 +-- .../src/MenuCallout/MenuCallout.android.tsx | 11 ++-- .../Menu/src/MenuCallout/MenuCallout.tsx | 12 ++-- .../Notification/src/Notification.helper.tsx | 57 ++++++++++--------- .../TabListAnimatedIndicator.tsx | 4 +- .../TabListAnimatedIndicator.win32.tsx | 4 +- packages/experimental/Overflow/package.json | 3 +- .../Overflow/src/Overflow/Overflow.tsx | 10 ++-- .../src/OverflowItem/OverflowItem.tsx | 9 +-- .../src/OverflowItem/OverflowItem.types.ts | 3 + packages/experimental/Shadow/package.json | 1 + packages/experimental/Shadow/src/Shadow.tsx | 23 +++----- .../experimental/Shadow/src/Shadow.types.ts | 4 ++ packages/experimental/Tooltip/package.json | 2 +- packages/experimental/Tooltip/src/Tooltip.tsx | 7 ++- .../src/component-patterns/phasedComponent.ts | 25 ++++++++ .../src/component-patterns/render.types.ts | 9 ++- .../src/component-patterns/stagedComponent.ts | 24 ++++++++ .../component-patterns/stagedComponent.tsx | 45 --------------- packages/framework-base/src/index.ts | 9 +-- yarn.lock | 5 +- 25 files changed, 150 insertions(+), 138 deletions(-) create mode 100644 packages/framework-base/src/component-patterns/phasedComponent.ts create mode 100644 packages/framework-base/src/component-patterns/stagedComponent.ts delete mode 100644 packages/framework-base/src/component-patterns/stagedComponent.tsx diff --git a/packages/components/Icon/package.json b/packages/components/Icon/package.json index 5f15e9d7b04..42a445d5a7b 100644 --- a/packages/components/Icon/package.json +++ b/packages/components/Icon/package.json @@ -34,6 +34,7 @@ "dependencies": { "@fluentui-react-native/adapters": "workspace:*", "@fluentui-react-native/framework": "workspace:*", + "@fluentui-react-native/framework-base": "workspace:*", "@fluentui-react-native/text": "workspace:*" }, "devDependencies": { diff --git a/packages/components/Icon/src/FontIcon/FontIcon.tsx b/packages/components/Icon/src/FontIcon/FontIcon.tsx index d8473943914..4e0a84023b6 100644 --- a/packages/components/Icon/src/FontIcon/FontIcon.tsx +++ b/packages/components/Icon/src/FontIcon/FontIcon.tsx @@ -1,12 +1,12 @@ import { Text } from 'react-native'; -import { mergeProps, stagedComponent } from '@fluentui-react-native/framework'; +import { mergeProps, phasedComponent } from '@fluentui-react-native/framework-base'; import type { FontIconProps } from './FontIcon.types'; import { fontIconName } from './FontIcon.types'; import { useFontIcon } from './useFontIcon'; -export const FontIcon = stagedComponent((props: FontIconProps) => { +export const FontIcon = phasedComponent((props: FontIconProps) => { const fontIconProps = useFontIcon(props); return (final: FontIconProps) => { const newProps = mergeProps(fontIconProps, final); diff --git a/packages/components/Icon/src/SvgIcon/SvgIcon.tsx b/packages/components/Icon/src/SvgIcon/SvgIcon.tsx index 51b046b62f0..10085c57433 100644 --- a/packages/components/Icon/src/SvgIcon/SvgIcon.tsx +++ b/packages/components/Icon/src/SvgIcon/SvgIcon.tsx @@ -1,13 +1,13 @@ import { Platform, View } from 'react-native'; -import { mergeProps, stagedComponent } from '@fluentui-react-native/framework'; +import { mergeProps, phasedComponent } from '@fluentui-react-native/framework-base'; import { SvgUri } from 'react-native-svg'; import type { SvgIconProps } from './SvgIcon.types'; import { svgIconName } from './SvgIcon.types'; import { useSvgIcon } from './useSvgIcon'; -export const SvgIcon = stagedComponent((props: SvgIconProps) => { +export const SvgIcon = phasedComponent((props: SvgIconProps) => { const svgProps = useSvgIcon(props); return (final: SvgIconProps) => { const { style, height, width, src, uri, viewBox, color, ...rest } = mergeProps(svgProps, final); diff --git a/packages/components/Icon/src/legacy/Icon.tsx b/packages/components/Icon/src/legacy/Icon.tsx index ee160fefe6f..c016bd5b2e4 100644 --- a/packages/components/Icon/src/legacy/Icon.tsx +++ b/packages/components/Icon/src/legacy/Icon.tsx @@ -2,7 +2,7 @@ import { Image, Platform, View } from 'react-native'; import type { ImageStyle, TextStyle } from 'react-native'; import { mergeStyles, useFluentTheme } from '@fluentui-react-native/framework'; -import { stagedComponent, mergeProps, getMemoCache, getTypedMemoCache } from '@fluentui-react-native/framework'; +import { phasedComponent, mergeProps, getMemoCache, getTypedMemoCache } from '@fluentui-react-native/framework-base'; import { Text } from '@fluentui-react-native/text'; import type { SvgProps } from 'react-native-svg'; import { SvgUri } from 'react-native-svg'; @@ -92,7 +92,7 @@ function renderSvg(iconProps: IconProps) { } } -export const Icon = stagedComponent((props: IconProps) => { +export const Icon = phasedComponent((props: IconProps) => { const theme = useFluentTheme(); return (rest: IconProps) => { diff --git a/packages/components/Menu/src/Menu/Menu.tsx b/packages/components/Menu/src/Menu/Menu.tsx index 6d281076305..5b18424acff 100644 --- a/packages/components/Menu/src/Menu/Menu.tsx +++ b/packages/components/Menu/src/Menu/Menu.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { stagedComponent } from '@fluentui-react-native/framework'; +import { phasedComponent } from '@fluentui-react-native/framework-base'; import type { MenuProps } from './Menu.types'; import { menuName } from './Menu.types'; @@ -8,12 +8,12 @@ import { renderFinalMenu } from './renderMenu'; import { useMenu } from './useMenu'; import { useMenuContextValue } from './useMenuContextValue'; -export const Menu = stagedComponent((props: MenuProps) => { +export const Menu = phasedComponent((props: MenuProps) => { const state = useMenu(props); const contextValue = useMenuContextValue(state); - return (_rest: MenuProps, children: React.ReactNode) => { - const childrenArray = React.Children.toArray(children) as React.ReactElement[]; + return (rest: MenuProps) => { + const childrenArray = React.Children.toArray(rest.children) as React.ReactElement[]; if (__DEV__) { if (childrenArray.length !== 2) { diff --git a/packages/components/Menu/src/MenuCallout/MenuCallout.android.tsx b/packages/components/Menu/src/MenuCallout/MenuCallout.android.tsx index d9f2f06d416..097b99b3567 100644 --- a/packages/components/Menu/src/MenuCallout/MenuCallout.android.tsx +++ b/packages/components/Menu/src/MenuCallout/MenuCallout.android.tsx @@ -1,8 +1,6 @@ -import React from 'react'; import { Animated, Modal, TouchableWithoutFeedback, View, StyleSheet, ScrollView } from 'react-native'; -import { stagedComponent } from '@fluentui-react-native/framework'; -import { mergeProps } from '@fluentui-react-native/framework'; +import { mergeProps, phasedComponent } from '@fluentui-react-native/framework-base'; import type { MenuCalloutProps } from './MenuCallout.types'; import { menuCalloutName } from './MenuCallout.types'; @@ -10,11 +8,12 @@ import { useMenuContext } from '../context'; const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView); -export const MenuCallout = stagedComponent((props: MenuCalloutProps) => { +export const MenuCallout = phasedComponent((props: MenuCalloutProps) => { const context = useMenuContext(); - return (_rest: MenuCalloutProps, children: React.ReactNode) => { - const mergedProps = mergeProps(props, _rest); + return (innerProps: MenuCalloutProps) => { + const { children, ...rest } = mergeProps(props, innerProps); + const mergedProps = mergeProps(props, rest); const tokens = props.tokens; return ( diff --git a/packages/components/Menu/src/MenuCallout/MenuCallout.tsx b/packages/components/Menu/src/MenuCallout/MenuCallout.tsx index b0f655c3ec2..48774761396 100644 --- a/packages/components/Menu/src/MenuCallout/MenuCallout.tsx +++ b/packages/components/Menu/src/MenuCallout/MenuCallout.tsx @@ -1,15 +1,13 @@ -import React from 'react'; - import { Callout } from '@fluentui-react-native/callout'; -import { stagedComponent } from '@fluentui-react-native/framework'; -import { mergeProps } from '@fluentui-react-native/framework'; +import { mergeProps, phasedComponent } from '@fluentui-react-native/framework-base'; import type { MenuCalloutProps } from './MenuCallout.types'; import { menuCalloutName } from './MenuCallout.types'; -export const MenuCallout = stagedComponent((props: MenuCalloutProps) => { - return (_rest: MenuCalloutProps, children: React.ReactNode) => { - const mergedProps = mergeProps(props, _rest); +export const MenuCallout = phasedComponent((props: MenuCalloutProps) => { + return (innerProps: MenuCalloutProps) => { + const { children, ...rest } = innerProps; + const mergedProps = mergeProps(props, rest); return {children}; }; diff --git a/packages/components/Notification/src/Notification.helper.tsx b/packages/components/Notification/src/Notification.helper.tsx index 1f7692245b3..53470446b2a 100644 --- a/packages/components/Notification/src/Notification.helper.tsx +++ b/packages/components/Notification/src/Notification.helper.tsx @@ -2,7 +2,7 @@ import React from 'react'; import type { ButtonProps, ButtonTokens } from '@fluentui-react-native/button'; import { ButtonV1 as Button } from '@fluentui-react-native/button'; -import { mergeProps, stagedComponent } from '@fluentui-react-native/framework'; +import { mergeProps, phasedComponent } from '@fluentui-react-native/framework-base'; import type { SvgIconProps } from '@fluentui-react-native/icon'; import { createIconProps } from '@fluentui-react-native/icon'; import { globalTokens } from '@fluentui-react-native/theme-tokens'; @@ -59,31 +59,36 @@ export function createNotificationButtonProps(userProps: NotificationProps) { * (e.g. setting color in Notification.styling.ts will not apply to the action button text) * This helper component is used to customize tokens via props. */ -export const NotificationButton = stagedComponent((props: NotificationButtonProps) => { - const CustomizedButton = Button.customize({ - subtle: { - backgroundColor: 'transparent', - color: props.color, - iconColor: props.color, - disabled: { - color: props.disabledColor, - }, - pressed: { - color: props.pressedColor, - }, - }, - medium: { - hasContent: { - minWidth: 0, - padding: globalTokens.sizeNone, - paddingHorizontal: globalTokens.sizeNone, - variant: 'body2Strong', - }, - }, - }); +export const NotificationButton = phasedComponent((props: NotificationButtonProps) => { + const CustomizedButton = React.useMemo( + () => + Button.customize({ + subtle: { + backgroundColor: 'transparent', + color: props.color, + iconColor: props.color, + disabled: { + color: props.disabledColor, + }, + pressed: { + color: props.pressedColor, + }, + }, + medium: { + hasContent: { + minWidth: 0, + padding: globalTokens.sizeNone, + paddingHorizontal: globalTokens.sizeNone, + variant: 'body2Strong', + }, + }, + }), + [props.color, props.disabledColor, props.pressedColor], + ); - return (final: NotificationButtonProps, children: React.ReactNode) => { - const mergedProps = mergeProps(props, final); + return (final: NotificationButtonProps) => { + const { children, ...rest } = final; + const mergedProps = mergeProps(props, rest); return {children}; }; -}, true); +}); diff --git a/packages/components/TabList/src/TabListAnimatedIndicator/TabListAnimatedIndicator.tsx b/packages/components/TabList/src/TabListAnimatedIndicator/TabListAnimatedIndicator.tsx index 7aee136acde..69f1bdc461c 100644 --- a/packages/components/TabList/src/TabListAnimatedIndicator/TabListAnimatedIndicator.tsx +++ b/packages/components/TabList/src/TabListAnimatedIndicator/TabListAnimatedIndicator.tsx @@ -1,12 +1,12 @@ import { Animated } from 'react-native'; -import { stagedComponent } from '@fluentui-react-native/framework'; +import { phasedComponent } from '@fluentui-react-native/framework-base'; import type { AnimatedIndicatorProps } from './TabListAnimatedIndicator.types'; import { tablistAnimatedIndicatorName } from './TabListAnimatedIndicator.types'; import { useAnimatedIndicatorStyles } from './useAnimatedIndicatorStyles'; -export const TabListAnimatedIndicator = stagedComponent((props) => { +export const TabListAnimatedIndicator = phasedComponent((props) => { const styles = useAnimatedIndicatorStyles(props); return () => { return ; diff --git a/packages/components/TabList/src/TabListAnimatedIndicator/TabListAnimatedIndicator.win32.tsx b/packages/components/TabList/src/TabListAnimatedIndicator/TabListAnimatedIndicator.win32.tsx index 11732f552c9..7826f7a6bb4 100644 --- a/packages/components/TabList/src/TabListAnimatedIndicator/TabListAnimatedIndicator.win32.tsx +++ b/packages/components/TabList/src/TabListAnimatedIndicator/TabListAnimatedIndicator.win32.tsx @@ -1,7 +1,7 @@ import { View } from 'react-native'; import type { Animated, ViewProps, ViewStyle } from 'react-native'; -import { stagedComponent, memoize } from '@fluentui-react-native/framework'; +import { phasedComponent, memoize } from '@fluentui-react-native/framework-base'; import type { AnimatedIndicatorProps } from './TabListAnimatedIndicator.types'; import { tablistAnimatedIndicatorName } from './TabListAnimatedIndicator.types'; @@ -16,7 +16,7 @@ function indicatorPropsWorker(animationClass: string, style: Animated.AnimatedPr * This component renders as the indicator for the selected tab. Its styles are manually calculated using * changing layout stored in the tablist context, so it doesn't need to use the compose or compressible franework. */ -export const TabListAnimatedIndicator = stagedComponent((props) => { +export const TabListAnimatedIndicator = phasedComponent((props) => { const styles = useAnimatedIndicatorStyles(props); return () => { const indicatorProps = getIndicatorProps('Ribbon_TabUnderline', styles); diff --git a/packages/experimental/Overflow/package.json b/packages/experimental/Overflow/package.json index d9d4cbc6ac0..d19993e2a5b 100644 --- a/packages/experimental/Overflow/package.json +++ b/packages/experimental/Overflow/package.json @@ -32,7 +32,8 @@ "update-snapshots": "fluentui-scripts jest -u" }, "dependencies": { - "@fluentui-react-native/framework": "workspace:*" + "@fluentui-react-native/framework": "workspace:*", + "@fluentui-react-native/framework-base": "workspace:*" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/packages/experimental/Overflow/src/Overflow/Overflow.tsx b/packages/experimental/Overflow/src/Overflow/Overflow.tsx index 9ec996449dc..b1fa373dd66 100644 --- a/packages/experimental/Overflow/src/Overflow/Overflow.tsx +++ b/packages/experimental/Overflow/src/Overflow/Overflow.tsx @@ -1,17 +1,17 @@ -import * as React from 'react'; import { View } from 'react-native'; -import { mergeProps, stagedComponent } from '@fluentui-react-native/framework'; +import { mergeProps, phasedComponent } from '@fluentui-react-native/framework-base'; import type { OverflowProps } from './Overflow.types'; import { overflowName } from './Overflow.types'; import { useOverflow } from './useOverflow'; import { OverflowContext } from '../OverflowContext'; -export const Overflow = stagedComponent((initial: OverflowProps) => { +export const Overflow = phasedComponent((initial: OverflowProps) => { const { props, state } = useOverflow(initial); - return (final: OverflowProps, ...children: React.ReactNode[]) => { - const mergedProps = mergeProps(props, final); + return (final: OverflowProps) => { + const { children, ...rest } = final; + const mergedProps = mergeProps(props, rest); return ( {children} diff --git a/packages/experimental/Overflow/src/OverflowItem/OverflowItem.tsx b/packages/experimental/Overflow/src/OverflowItem/OverflowItem.tsx index 8ce16638f9b..2188390ddf6 100644 --- a/packages/experimental/Overflow/src/OverflowItem/OverflowItem.tsx +++ b/packages/experimental/Overflow/src/OverflowItem/OverflowItem.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import type { StyleProp, ViewProps, ViewStyle } from 'react-native'; -import { mergeProps, stagedComponent, memoize, mergeStyles } from '@fluentui-react-native/framework'; +import { mergeProps, phasedComponent, memoize, mergeStyles } from '@fluentui-react-native/framework-base'; import type { OverflowItemProps } from './OverflowItem.types'; import { overflowItemName } from './OverflowItem.types'; @@ -12,14 +12,15 @@ function overflowItemPropWorker(props: ViewProps, style: StyleProp): return { ...props, style }; } -export const OverflowItem = stagedComponent((userProps: OverflowItemProps) => { +export const OverflowItem = phasedComponent((userProps: OverflowItemProps) => { const { props, state } = useOverflowItem(userProps); - return (finalProps: OverflowItemProps, children: React.ReactNode) => { + return (finalProps: OverflowItemProps) => { + const { children, ...rest } = finalProps; if (state.layoutDone && !state.visible) { return null; } - const mergedProps = mergeProps(props, finalProps); + const mergedProps = mergeProps(props, rest); const childrenArray = React.Children.toArray(children); const child = childrenArray[0]; diff --git a/packages/experimental/Overflow/src/OverflowItem/OverflowItem.types.ts b/packages/experimental/Overflow/src/OverflowItem/OverflowItem.types.ts index 1197099c3e7..a6cbdea862d 100644 --- a/packages/experimental/Overflow/src/OverflowItem/OverflowItem.types.ts +++ b/packages/experimental/Overflow/src/OverflowItem/OverflowItem.types.ts @@ -1,3 +1,4 @@ +import type React from 'react'; import type { ViewProps } from 'react-native'; import type { OverflowItemChangeHandler } from '../Overflow/Overflow.types'; @@ -11,6 +12,8 @@ export interface OverflowItemProps extends ViewProps { priority?: number; /** Callback that runs whenever this item's visibility changes or whenever its dimensions should be manually set */ onOverflowItemChange?: OverflowItemChangeHandler; + /** Mark this as having exactly one child */ + children: React.ReactElement; } export interface OverflowItemState { diff --git a/packages/experimental/Shadow/package.json b/packages/experimental/Shadow/package.json index ee77f8d5f73..72dbdd7587e 100644 --- a/packages/experimental/Shadow/package.json +++ b/packages/experimental/Shadow/package.json @@ -33,6 +33,7 @@ }, "dependencies": { "@fluentui-react-native/framework": "workspace:*", + "@fluentui-react-native/framework-base": "workspace:*", "@fluentui-react-native/pressable": "workspace:*", "@fluentui-react-native/theme-types": "workspace:*" }, diff --git a/packages/experimental/Shadow/src/Shadow.tsx b/packages/experimental/Shadow/src/Shadow.tsx index 8e30aaff126..9b1a6f29fcb 100644 --- a/packages/experimental/Shadow/src/Shadow.tsx +++ b/packages/experimental/Shadow/src/Shadow.tsx @@ -2,36 +2,27 @@ import * as React from 'react'; import type { ViewStyle } from 'react-native'; import { View } from 'react-native'; -import { mergeProps, stagedComponent } from '@fluentui-react-native/framework'; -import { memoize } from '@fluentui-react-native/framework'; +import { memoize, mergeProps, phasedComponent } from '@fluentui-react-native/framework-base'; import type { ShadowToken } from '@fluentui-react-native/theme-types'; import type { ShadowProps } from './Shadow.types'; import { shadowName } from './Shadow.types'; import { getShadowTokenStyleSet } from './shadowStyle'; -export const Shadow = stagedComponent((props: ShadowProps) => { - return (final: ShadowProps, children: React.ReactNode) => { +export const Shadow = phasedComponent((props: ShadowProps) => { + return (final: ShadowProps) => { + const { children, ...rest } = final; if (!props.shadowToken) { return <>{children}; } - const childrenArray = React.Children.toArray(children) as React.ReactElement[]; - const child = childrenArray[0]; - - if (__DEV__) { - if (childrenArray.length !== 1) { - console.warn('Shadow must only have one child'); - } - } - - const { style: childStyle, ...restOfChildProps } = child.props; + const { style: childStyle, ...restOfChildProps } = children.props; const shadowViewStyleProps = getStylePropsForShadowViews(childStyle, props.shadowToken); const innerShadowViewProps = mergeProps(restOfChildProps, shadowViewStyleProps.inner); - const outerShadowViewProps = mergeProps(final, shadowViewStyleProps.outer); + const outerShadowViewProps = mergeProps(rest, shadowViewStyleProps.outer); - const childWithInnerShadow = React.cloneElement(child, innerShadowViewProps); + const childWithInnerShadow = React.cloneElement(children, innerShadowViewProps); return {childWithInnerShadow}; }; diff --git a/packages/experimental/Shadow/src/Shadow.types.ts b/packages/experimental/Shadow/src/Shadow.types.ts index 0822798c008..601d8e678aa 100644 --- a/packages/experimental/Shadow/src/Shadow.types.ts +++ b/packages/experimental/Shadow/src/Shadow.types.ts @@ -1,3 +1,4 @@ +import type React from 'react'; import type { ViewProps } from 'react-native'; import type { ShadowToken } from '@fluentui-react-native/theme-types'; @@ -6,4 +7,7 @@ export const shadowName = 'Shadow'; export interface ShadowProps extends ViewProps { shadowToken?: ShadowToken; + + /** Exactly one child */ + children: React.ReactElement; } diff --git a/packages/experimental/Tooltip/package.json b/packages/experimental/Tooltip/package.json index ada535f8f83..b4e6abcfa24 100644 --- a/packages/experimental/Tooltip/package.json +++ b/packages/experimental/Tooltip/package.json @@ -33,7 +33,7 @@ }, "dependencies": { "@fluentui-react-native/callout": "workspace:*", - "@fluentui-react-native/framework": "workspace:*" + "@fluentui-react-native/framework-base": "workspace:*" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/packages/experimental/Tooltip/src/Tooltip.tsx b/packages/experimental/Tooltip/src/Tooltip.tsx index e20acf27f20..d0355972a25 100644 --- a/packages/experimental/Tooltip/src/Tooltip.tsx +++ b/packages/experimental/Tooltip/src/Tooltip.tsx @@ -7,13 +7,13 @@ import * as React from 'react'; import { findNodeHandle } from 'react-native'; -import { mergeProps, stagedComponent } from '@fluentui-react-native/framework'; +import { mergeProps, phasedComponent } from '@fluentui-react-native/framework-base'; import type { TooltipProps } from './Tooltip.types'; import { tooltipName } from './Tooltip.types'; import NativeTooltipView from './TooltipNativeComponent'; -export const Tooltip = stagedComponent((props: TooltipProps) => { +export const Tooltip = phasedComponent((props: TooltipProps) => { const { target } = props; const [nativeTarget, setNativeTarget] = React.useState(null); @@ -31,7 +31,8 @@ export const Tooltip = stagedComponent((props: TooltipProps) => { } }, [target]); - const TooltipComponent = (rest: TooltipProps, children: React.ReactNode) => { + const TooltipComponent = (innerProps: TooltipProps) => { + const { children, ...rest } = innerProps; return ( {children} diff --git a/packages/framework-base/src/component-patterns/phasedComponent.ts b/packages/framework-base/src/component-patterns/phasedComponent.ts new file mode 100644 index 00000000000..a7d28a10261 --- /dev/null +++ b/packages/framework-base/src/component-patterns/phasedComponent.ts @@ -0,0 +1,25 @@ +import type React from 'react'; +import { jsx, jsxs } from '../jsx-runtime'; +import type { PhasedComponent, PhasedRender } from './render.types'; + +/** + * Take a phased render function and make a real component out of it, attaching the phased render function + * so it can be split if used in that manner. + * @param getInnerPhase - phased render function to wrap into a staged component + */ +export function phasedComponent(getInnerPhase: PhasedRender): PhasedComponent { + return Object.assign( + (props: React.PropsWithChildren) => { + // pull out children from props + const { children, ...outerProps } = props; + const Inner = getInnerPhase(outerProps as TProps); + + if (Array.isArray(children) && children.length > 1) { + return jsxs(Inner, { children }); + } else { + return jsx(Inner, { children }); + } + }, + { _phasedRender: getInnerPhase }, + ); +} diff --git a/packages/framework-base/src/component-patterns/render.types.ts b/packages/framework-base/src/component-patterns/render.types.ts index f86b39bbfe8..a84d18cd3b9 100644 --- a/packages/framework-base/src/component-patterns/render.types.ts +++ b/packages/framework-base/src/component-patterns/render.types.ts @@ -79,15 +79,14 @@ export type SlotFn = { * * The `children` prop will be automatically inferred and typed correctly by the prop type. Hooks are still expected */ -export type TwoStageRender = (props: TProps) => React.ComponentType>; +export type PhasedRender = (props: TProps) => React.ComponentType>; /** * Component type for a component that can be rendered in two stages, with the attached render function. */ -export type StagedComponent = React.FunctionComponent & { - _twoStageRender?: TwoStageRender; +export type PhasedComponent = React.FunctionComponent & { + _phasedRender?: PhasedRender; }; - /** * The final rendering of the props in a staged render. This is the function component signature that matches that of * React.createElement, children (if present) will be part of the variable args at the end. @@ -113,6 +112,6 @@ export type ComposableFunction = React.FunctionComponent & { _st export type AnyCustomType = | React.FunctionComponent | DirectComponent - | StagedComponent + | PhasedComponent | ComposableFunction | LegacyDirectComponent; diff --git a/packages/framework-base/src/component-patterns/stagedComponent.ts b/packages/framework-base/src/component-patterns/stagedComponent.ts new file mode 100644 index 00000000000..35a03214da7 --- /dev/null +++ b/packages/framework-base/src/component-patterns/stagedComponent.ts @@ -0,0 +1,24 @@ +import * as React from 'react'; + +import type { StagedRender, ComposableFunction } from './render.types'; + +function asArray(val: T | T[]): T[] { + return Array.isArray(val) ? val : [val]; +} + +/** + * Take a staged render function and make a real component out of it + * + * @param staged - staged render function to wrap into a staged component + * @param memo - optional flag to enable wrapping the created component in a React.memo HOC + * @deprecated Use phasedComponent from phasedComponent.ts instead + */ +export function stagedComponent(staged: StagedRender, memo?: boolean): ComposableFunction { + const component = (props: React.PropsWithChildren) => { + const { children, ...rest } = props; + return staged(rest as TProps)({} as React.PropsWithChildren, asArray(children)); + }; + const stagedComponent = memo ? React.memo(component) : component; + Object.assign(stagedComponent, { _staged: staged }); + return stagedComponent as ComposableFunction; +} diff --git a/packages/framework-base/src/component-patterns/stagedComponent.tsx b/packages/framework-base/src/component-patterns/stagedComponent.tsx deleted file mode 100644 index 492370ebe4e..00000000000 --- a/packages/framework-base/src/component-patterns/stagedComponent.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @jsxRuntime classic - * @jsx withSlots - */ -import * as React from 'react'; -import { withSlots } from './withSlots'; - -import type { StagedComponent, TwoStageRender, StagedRender, ComposableFunction } from './render.types'; - -function asArray(val: T | T[]): T[] { - return Array.isArray(val) ? val : [val]; -} - -/** - * Take a staged render function and make a real component out of it - * - * @param staged - staged render function to wrap into a staged component - * @param memo - optional flag to enable wrapping the created component in a React.memo HOC - */ -export function stagedComponent(staged: StagedRender, memo?: boolean): ComposableFunction { - const component = (props: React.PropsWithChildren) => { - const { children, ...rest } = props; - return staged(rest as TProps)({} as React.PropsWithChildren, asArray(children)); - }; - const stagedComponent = memo ? React.memo(component) : component; - Object.assign(stagedComponent, { _staged: staged }); - return stagedComponent as ComposableFunction; -} - -/** - * Take a two stage render function and make a real component out of it, attaching the staged render function - * so it can be split if used in that manner. - * @param staged - two stage render function to wrap into a staged component - */ -export function twoStageComponent(staged: TwoStageRender): StagedComponent { - return Object.assign( - (props: React.PropsWithChildren) => { - const { children, ...outerProps } = props; - const innerProps = { children } as React.PropsWithChildren; - const Inner = staged(outerProps as TProps); - return ; - }, - { _twoStageRender: staged }, - ); -} diff --git a/packages/framework-base/src/index.ts b/packages/framework-base/src/index.ts index 3a2fd0510d3..c37f7fdbcb4 100644 --- a/packages/framework-base/src/index.ts +++ b/packages/framework-base/src/index.ts @@ -25,16 +25,17 @@ export type { DirectComponent, DirectComponentFunction, LegacyDirectComponent, - StagedComponent, - StagedRender, - TwoStageRender, + PhasedComponent, + PhasedRender, RenderType, RenderResult, + StagedRender, ComposableFunction, FinalRender, SlotFn, NativeReactType, } from './component-patterns/render.types'; +export { phasedComponent } from './component-patterns/phasedComponent'; export { withSlots } from './component-patterns/withSlots'; -export { stagedComponent, twoStageComponent } from './component-patterns/stagedComponent'; +export { stagedComponent } from './component-patterns/stagedComponent'; export { jsx, jsxs } from './jsx-runtime'; diff --git a/yarn.lock b/yarn.lock index 80296201392..2422eae06f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3687,6 +3687,7 @@ __metadata: "@fluentui-react-native/babel-config": "workspace:*" "@fluentui-react-native/eslint-config-rules": "workspace:*" "@fluentui-react-native/framework": "workspace:*" + "@fluentui-react-native/framework-base": "workspace:*" "@fluentui-react-native/jest-config": "workspace:*" "@fluentui-react-native/kit-config": "workspace:*" "@fluentui-react-native/pressable": "workspace:*" @@ -3955,6 +3956,7 @@ __metadata: "@fluentui-react-native/babel-config": "workspace:*" "@fluentui-react-native/eslint-config-rules": "workspace:*" "@fluentui-react-native/framework": "workspace:*" + "@fluentui-react-native/framework-base": "workspace:*" "@fluentui-react-native/jest-config": "workspace:*" "@fluentui-react-native/kit-config": "workspace:*" "@fluentui-react-native/scripts": "workspace:*" @@ -4373,6 +4375,7 @@ __metadata: "@fluentui-react-native/button": "workspace:*" "@fluentui-react-native/eslint-config-rules": "workspace:*" "@fluentui-react-native/framework": "workspace:*" + "@fluentui-react-native/framework-base": "workspace:*" "@fluentui-react-native/jest-config": "workspace:*" "@fluentui-react-native/kit-config": "workspace:*" "@fluentui-react-native/menu": "workspace:*" @@ -5389,7 +5392,7 @@ __metadata: "@fluentui-react-native/button": "workspace:*" "@fluentui-react-native/callout": "workspace:*" "@fluentui-react-native/eslint-config-rules": "workspace:*" - "@fluentui-react-native/framework": "workspace:*" + "@fluentui-react-native/framework-base": "workspace:*" "@fluentui-react-native/jest-config": "workspace:*" "@fluentui-react-native/kit-config": "workspace:*" "@fluentui-react-native/scripts": "workspace:*" From d9aa22ec36d596de4d29602aee7e042f2e4ac71b Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Wed, 28 Jan 2026 15:30:38 -0800 Subject: [PATCH 02/15] Change files --- ...mental-shadow-e6c1af9b-c5aa-44e6-9b66-cd38207c7522.json | 7 +++++++ ...ramework-base-9f12c70a-179d-4a28-803a-73bbb2b3df97.json | 7 +++++++ ...t-native-icon-2d7e4a6e-0c5a-4f1f-b456-98beb80c1bff.json | 7 +++++++ ...t-native-menu-52a2dd2f-433b-4a98-bbd2-02d1ca0c4de3.json | 7 +++++++ ...-notification-39168bfc-7644-4bb1-b193-784c84c637a8.json | 7 +++++++ ...tive-overflow-913fd2cf-938c-4465-a1ae-3c42126abb1a.json | 7 +++++++ ...ative-tablist-eae99f12-b52e-426c-938f-460f65a5ce4f.json | 7 +++++++ ...ative-tooltip-d650af05-51c4-488a-878b-6dca19b33999.json | 7 +++++++ 8 files changed, 56 insertions(+) create mode 100644 change/@fluentui-react-native-experimental-shadow-e6c1af9b-c5aa-44e6-9b66-cd38207c7522.json create mode 100644 change/@fluentui-react-native-framework-base-9f12c70a-179d-4a28-803a-73bbb2b3df97.json create mode 100644 change/@fluentui-react-native-icon-2d7e4a6e-0c5a-4f1f-b456-98beb80c1bff.json create mode 100644 change/@fluentui-react-native-menu-52a2dd2f-433b-4a98-bbd2-02d1ca0c4de3.json create mode 100644 change/@fluentui-react-native-notification-39168bfc-7644-4bb1-b193-784c84c637a8.json create mode 100644 change/@fluentui-react-native-overflow-913fd2cf-938c-4465-a1ae-3c42126abb1a.json create mode 100644 change/@fluentui-react-native-tablist-eae99f12-b52e-426c-938f-460f65a5ce4f.json create mode 100644 change/@fluentui-react-native-tooltip-d650af05-51c4-488a-878b-6dca19b33999.json diff --git a/change/@fluentui-react-native-experimental-shadow-e6c1af9b-c5aa-44e6-9b66-cd38207c7522.json b/change/@fluentui-react-native-experimental-shadow-e6c1af9b-c5aa-44e6-9b66-cd38207c7522.json new file mode 100644 index 00000000000..ced6cf240ab --- /dev/null +++ b/change/@fluentui-react-native-experimental-shadow-e6c1af9b-c5aa-44e6-9b66-cd38207c7522.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "update phasedComponent pattern and switch direct stagedComponent callers to use it", + "packageName": "@fluentui-react-native/experimental-shadow", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-framework-base-9f12c70a-179d-4a28-803a-73bbb2b3df97.json b/change/@fluentui-react-native-framework-base-9f12c70a-179d-4a28-803a-73bbb2b3df97.json new file mode 100644 index 00000000000..24879b9ebe9 --- /dev/null +++ b/change/@fluentui-react-native-framework-base-9f12c70a-179d-4a28-803a-73bbb2b3df97.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "update phasedComponent pattern and switch direct stagedComponent callers to use it", + "packageName": "@fluentui-react-native/framework-base", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-icon-2d7e4a6e-0c5a-4f1f-b456-98beb80c1bff.json b/change/@fluentui-react-native-icon-2d7e4a6e-0c5a-4f1f-b456-98beb80c1bff.json new file mode 100644 index 00000000000..dbb1288ad69 --- /dev/null +++ b/change/@fluentui-react-native-icon-2d7e4a6e-0c5a-4f1f-b456-98beb80c1bff.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "update phasedComponent pattern and switch direct stagedComponent callers to use it", + "packageName": "@fluentui-react-native/icon", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-menu-52a2dd2f-433b-4a98-bbd2-02d1ca0c4de3.json b/change/@fluentui-react-native-menu-52a2dd2f-433b-4a98-bbd2-02d1ca0c4de3.json new file mode 100644 index 00000000000..0513030fb3b --- /dev/null +++ b/change/@fluentui-react-native-menu-52a2dd2f-433b-4a98-bbd2-02d1ca0c4de3.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "update phasedComponent pattern and switch direct stagedComponent callers to use it", + "packageName": "@fluentui-react-native/menu", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-notification-39168bfc-7644-4bb1-b193-784c84c637a8.json b/change/@fluentui-react-native-notification-39168bfc-7644-4bb1-b193-784c84c637a8.json new file mode 100644 index 00000000000..d7a9734f41c --- /dev/null +++ b/change/@fluentui-react-native-notification-39168bfc-7644-4bb1-b193-784c84c637a8.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "update phasedComponent pattern and switch direct stagedComponent callers to use it", + "packageName": "@fluentui-react-native/notification", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-overflow-913fd2cf-938c-4465-a1ae-3c42126abb1a.json b/change/@fluentui-react-native-overflow-913fd2cf-938c-4465-a1ae-3c42126abb1a.json new file mode 100644 index 00000000000..cf63194b97d --- /dev/null +++ b/change/@fluentui-react-native-overflow-913fd2cf-938c-4465-a1ae-3c42126abb1a.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "update phasedComponent pattern and switch direct stagedComponent callers to use it", + "packageName": "@fluentui-react-native/overflow", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-tablist-eae99f12-b52e-426c-938f-460f65a5ce4f.json b/change/@fluentui-react-native-tablist-eae99f12-b52e-426c-938f-460f65a5ce4f.json new file mode 100644 index 00000000000..2ad7de2a796 --- /dev/null +++ b/change/@fluentui-react-native-tablist-eae99f12-b52e-426c-938f-460f65a5ce4f.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "update phasedComponent pattern and switch direct stagedComponent callers to use it", + "packageName": "@fluentui-react-native/tablist", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-tooltip-d650af05-51c4-488a-878b-6dca19b33999.json b/change/@fluentui-react-native-tooltip-d650af05-51c4-488a-878b-6dca19b33999.json new file mode 100644 index 00000000000..98080bae984 --- /dev/null +++ b/change/@fluentui-react-native-tooltip-d650af05-51c4-488a-878b-6dca19b33999.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "update phasedComponent pattern and switch direct stagedComponent callers to use it", + "packageName": "@fluentui-react-native/tooltip", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} From f6b192859cd89211ee2d67a29b086337300caee6 Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Wed, 28 Jan 2026 18:10:42 -0800 Subject: [PATCH 03/15] fix use-slot to use new rendering patterns --- .../Dropdown/src/Dropdown/Dropdown.tsx | 3 +- .../src/component-patterns/directComponent.ts | 9 +++ .../src/component-patterns/phasedComponent.ts | 22 ++++- .../src/component-patterns/render.ts | 28 ++++--- packages/framework-base/src/index.ts | 10 ++- .../src/utilities/filterProps.ts | 14 ++++ .../framework/use-slot/src/useSlot.test.tsx | 28 ++++--- packages/framework/use-slot/src/useSlot.ts | 80 ++++++------------- 8 files changed, 107 insertions(+), 87 deletions(-) create mode 100644 packages/framework-base/src/component-patterns/directComponent.ts create mode 100644 packages/framework-base/src/utilities/filterProps.ts diff --git a/packages/experimental/Dropdown/src/Dropdown/Dropdown.tsx b/packages/experimental/Dropdown/src/Dropdown/Dropdown.tsx index 99b0d8986c1..b72a9c8d5c3 100644 --- a/packages/experimental/Dropdown/src/Dropdown/Dropdown.tsx +++ b/packages/experimental/Dropdown/src/Dropdown/Dropdown.tsx @@ -50,7 +50,8 @@ const Dropdown = compressible((props: DropdownPro [defaultRef], ); - const RootSlot = useSlot(View, props); + type PressableView = React.FunctionComponent; + const RootSlot = useSlot(View as unknown as PressableView, props); const ButtonSlot = useSlot(Button, buttonProps); const ExpandIconSlot = useSlot(Svg, expandIconProps); const ListboxSlot = useSlot(Listbox, listboxProps); diff --git a/packages/framework-base/src/component-patterns/directComponent.ts b/packages/framework-base/src/component-patterns/directComponent.ts new file mode 100644 index 00000000000..60cdb0d3487 --- /dev/null +++ b/packages/framework-base/src/component-patterns/directComponent.ts @@ -0,0 +1,9 @@ +import type React from 'react'; + +/** + * @param component functional component, usually a closure, to make into a direct component + * @return the same component with the direct component flag set, return type is a pure function component + */ +export function directComponent(component: React.FunctionComponent): React.FunctionComponent { + return Object.assign(component, { _callDirect: true }); +} diff --git a/packages/framework-base/src/component-patterns/phasedComponent.ts b/packages/framework-base/src/component-patterns/phasedComponent.ts index a7d28a10261..84bda02243c 100644 --- a/packages/framework-base/src/component-patterns/phasedComponent.ts +++ b/packages/framework-base/src/component-patterns/phasedComponent.ts @@ -1,6 +1,24 @@ -import type React from 'react'; +import React from 'react'; import { jsx, jsxs } from '../jsx-runtime'; -import type { PhasedComponent, PhasedRender } from './render.types'; +import type { ComposableFunction, PhasedComponent, PhasedRender } from './render.types'; + +export function getPhasedRender(component: React.ComponentType): PhasedRender | undefined { + // only a function component can have a phased render + if (typeof component !== 'function') { + // if this has a phased render function, return it + if ((component as PhasedComponent)._phasedRender) { + return (component as PhasedComponent)._phasedRender; + } else if ((component as ComposableFunction)._staged) { + // for backward compatibility check for staged render and return a wrapper that maps the signature + const staged = (component as ComposableFunction)._staged; + return (props: TProps) => { + const { children, ...rest } = props as React.PropsWithChildren; + return staged(rest as TProps, ...React.Children.toArray(children)); + }; + } + } + return undefined; +} /** * Take a phased render function and make a real component out of it, attaching the phased render function diff --git a/packages/framework-base/src/component-patterns/render.ts b/packages/framework-base/src/component-patterns/render.ts index 80f0848f2e3..e4a50123976 100644 --- a/packages/framework-base/src/component-patterns/render.ts +++ b/packages/framework-base/src/component-patterns/render.ts @@ -4,7 +4,7 @@ import type { RenderType, RenderResult, DirectComponent, LegacyDirectComponent } export type CustomRender = () => RenderResult; -function asDirectComponent(type: RenderType): DirectComponent | undefined { +export function asDirectComponent(type: RenderType): DirectComponent | undefined { if (typeof type === 'function' && (type as DirectComponent)._callDirect) { return type as DirectComponent; } @@ -22,7 +22,7 @@ export function renderForJsxRuntime( type: React.ElementType, props: React.PropsWithChildren, key?: React.Key, - jsxFn: typeof ReactJSX.jsx = ReactJSX.jsx, + jsxFn: typeof ReactJSX.jsx = undefined, ): RenderResult { const legacyDirect = asLegacyDirectComponent(type); if (legacyDirect) { @@ -35,20 +35,24 @@ export function renderForJsxRuntime( const newProps = { ...props, key }; return directComponent(newProps); } + + // auto-detect whether to use jsx or jsxs based on number of children, 0 or 1 = jsx, more than 1 = jsxs + if (!jsxFn) { + const children = props.children; + if (Array.isArray(children) && children.length > 1) { + jsxFn = ReactJSX.jsxs; + } else { + jsxFn = ReactJSX.jsx; + } + } + // now call the appropriate jsx function to return jsxFn(type, props, key); } export function renderForClassicRuntime(type: RenderType, props: TProps, ...children: React.ReactNode[]): RenderResult { - const legacyDirect = asLegacyDirectComponent(type); - if (legacyDirect) { - return legacyDirect(props, ...children) as RenderResult; - } - const directComponent = asDirectComponent(type); - if (directComponent) { - const newProps = { ...props, children }; - return directComponent(newProps); - } - return React.createElement(type, props, ...children); + // if it is a non-string type with _canCompose set just call the function directly, otherwise call createElement as normal + const propsWithChildren = { children, ...props }; + return renderForJsxRuntime(type as React.ElementType, propsWithChildren); } export const renderSlot = renderForClassicRuntime; diff --git a/packages/framework-base/src/index.ts b/packages/framework-base/src/index.ts index c37f7fdbcb4..9581c8453d1 100644 --- a/packages/framework-base/src/index.ts +++ b/packages/framework-base/src/index.ts @@ -20,10 +20,9 @@ export { mergeStyles } from './merge-props/mergeStyles'; export { mergeProps } from './merge-props/mergeProps'; // component pattern exports -export { renderForClassicRuntime, renderForJsxRuntime, renderSlot } from './component-patterns/render'; +export { renderForJsxRuntime, renderSlot, asDirectComponent } from './component-patterns/render'; export type { DirectComponent, - DirectComponentFunction, LegacyDirectComponent, PhasedComponent, PhasedRender, @@ -35,7 +34,12 @@ export type { SlotFn, NativeReactType, } from './component-patterns/render.types'; -export { phasedComponent } from './component-patterns/phasedComponent'; +export { directComponent } from './component-patterns/directComponent'; +export { getPhasedRender, phasedComponent } from './component-patterns/phasedComponent'; export { withSlots } from './component-patterns/withSlots'; export { stagedComponent } from './component-patterns/stagedComponent'; export { jsx, jsxs } from './jsx-runtime'; + +// general utilities +export { filterProps } from './utilities/filterProps'; +export type { PropsFilter } from './utilities/filterProps'; diff --git a/packages/framework-base/src/utilities/filterProps.ts b/packages/framework-base/src/utilities/filterProps.ts new file mode 100644 index 00000000000..b3ee7bbe935 --- /dev/null +++ b/packages/framework-base/src/utilities/filterProps.ts @@ -0,0 +1,14 @@ +import { mergeProps } from '../merge-props/mergeProps'; + +export type PropsFilter = (propName: string) => boolean; + +export function filterProps(props: TProps, filter?: PropsFilter): TProps { + if (filter && typeof props === 'object' && !Array.isArray(props)) { + const propsToRemove = filter ? Object.keys(props).filter((key) => !filter(key)) : undefined; + if (propsToRemove?.length > 0) { + const propsToRemoveObj = Object.fromEntries(propsToRemove.map((prop) => [prop, undefined])) as TProps; + return mergeProps(props, propsToRemoveObj); + } + } + return props; +} diff --git a/packages/framework/use-slot/src/useSlot.test.tsx b/packages/framework/use-slot/src/useSlot.test.tsx index f05d40778b3..2cfbe4f7d1c 100644 --- a/packages/framework/use-slot/src/useSlot.test.tsx +++ b/packages/framework/use-slot/src/useSlot.test.tsx @@ -6,16 +6,15 @@ import { Text, View } from 'react-native'; import { mergeStyles } from '@fluentui-react-native/framework-base'; import * as renderer from 'react-test-renderer'; -import type { NativeReactType } from '@fluentui-react-native/framework-base'; -import { stagedComponent } from '@fluentui-react-native/framework-base'; +import { phasedComponent } from '@fluentui-react-native/framework-base'; import { useSlot } from './useSlot'; -type PluggableTextProps = React.PropsWithChildren & { inner?: NativeReactType | React.FunctionComponent }; +type PluggableTextProps = React.PropsWithChildren & { inner?: React.FunctionComponent }; /** * Text component that demonstrates pluggability, in this case via passing an alternative component type into a prop called inner. */ -const PluggableText = stagedComponent((props: PluggableTextProps) => { +const PluggableText = phasedComponent((props: PluggableTextProps) => { // start by splitting inner and children from the incoming props const { inner, ...rest } = props; @@ -24,15 +23,15 @@ const PluggableText = stagedComponent((props: PluggableTextProps) => { const Inner = useSlot(inner || Text, rest); // return a closure for finishing off render - return (extra: TextProps, children: React.ReactNode) => {children}; + return (extra: TextProps) => { + // split children from extra props + const { children, ...rest } = extra; + return {children}; + }; }); PluggableText.displayName = 'PluggableText'; -const useStyledStagedText = ( - props: PluggableTextProps, - baseStyle: TextProps['style'], - inner?: NativeReactType | React.FunctionComponent, -) => { +const useStyledStagedText = (props: PluggableTextProps, baseStyle: TextProps['style'], inner?: React.FunctionComponent) => { // split out any passed in style const { style, ...rest } = props; @@ -43,10 +42,13 @@ const useStyledStagedText = ( const InnerText = useSlot(PluggableText, mergedProps); // return a closure to complete the staged pattern - return (extra: PluggableTextProps, children: React.ReactNode) => {children}; + return (extra: PluggableTextProps) => { + const { children, ...rest } = extra; + return {children}; + }; }; -const HeaderText = stagedComponent((props: PluggableTextProps) => { +const HeaderText = phasedComponent((props: PluggableTextProps) => { // could be done outside but showing the pattern of using useMemo to avoid creating a new object on every execution const baseStyle = React.useMemo(() => ({ fontSize: 24, fontWeight: 'bold' }), []); @@ -54,7 +56,7 @@ const HeaderText = stagedComponent((props: PluggableTextProps) => { return useStyledStagedText(props, baseStyle); }); -const CaptionText = stagedComponent((props: PluggableTextProps) => { +const CaptionText = phasedComponent((props: PluggableTextProps) => { // memo to not recreate style every time const baseStyle = React.useMemo(() => ({ fontFamily: 'Arial', fontWeight: '200' }), []); diff --git a/packages/framework/use-slot/src/useSlot.ts b/packages/framework/use-slot/src/useSlot.ts index d473437ac02..86fb0fb8017 100644 --- a/packages/framework/use-slot/src/useSlot.ts +++ b/packages/framework/use-slot/src/useSlot.ts @@ -1,72 +1,40 @@ import * as React from 'react'; -import { mergeProps } from '@fluentui-react-native/framework-base'; - -import type { SlotFn, NativeReactType, FinalRender } from '@fluentui-react-native/framework-base'; -import type { ComposableFunction, StagedRender } from '@fluentui-react-native/framework-base'; - -/** - * - * @param slot - component which may or may not be built using the staged pattern - * @returns - the staged function or undefined - */ -function getStagedRender(slot: NativeReactType | ComposableFunction): StagedRender | undefined { - return (typeof slot === 'function' && (slot as ComposableFunction)._staged) || undefined; -} +import { mergeProps, getPhasedRender, directComponent, renderForJsxRuntime, filterProps } from '@fluentui-react-native/framework-base'; +import type { PropsFilter } from '@fluentui-react-native/framework-base'; /** * useSlot hook function, allows authoring against pluggable slots as well as allowing components to be called as functions rather than * via createElement if they support it. * * @param component - any kind of component that can be rendered as part of the tree - * @param props - props, particularly the portion that includes styles, that should be passed to the component. These will be merged with what are specified in the JSX tree + * @param initialProps - props, particularly the portion that includes styles, that should be passed to the component. These will be merged with what are specified in the JSX tree * @param filter - optional filter that will prune the props before forwarding to the component * @returns */ export function useSlot( - component: NativeReactType | ComposableFunction, - props: TProps, - filter?: (propName: string) => boolean, -): SlotFn { - // some types to make things cleaner - type ResultHolder = { result: FinalRender | TProps }; - type MemoTuple = [SlotFn, ResultHolder]; - - // extract the staged component function if that pattern is being used, will be undefined if it is a standard component - const stagedComponent = getStagedRender(component); + component: React.ComponentType, + initialProps: TProps, + filter?: PropsFilter, +): React.ComponentType { + // filter the initial props if a filter is specified + const filteredProps = filterProps(initialProps, filter); // build the secondary processing function and the result holder, done via useMemo so the function identity stays the same. Rebuilding the closure every time would invalidate render - const [fn, results] = React.useMemo(() => { - // create a holder object so values can be passed to the closure - const resultHolder = {} as ResultHolder; - - // create a function that is in the right format for rendering in JSX/TSX, this has children split out - const slotFn: SlotFn = (extraProps: TProps, ...children: React.ReactNode[]) => { - const result = resultHolder.result; - - // result is either a function (if a staged component) or a set of props passed to useSlot (and sent here via resultHolder) - let props: TProps = typeof result === 'function' ? extraProps : mergeProps(result, extraProps); - - // if we have a filter specified, run it creating a prop collection of { [key]: undefined } which will end up deleting the values via mergeStyles - const propsToRemove = filter ? Object.keys(props).filter((key) => !filter(key)) : undefined; - if (propsToRemove?.length > 0) { - props = mergeProps(props, Object.assign({}, ...propsToRemove.map((prop) => ({ [prop]: undefined }))) as unknown as TProps); - } - - // now if result was a function then call it directly, if not go through the standard React.createElement process - // Type assertion is safe here because result is either FinalRender (from stagedComponent) or TProps (props object) - return typeof result === 'function' - ? (result as FinalRender)(props, ...children) - : React.createElement(component, props, ...children); - }; - // mark the slotFn so that withSlots knows to handle it differently - slotFn._canCompose = true; - return [slotFn, resultHolder]; + return React.useMemo>(() => { + // extract the phased component function if that pattern is being used, will be undefined if it is a standard component + const phasedRender = getPhasedRender(component); + + // do the first phase render with the initial props if we are using the staged pattern. This is typically getting + // styles and tokens in place a single time for the component. + const finalRender = phasedRender ? phasedRender(initialProps) : component; + + // now return a direct component function that can be used in JSX/TSX, this pattern is safe since we won't be using + // hooks in this closure + return directComponent((innerProps: TProps) => { + const finalInner = filterProps(innerProps, filter); + const finalProps = phasedRender ? finalInner : mergeProps(filteredProps, finalInner); + return renderForJsxRuntime(finalRender, finalProps); + }); }, [component, filter]); - - // if it is a staged component executre the first part with the props, otherwise just remember the props - results.result = stagedComponent ? stagedComponent(props) : props; - - // return the function - return fn; } From b5a490034b83e26ac4f8ca36e10eeb9f7f4bb8a3 Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Wed, 28 Jan 2026 18:11:18 -0800 Subject: [PATCH 04/15] Change files --- ...tive-dropdown-87c83670-53db-4b52-b2d1-85e71f24ba52.json | 7 +++++++ ...tive-use-slot-04b210fb-fca7-4403-818f-9560758017cb.json | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 change/@fluentui-react-native-dropdown-87c83670-53db-4b52-b2d1-85e71f24ba52.json create mode 100644 change/@fluentui-react-native-use-slot-04b210fb-fca7-4403-818f-9560758017cb.json diff --git a/change/@fluentui-react-native-dropdown-87c83670-53db-4b52-b2d1-85e71f24ba52.json b/change/@fluentui-react-native-dropdown-87c83670-53db-4b52-b2d1-85e71f24ba52.json new file mode 100644 index 00000000000..49e28bb60dd --- /dev/null +++ b/change/@fluentui-react-native-dropdown-87c83670-53db-4b52-b2d1-85e71f24ba52.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix use-slot to use new rendering patterns", + "packageName": "@fluentui-react-native/dropdown", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-use-slot-04b210fb-fca7-4403-818f-9560758017cb.json b/change/@fluentui-react-native-use-slot-04b210fb-fca7-4403-818f-9560758017cb.json new file mode 100644 index 00000000000..a581971db02 --- /dev/null +++ b/change/@fluentui-react-native-use-slot-04b210fb-fca7-4403-818f-9560758017cb.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix use-slot to use new rendering patterns", + "packageName": "@fluentui-react-native/use-slot", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} From b0eaea7498db29f8dc8d36dcf7f6c5d8f1134385 Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Thu, 29 Jan 2026 11:57:47 -0800 Subject: [PATCH 05/15] tighten up typing for framework, fixing the resulting errors --- .../CompoundButton/CompoundButton.mobile.tsx | 13 ++- .../src/ToggleButton/ToggleButton.android.tsx | 14 +++- .../Notification/src/Notification.tsx | 6 +- .../Notification/src/Notification.types.ts | 4 +- packages/components/Switch/src/Switch.tsx | 4 +- .../Checkbox/src/Checkbox.macos.tsx | 4 +- .../experimental/Expander/src/Expander.tsx | 2 +- .../Expander/src/Expander.types.ts | 3 +- .../Expander/src/ExpanderNativeComponent.ts | 6 +- .../MenuButton/src/MenuButton.types.ts | 10 ++- .../src/component-patterns/directComponent.ts | 4 +- .../src/component-patterns/phasedComponent.ts | 4 +- .../src/component-patterns/render.types.ts | 16 +++- packages/framework-base/src/index.ts | 1 + .../composition/src/composeFactory.ts | 20 +++-- packages/framework/use-slot/src/index.ts | 1 + .../framework/use-slot/src/useSlot.test.tsx | 11 +-- packages/framework/use-slot/src/useSlot.ts | 18 ++-- .../useSlots.samples.test.tsx.snap | 84 +++++++++---------- .../framework/use-slots/src/buildUseSlots.ts | 13 +-- .../use-slots/src/useSlots.samples.test.tsx | 47 +++++------ .../framework/use-styling/src/buildProps.ts | 4 +- 22 files changed, 163 insertions(+), 126 deletions(-) diff --git a/packages/components/Button/src/CompoundButton/CompoundButton.mobile.tsx b/packages/components/Button/src/CompoundButton/CompoundButton.mobile.tsx index 961cfaea3ad..59ad0f67a44 100644 --- a/packages/components/Button/src/CompoundButton/CompoundButton.mobile.tsx +++ b/packages/components/Button/src/CompoundButton/CompoundButton.mobile.tsx @@ -1,14 +1,21 @@ /** @jsxImportSource @fluentui-react-native/framework-base */ -import { View } from 'react-native'; +import { View, type ViewProps } from 'react-native'; import { compose } from '@fluentui-react-native/framework'; import { Icon } from '@fluentui-react-native/icon'; import { TextV1 as Text } from '@fluentui-react-native/text'; -import type { CompoundButtonType } from './CompoundButton.types'; +import type { CompoundButtonSlotProps, CompoundButtonType } from './CompoundButton.types'; import { compoundButtonName } from './CompoundButton.types'; -export const CompoundButton = compose({ +export interface MobileSlotProps extends CompoundButtonSlotProps { + root: ViewProps; +} +export interface CompoundButtonMobileType extends CompoundButtonType { + slotProps: MobileSlotProps; +} + +export const CompoundButton = compose({ displayName: compoundButtonName, slots: { root: View, diff --git a/packages/components/Button/src/ToggleButton/ToggleButton.android.tsx b/packages/components/Button/src/ToggleButton/ToggleButton.android.tsx index 7e0bc343764..edf7dea14ed 100644 --- a/packages/components/Button/src/ToggleButton/ToggleButton.android.tsx +++ b/packages/components/Button/src/ToggleButton/ToggleButton.android.tsx @@ -1,14 +1,22 @@ /** @jsxImportSource @fluentui-react-native/framework-base */ -import { View } from 'react-native'; +import { View, type ViewProps } from 'react-native'; import { compose } from '@fluentui-react-native/framework'; import { Icon } from '@fluentui-react-native/icon'; import { TextV1 as Text } from '@fluentui-react-native/text'; -import type { ToggleButtonType } from './ToggleButton.types'; +import type { ToggleButtonSlotProps, ToggleButtonType } from './ToggleButton.types'; import { toggleButtonName } from './ToggleButton.types'; -export const ToggleButton = compose({ +interface ToggleButtonSlotPropsAndroid extends ToggleButtonSlotProps { + root: ViewProps; +} + +interface ToggleButtonAndroidType extends ToggleButtonType { + slotProps: ToggleButtonSlotPropsAndroid; +} + +export const ToggleButton = compose({ displayName: toggleButtonName, slots: { root: View, diff --git a/packages/components/Notification/src/Notification.tsx b/packages/components/Notification/src/Notification.tsx index f3d1980ff3c..5e72a34b810 100644 --- a/packages/components/Notification/src/Notification.tsx +++ b/packages/components/Notification/src/Notification.tsx @@ -1,5 +1,5 @@ /** @jsxImportSource @fluentui-react-native/framework-base */ -import type { PressableProps, ViewStyle, ViewProps } from 'react-native'; +import type { ViewStyle, ViewProps } from 'react-native'; import { useWindowDimensions, View } from 'react-native'; import type { SizeClassIOS } from '@fluentui-react-native/experimental-appearance-additions'; @@ -9,7 +9,7 @@ import type { UseSlots } from '@fluentui-react-native/framework'; import { compose, mergeProps, memoize } from '@fluentui-react-native/framework'; import { Icon, createIconProps } from '@fluentui-react-native/icon'; import type { InteractionEvent } from '@fluentui-react-native/interactive-hooks'; -import { Pressable } from '@fluentui-react-native/pressable'; +import { type IPressableProps, Pressable } from '@fluentui-react-native/pressable'; import { Body2, Body2Strong } from '@fluentui-react-native/text'; import { NotificationButton, createNotificationButtonProps } from './Notification.helper'; @@ -54,7 +54,7 @@ export const Notification = compose({ return (final: NotificationProps, ...children: React.ReactNode[]) => { const { variant, icon, title, action, onActionPress, ...rest } = mergeProps(userProps, final); - const mergedProps = mergeProps(rest, rootStyle); + const mergedProps = mergeProps(rest, rootStyle); const iconProps = createIconProps(icon); const notificationButtonProps = createNotificationButtonProps(userProps); diff --git a/packages/components/Notification/src/Notification.types.ts b/packages/components/Notification/src/Notification.types.ts index 58ec13d41ae..56035d33668 100644 --- a/packages/components/Notification/src/Notification.types.ts +++ b/packages/components/Notification/src/Notification.types.ts @@ -1,4 +1,4 @@ -import type { PressableProps } from 'react-native'; +import type { IPressableProps } from '@fluentui-react-native/pressable'; import type { IViewProps, ITextProps } from '@fluentui-react-native/adapters'; import type { ButtonProps } from '@fluentui-react-native/button'; @@ -59,7 +59,7 @@ export type NotificationProps = React.PropsWithChildren<{ }>; export interface NotificationSlotProps { - root: PressableProps; + root: IPressableProps; icon?: IconProps; contentContainer: IViewProps; title?: ITextProps; diff --git a/packages/components/Switch/src/Switch.tsx b/packages/components/Switch/src/Switch.tsx index ed546c91caf..755cbf53b69 100644 --- a/packages/components/Switch/src/Switch.tsx +++ b/packages/components/Switch/src/Switch.tsx @@ -42,8 +42,8 @@ export const Switch = compose({ slots: { root: Pressable, label: Text, - track: Animated.View, // Conversion from View to Animated.View for Animated API to work - thumb: Animated.View, + track: Animated.View as unknown as typeof View, // Conversion from View to Animated.View for Animated API to work + thumb: Animated.View as unknown as typeof View, // Conversion from View to Animated.View for Animated API to work toggleContainer: View, onOffTextContainer: View, onOffText: Text, diff --git a/packages/experimental/Checkbox/src/Checkbox.macos.tsx b/packages/experimental/Checkbox/src/Checkbox.macos.tsx index 168bcf4fcde..eff1a7751ab 100644 --- a/packages/experimental/Checkbox/src/Checkbox.macos.tsx +++ b/packages/experimental/Checkbox/src/Checkbox.macos.tsx @@ -4,16 +4,16 @@ * @format */ /** @jsxImportSource @fluentui-react-native/framework-base */ -import type { IViewProps } from '@fluentui-react-native/adapters'; import type { CheckboxTokens, CheckboxProps, CheckboxState } from '@fluentui-react-native/checkbox'; import { checkboxName } from '@fluentui-react-native/checkbox'; import type { UseSlots } from '@fluentui-react-native/framework'; import { compose, mergeProps, buildProps } from '@fluentui-react-native/framework'; import NativeCheckboxView from './MacOSCheckboxNativeComponent'; +import type { NativeProps } from './MacOSCheckboxNativeComponent'; interface CheckboxSlotPropsMacOS { - root: React.PropsWithRef & { onPress: (e: any) => void }; + root: React.PropsWithRef; } interface CheckboxTypeMacOS { diff --git a/packages/experimental/Expander/src/Expander.tsx b/packages/experimental/Expander/src/Expander.tsx index 3f457ecc969..cc03e968d23 100644 --- a/packages/experimental/Expander/src/Expander.tsx +++ b/packages/experimental/Expander/src/Expander.tsx @@ -12,7 +12,7 @@ import { compose, mergeProps, buildProps } from '@fluentui-react-native/framewor import type { ExpanderType, ExpanderProps, ExpanderViewProps } from './Expander.types'; import { expanderName } from './Expander.types'; -import ExpanderComponent from './ExpanderNativeComponent'; +import { ExpanderComponent } from './ExpanderNativeComponent'; function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/packages/experimental/Expander/src/Expander.types.ts b/packages/experimental/Expander/src/Expander.types.ts index adc16ff164e..c9484156542 100644 --- a/packages/experimental/Expander/src/Expander.types.ts +++ b/packages/experimental/Expander/src/Expander.types.ts @@ -1,5 +1,6 @@ import type { PropsWithChildren } from 'react'; import type { ColorValue } from 'react-native'; +import type { NativeProps } from './ExpanderNativeComponent'; export const expanderName = 'Expander'; @@ -157,6 +158,6 @@ export interface ExpanderType { props: ExpanderProps; tokens: ExpanderTokens; slotProps: { - root: ExpanderViewProps; + root: NativeProps; }; } diff --git a/packages/experimental/Expander/src/ExpanderNativeComponent.ts b/packages/experimental/Expander/src/ExpanderNativeComponent.ts index 6e49281a4fd..fb4742ccad2 100644 --- a/packages/experimental/Expander/src/ExpanderNativeComponent.ts +++ b/packages/experimental/Expander/src/ExpanderNativeComponent.ts @@ -44,6 +44,6 @@ export interface NativeProps extends ViewProps { onExpanding?: DirectEventHandler; } -export default codegenNativeComponent( - 'ExpanderView' -) as HostComponent; \ No newline at end of file +export const ExpanderComponent: HostComponent = codegenNativeComponent('ExpanderView'); + +export default ExpanderComponent; diff --git a/packages/experimental/MenuButton/src/MenuButton.types.ts b/packages/experimental/MenuButton/src/MenuButton.types.ts index cd95116c153..da3a932358b 100644 --- a/packages/experimental/MenuButton/src/MenuButton.types.ts +++ b/packages/experimental/MenuButton/src/MenuButton.types.ts @@ -1,10 +1,14 @@ import type { ButtonProps } from '@fluentui-react-native/button'; import type { ContextualMenuItemProps, ContextualMenuProps, SubmenuProps } from '@fluentui-react-native/contextual-menu'; import type { FontTokens, IForegroundColorTokens, IBackgroundColorTokens, IBorderTokens } from '@fluentui-react-native/tokens'; -import type { SvgProps, XmlProps } from 'react-native-svg'; +import type { XmlProps } from 'react-native-svg'; export const menuButtonName = 'MenuButton'; +export interface FragmentProps { + children?: React.ReactNode; +} + export interface MenuButtonContext { showContextualMenu?: boolean; } @@ -30,8 +34,8 @@ export interface MenuButtonProps extends ButtonProps { } export type MenuButtonSlotProps = { - root: MenuButtonProps; - chevronIcon: SvgProps | XmlProps; + root: FragmentProps; + chevronIcon: XmlProps; }; export interface MenuButtonType { diff --git a/packages/framework-base/src/component-patterns/directComponent.ts b/packages/framework-base/src/component-patterns/directComponent.ts index 60cdb0d3487..4c085fc73db 100644 --- a/packages/framework-base/src/component-patterns/directComponent.ts +++ b/packages/framework-base/src/component-patterns/directComponent.ts @@ -1,9 +1,9 @@ -import type React from 'react'; +import type { FunctionComponent } from './render.types'; /** * @param component functional component, usually a closure, to make into a direct component * @return the same component with the direct component flag set, return type is a pure function component */ -export function directComponent(component: React.FunctionComponent): React.FunctionComponent { +export function directComponent(component: FunctionComponent): FunctionComponent { return Object.assign(component, { _callDirect: true }); } diff --git a/packages/framework-base/src/component-patterns/phasedComponent.ts b/packages/framework-base/src/component-patterns/phasedComponent.ts index 84bda02243c..b07f97e1ab3 100644 --- a/packages/framework-base/src/component-patterns/phasedComponent.ts +++ b/packages/framework-base/src/component-patterns/phasedComponent.ts @@ -1,6 +1,6 @@ import React from 'react'; import { jsx, jsxs } from '../jsx-runtime'; -import type { ComposableFunction, PhasedComponent, PhasedRender } from './render.types'; +import type { ComposableFunction, PhasedComponent, PhasedRender, FunctionComponent } from './render.types'; export function getPhasedRender(component: React.ComponentType): PhasedRender | undefined { // only a function component can have a phased render @@ -25,7 +25,7 @@ export function getPhasedRender(component: React.ComponentType): * so it can be split if used in that manner. * @param getInnerPhase - phased render function to wrap into a staged component */ -export function phasedComponent(getInnerPhase: PhasedRender): PhasedComponent { +export function phasedComponent(getInnerPhase: PhasedRender): FunctionComponent { return Object.assign( (props: React.PropsWithChildren) => { // pull out children from props diff --git a/packages/framework-base/src/component-patterns/render.types.ts b/packages/framework-base/src/component-patterns/render.types.ts index a84d18cd3b9..914ae429321 100644 --- a/packages/framework-base/src/component-patterns/render.types.ts +++ b/packages/framework-base/src/component-patterns/render.types.ts @@ -29,12 +29,20 @@ export type NativeReactType = RenderType; /** * type of the render function, not a FunctionComponent to help prevent hook usage */ -export type DirectComponentFunction = (props: TProps) => RenderResult; +export type FunctionComponentCore = (props: TProps) => RenderResult; + +/** + * A function component that returns an element type. This allows for the empty call props usage for native + * components, as well as handles the returns of React components. + */ +export type FunctionComponent = FunctionComponentCore & { + displayName?: string; +}; /** * The full component definition that has the attached properties to allow the jsx handlers to render it directly. */ -export type DirectComponent = DirectComponentFunction & { +export type DirectComponent = FunctionComponentCore & { displayName?: string; _callDirect?: boolean; }; @@ -84,7 +92,7 @@ export type PhasedRender = (props: TProps) => React.ComponentType = React.FunctionComponent & { +export type PhasedComponent = FunctionComponent & { _phasedRender?: PhasedRender; }; /** @@ -103,7 +111,7 @@ export type StagedRender = (props: TProps, ...args: any[]) => FinalRende * Signature for a component that uses the staged render pattern. * @deprecated Use TwoStageRender instead */ -export type ComposableFunction = React.FunctionComponent & { _staged?: StagedRender }; +export type ComposableFunction = FunctionComponent & { _staged?: StagedRender }; /** * A type aggregating all the custom types that can be used in the render process. diff --git a/packages/framework-base/src/index.ts b/packages/framework-base/src/index.ts index 9581c8453d1..ac66d45af60 100644 --- a/packages/framework-base/src/index.ts +++ b/packages/framework-base/src/index.ts @@ -23,6 +23,7 @@ export { mergeProps } from './merge-props/mergeProps'; export { renderForJsxRuntime, renderSlot, asDirectComponent } from './component-patterns/render'; export type { DirectComponent, + FunctionComponent, LegacyDirectComponent, PhasedComponent, PhasedRender, diff --git a/packages/framework/composition/src/composeFactory.ts b/packages/framework/composition/src/composeFactory.ts index c7fc4c32dcf..f550b7243db 100644 --- a/packages/framework/composition/src/composeFactory.ts +++ b/packages/framework/composition/src/composeFactory.ts @@ -36,9 +36,9 @@ export type ComposeFactoryOptions = ComposableFunction & { __options: ComposeFactoryOptions; customize: (...tokens: TokenSettings[]) => ComposeFactoryComponent; - compose: ( - options: Partial>, - ) => ComposeFactoryComponent; + compose( + options: Partial>, + ): ComposeFactoryComponent; } & TStatics; /** @@ -79,9 +79,17 @@ export function composeFactory) => - composeFactory( - immutableMergeCore(mergeOptions, options, customOptions) as LocalOptions, + component.compose = ( + customOptions: Partial>, + ) => + composeFactory( + immutableMergeCore(mergeOptions, options, customOptions as object) as unknown as ComposeFactoryOptions< + TProps, + TOverrideSlotProps, + TTokens, + TTheme, + TStatics + >, themeHelper, ); diff --git a/packages/framework/use-slot/src/index.ts b/packages/framework/use-slot/src/index.ts index 8795ea49ed5..366f91eebc6 100644 --- a/packages/framework/use-slot/src/index.ts +++ b/packages/framework/use-slot/src/index.ts @@ -1,4 +1,5 @@ export { useSlot } from './useSlot'; +export type { ComponentType } from './useSlot'; // re-export functions and types from framework-base that used to be here to not break existing imports export { renderSlot, stagedComponent, withSlots } from '@fluentui-react-native/framework-base'; diff --git a/packages/framework/use-slot/src/useSlot.test.tsx b/packages/framework/use-slot/src/useSlot.test.tsx index 2cfbe4f7d1c..fe61bab2836 100644 --- a/packages/framework/use-slot/src/useSlot.test.tsx +++ b/packages/framework/use-slot/src/useSlot.test.tsx @@ -3,13 +3,13 @@ import * as React from 'react'; import type { TextProps } from 'react-native'; import { Text, View } from 'react-native'; -import { mergeStyles } from '@fluentui-react-native/framework-base'; +import { type FunctionComponent, mergeStyles } from '@fluentui-react-native/framework-base'; import * as renderer from 'react-test-renderer'; import { phasedComponent } from '@fluentui-react-native/framework-base'; import { useSlot } from './useSlot'; -type PluggableTextProps = React.PropsWithChildren & { inner?: React.FunctionComponent }; +type PluggableTextProps = TextProps & { inner?: FunctionComponent }; /** * Text component that demonstrates pluggability, in this case via passing an alternative component type into a prop called inner. @@ -32,11 +32,8 @@ const PluggableText = phasedComponent((props: PluggableTextProps) => { PluggableText.displayName = 'PluggableText'; const useStyledStagedText = (props: PluggableTextProps, baseStyle: TextProps['style'], inner?: React.FunctionComponent) => { - // split out any passed in style - const { style, ...rest } = props; - // create merged props to pass in to the inner slot - const mergedProps = { ...rest, style: mergeStyles(baseStyle, style), ...(inner && { inner }) }; + const mergedProps = { ...props, style: mergeStyles(baseStyle, props.style), ...(inner && { inner }) } as PluggableTextProps; // create a slot based on the pluggable text const InnerText = useSlot(PluggableText, mergedProps); @@ -65,7 +62,7 @@ const CaptionText = phasedComponent((props: PluggableTextProps) => { }); // Control authored as simple containment -const HeaderCaptionText1 = (props: React.PropsWithChildren) => { +const HeaderCaptionText1 = (props: TextProps) => { const { children, ...rest } = props; const baseStyle = React.useMemo(() => ({ fontSize: 24, fontWeight: 'bold' }), []); const mergedProps = { ...rest, style: mergeStyles(baseStyle, props.style) }; diff --git a/packages/framework/use-slot/src/useSlot.ts b/packages/framework/use-slot/src/useSlot.ts index 86fb0fb8017..c3c7e566abf 100644 --- a/packages/framework/use-slot/src/useSlot.ts +++ b/packages/framework/use-slot/src/useSlot.ts @@ -1,7 +1,9 @@ import * as React from 'react'; import { mergeProps, getPhasedRender, directComponent, renderForJsxRuntime, filterProps } from '@fluentui-react-native/framework-base'; -import type { PropsFilter } from '@fluentui-react-native/framework-base'; +import type { PropsFilter, FunctionComponent } from '@fluentui-react-native/framework-base'; + +export type ComponentType = React.ComponentType; /** * useSlot hook function, allows authoring against pluggable slots as well as allowing components to be called as functions rather than @@ -14,27 +16,27 @@ import type { PropsFilter } from '@fluentui-react-native/framework-base'; */ export function useSlot( component: React.ComponentType, - initialProps: TProps, + initialProps: Partial, filter?: PropsFilter, -): React.ComponentType { +): FunctionComponent { // filter the initial props if a filter is specified const filteredProps = filterProps(initialProps, filter); // build the secondary processing function and the result holder, done via useMemo so the function identity stays the same. Rebuilding the closure every time would invalidate render - return React.useMemo>(() => { + return React.useMemo>(() => { // extract the phased component function if that pattern is being used, will be undefined if it is a standard component - const phasedRender = getPhasedRender(component); + const phasedRender = getPhasedRender(component as React.ComponentType); // do the first phase render with the initial props if we are using the staged pattern. This is typically getting // styles and tokens in place a single time for the component. - const finalRender = phasedRender ? phasedRender(initialProps) : component; + const finalRender = phasedRender ? phasedRender(initialProps as TProps) : component; // now return a direct component function that can be used in JSX/TSX, this pattern is safe since we won't be using // hooks in this closure return directComponent((innerProps: TProps) => { - const finalInner = filterProps(innerProps, filter); + const finalInner = filterProps(innerProps, filter); const finalProps = phasedRender ? finalInner : mergeProps(filteredProps, finalInner); - return renderForJsxRuntime(finalRender, finalProps); + return renderForJsxRuntime(finalRender as React.ComponentType, finalProps); }); }, [component, filter]); } diff --git a/packages/framework/use-slots/src/__snapshots__/useSlots.samples.test.tsx.snap b/packages/framework/use-slots/src/__snapshots__/useSlots.samples.test.tsx.snap index 19df4c49d9e..f2bfa190bc7 100644 --- a/packages/framework/use-slots/src/__snapshots__/useSlots.samples.test.tsx.snap +++ b/packages/framework/use-slots/src/__snapshots__/useSlots.samples.test.tsx.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`useSlots sample code test suite renders sample 1 - the two types of basic bold text components 1`] = ` -
- + Staged component at one level - - + Standard component of a single level - -
+ + `; exports[`useSlots sample code test suite renders sample 2 = the two types of two level header components 1`] = `
- Staged component with two levels - - + Standard component with two levels - +
`; @@ -57,7 +57,7 @@ exports[`useSlots sample code test suite renders sample 3 - the two types of hig --- SIMPLE USAGE COMPARISON --- -
- Standard HOC - -
-
+ + - Staged HOC - -
+ + --- COMPARISON WITH CAPTIONS --- -
- Standard HOC with Caption - - + Caption text - -
-
+ + - Staged HOC with Caption - - + Caption text - -
+ + --- COMPARISON WITH CAPTIONS AND CUSTOMIZATIONS --- -
- Standard HOC with caption and customizations - - + Caption text - -
-
+ + - Staged HOC with caption and customizations - - + Caption text - -
+ + `; diff --git a/packages/framework/use-slots/src/buildUseSlots.ts b/packages/framework/use-slots/src/buildUseSlots.ts index 0ed9492d54f..8a7cdfcd74d 100644 --- a/packages/framework/use-slots/src/buildUseSlots.ts +++ b/packages/framework/use-slots/src/buildUseSlots.ts @@ -1,5 +1,5 @@ -import type { ComposableFunction, SlotFn, NativeReactType } from '@fluentui-react-native/framework-base'; -import { useSlot } from '@fluentui-react-native/use-slot'; +import { useSlot, type ComponentType } from '@fluentui-react-native/use-slot'; +import type { FunctionComponent, PropsFilter } from '@fluentui-react-native/framework-base'; // type AsObject = T extends object ? T : never @@ -8,11 +8,11 @@ import { useSlot } from '@fluentui-react-native/use-slot'; */ type UseStyling = (...props: unknown[]) => TSlotProps; -export type Slots = { [K in keyof TSlotProps]: SlotFn }; +export type Slots = { [K in keyof TSlotProps]: FunctionComponent }; export type UseSlotOptions = { - slots: { [K in keyof TSlotProps]: NativeReactType | ComposableFunction }; - filters?: { [K in keyof TSlotProps]?: (propName: string) => boolean }; + slots: { [K in keyof TSlotProps]: ComponentType }; + filters?: { [K in keyof TSlotProps]?: PropsFilter }; useStyling?: TSlotProps | GetSlotProps; }; @@ -31,6 +31,9 @@ export function buildUseSlots(options: UseSlotOptions): const builtSlots: Slots = {} as Slots; // for each slot go through and either cache the slot props or call part one render if it is staged + + // note: changing this to a for..in loop causes rule of hooks violations + // eslint-disable-next-line @rnx-kit/no-foreach-with-captured-variables Object.keys(slots).forEach((slotName) => { builtSlots[slotName] = useSlot(slots[slotName], slotProps[slotName], filters[slotName]); }); diff --git a/packages/framework/use-slots/src/useSlots.samples.test.tsx b/packages/framework/use-slots/src/useSlots.samples.test.tsx index 72be5e9ff0a..804c04ee73c 100644 --- a/packages/framework/use-slots/src/useSlots.samples.test.tsx +++ b/packages/framework/use-slots/src/useSlots.samples.test.tsx @@ -1,18 +1,12 @@ /** @jsxImportSource @fluentui-react-native/framework-base */ -import type { CSSProperties } from 'react'; - import { mergeProps } from '@fluentui-react-native/framework-base'; -import { stagedComponent } from '@fluentui-react-native/framework-base'; +import { phasedComponent } from '@fluentui-react-native/framework-base'; import * as renderer from 'react-test-renderer'; +import { View, Text } from 'react-native'; +import type { ViewProps, TextProps, ViewStyle, TextStyle } from 'react-native'; import { buildUseSlots } from './buildUseSlots'; -// types for web -type TextProps = { style?: CSSProperties }; -type ViewProps = { style?: CSSProperties }; -type ViewStyle = CSSProperties; -type TextStyle = CSSProperties; - /** * This file contains samples and description to help explain what the useSlots hook does and why it is useful * for building components. @@ -46,7 +40,7 @@ describe('useSlots sample code test suite', () => { * Now render the text, merging the baseProps with the style updates with the rest param. Note that this leverages the fact * that mergeProps will reliably produce style objects with the same reference, given the same inputs. */ - return {children}; + return {children}; }; BoldTextStandard.displayName = 'BoldTextStandard'; @@ -54,19 +48,21 @@ describe('useSlots sample code test suite', () => { * To write the same component using the staged pattern is only slightly more complex. The pattern involves splitting the component rendering into * two parts and executing any hooks in the first part. * - * The stagedComponent function takes an input function of this form and wraps it in a function component that react knows how to render + * The phasedComponent function takes an input function of this form and wraps it in a function component that react knows how to render */ - const BoldTextStaged = stagedComponent((props: React.PropsWithChildren) => { + const BoldTextStaged = phasedComponent((props: React.PropsWithChildren) => { /** * This section would be where hook/styling code would go, props here would include everything coming in from the base react tree with the * exception of children, which will be passed in stage 2. */ - return (extra: TextProps, children: React.ReactNode) => { + return (extra: React.PropsWithChildren) => { /** * extra are additional props that may be filled in by a higher order component. They should not include styling and are only props the * enclosing component are passing to the JSX elements */ - return {children}; + + const { children, ...rest } = extra; + return {children}; }; }); BoldTextStaged.displayName = 'BoldTextStaged'; @@ -79,10 +75,10 @@ describe('useSlots sample code test suite', () => { */ const wrapper = renderer .create( -
+ Staged component at one level Standard component of a single level -
, + , ) .toJSON(); expect(wrapper).toMatchSnapshot(); @@ -120,7 +116,7 @@ describe('useSlots sample code test suite', () => { /** * Now author the staged component using the slot hook */ - const HeaderStaged = stagedComponent((props: React.PropsWithChildren) => { + const HeaderStaged = phasedComponent((props: React.PropsWithChildren) => { /** * Call the slots hook (or any hook) outside of the inner closure. The useSlots hook will return an object with each slot as a renderable * function. The hooks for sub-components will be called as part of this call. Props passed in at this point will be the props that appear @@ -131,7 +127,8 @@ describe('useSlots sample code test suite', () => { const BoldText = useHeaderSlots(props).text; /** Now the inner closure, pretty much the same as before */ - return (extra: TextProps, children: React.ReactNode) => { + return (extra: TextProps) => { + const { children, ...rest } = extra; /** * Instead of rendering the component directly we render using the slot. If this is a staged component it will call the * inner closure directly, without going through createElement. Entries passed into the JSX, including children, are what appear in the @@ -140,7 +137,7 @@ describe('useSlots sample code test suite', () => { * NOTE: this requires using the withSlots helper via the jsx directive. This knows how to pick apart the entries and just call the second * part of the function */ - return {children}; + return {children}; }; }); HeaderStaged.displayName = 'HeaderStaged'; @@ -198,10 +195,10 @@ describe('useSlots sample code test suite', () => { const headerColorProps = getColorProps(headerColor); const captionColorProps = getColorProps(captionColor); return ( -
+ {children} {captionText && {captionText}} -
+ ); }; CaptionedHeaderStandard.displayName = `CaptionedHeaderStandard';`; @@ -212,7 +209,7 @@ describe('useSlots sample code test suite', () => { const useCaptionedHeaderSlots = buildUseSlots({ /** Slots are just like above, this component will have three sub-components */ slots: { - container: 'div', + container: View, header: HeaderStaged, caption: BoldTextStaged, }, @@ -230,12 +227,12 @@ describe('useSlots sample code test suite', () => { /** * now use the hook to implement it as a staged component */ - const CaptionedHeaderStaged = stagedComponent>((props) => { + const CaptionedHeaderStaged = phasedComponent>((props) => { // At the point where this is called the slots are initialized with the initial prop values from useStyling above const Slots = useCaptionedHeaderSlots(props); - return (extra: HeaderWithCaptionProps, children: React.ReactNode) => { + return (extra: HeaderWithCaptionProps) => { // merge the props together, picking out the caption text and clearing any custom values we don't want forwarded to the view - const { captionText, ...rest } = mergeProps(props, extra, clearCustomProps); + const { children, captionText, ...rest } = mergeProps(props, extra, clearCustomProps); // now render using the slots. Any values passed in via JSX will be merged with values from the slot hook above return ( diff --git a/packages/framework/use-styling/src/buildProps.ts b/packages/framework/use-styling/src/buildProps.ts index ff5a49c0dae..7686421e2ff 100644 --- a/packages/framework/use-styling/src/buildProps.ts +++ b/packages/framework/use-styling/src/buildProps.ts @@ -97,10 +97,10 @@ export function refinePropsFunctions( mask: TokensThatAreAlsoProps, ): BuildSlotProps { const result = {}; - Object.keys(styles).forEach((key) => { + for (const key of Object.keys(styles)) { const refine = typeof styles[key] === 'function' && (styles[key] as RefinableBuildPropsBase).refine; result[key] = refine ? refine(mask) : styles[key]; - }); + } return result; } From 692d9946a4d05ceab4a234b2b3aba4e44038fa56 Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Thu, 29 Jan 2026 11:58:16 -0800 Subject: [PATCH 06/15] Change files --- ...native-button-720b78fc-c110-4878-8d65-4b9e3f2598e9.json | 7 +++++++ ...e-composition-3f1b63a7-572b-41d3-aa58-4e9e63a67d7b.json | 7 +++++++ ...ntal-checkbox-272753d7-8b40-4478-b320-23b75fda2f54.json | 7 +++++++ ...ntal-expander-f3f75eb3-6a92-43b2-99d7-9d8eaae37046.json | 7 +++++++ ...l-menu-button-87ab93a8-c1d4-4b77-95ae-85825caa81f7.json | 7 +++++++ ...native-switch-e9541e88-729c-46a0-b35a-8e6caa138780.json | 7 +++++++ ...ive-use-slots-0995e553-335e-4dd8-b247-323d189a4a9d.json | 7 +++++++ ...e-use-styling-563d6e04-7aa2-45a0-b967-6f289375c2b2.json | 7 +++++++ 8 files changed, 56 insertions(+) create mode 100644 change/@fluentui-react-native-button-720b78fc-c110-4878-8d65-4b9e3f2598e9.json create mode 100644 change/@fluentui-react-native-composition-3f1b63a7-572b-41d3-aa58-4e9e63a67d7b.json create mode 100644 change/@fluentui-react-native-experimental-checkbox-272753d7-8b40-4478-b320-23b75fda2f54.json create mode 100644 change/@fluentui-react-native-experimental-expander-f3f75eb3-6a92-43b2-99d7-9d8eaae37046.json create mode 100644 change/@fluentui-react-native-experimental-menu-button-87ab93a8-c1d4-4b77-95ae-85825caa81f7.json create mode 100644 change/@fluentui-react-native-switch-e9541e88-729c-46a0-b35a-8e6caa138780.json create mode 100644 change/@fluentui-react-native-use-slots-0995e553-335e-4dd8-b247-323d189a4a9d.json create mode 100644 change/@fluentui-react-native-use-styling-563d6e04-7aa2-45a0-b967-6f289375c2b2.json diff --git a/change/@fluentui-react-native-button-720b78fc-c110-4878-8d65-4b9e3f2598e9.json b/change/@fluentui-react-native-button-720b78fc-c110-4878-8d65-4b9e3f2598e9.json new file mode 100644 index 00000000000..2dc890e24f6 --- /dev/null +++ b/change/@fluentui-react-native-button-720b78fc-c110-4878-8d65-4b9e3f2598e9.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "tighten up typing for framework, fixing the resulting errors", + "packageName": "@fluentui-react-native/button", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-composition-3f1b63a7-572b-41d3-aa58-4e9e63a67d7b.json b/change/@fluentui-react-native-composition-3f1b63a7-572b-41d3-aa58-4e9e63a67d7b.json new file mode 100644 index 00000000000..1a913ed1910 --- /dev/null +++ b/change/@fluentui-react-native-composition-3f1b63a7-572b-41d3-aa58-4e9e63a67d7b.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "tighten up typing for framework, fixing the resulting errors", + "packageName": "@fluentui-react-native/composition", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-experimental-checkbox-272753d7-8b40-4478-b320-23b75fda2f54.json b/change/@fluentui-react-native-experimental-checkbox-272753d7-8b40-4478-b320-23b75fda2f54.json new file mode 100644 index 00000000000..3defd327817 --- /dev/null +++ b/change/@fluentui-react-native-experimental-checkbox-272753d7-8b40-4478-b320-23b75fda2f54.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "tighten up typing for framework, fixing the resulting errors", + "packageName": "@fluentui-react-native/experimental-checkbox", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-experimental-expander-f3f75eb3-6a92-43b2-99d7-9d8eaae37046.json b/change/@fluentui-react-native-experimental-expander-f3f75eb3-6a92-43b2-99d7-9d8eaae37046.json new file mode 100644 index 00000000000..cbc90bce730 --- /dev/null +++ b/change/@fluentui-react-native-experimental-expander-f3f75eb3-6a92-43b2-99d7-9d8eaae37046.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "tighten up typing for framework, fixing the resulting errors", + "packageName": "@fluentui-react-native/experimental-expander", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-experimental-menu-button-87ab93a8-c1d4-4b77-95ae-85825caa81f7.json b/change/@fluentui-react-native-experimental-menu-button-87ab93a8-c1d4-4b77-95ae-85825caa81f7.json new file mode 100644 index 00000000000..c9b2ab8e403 --- /dev/null +++ b/change/@fluentui-react-native-experimental-menu-button-87ab93a8-c1d4-4b77-95ae-85825caa81f7.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "tighten up typing for framework, fixing the resulting errors", + "packageName": "@fluentui-react-native/experimental-menu-button", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-switch-e9541e88-729c-46a0-b35a-8e6caa138780.json b/change/@fluentui-react-native-switch-e9541e88-729c-46a0-b35a-8e6caa138780.json new file mode 100644 index 00000000000..c63c36c93fd --- /dev/null +++ b/change/@fluentui-react-native-switch-e9541e88-729c-46a0-b35a-8e6caa138780.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "tighten up typing for framework, fixing the resulting errors", + "packageName": "@fluentui-react-native/switch", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-use-slots-0995e553-335e-4dd8-b247-323d189a4a9d.json b/change/@fluentui-react-native-use-slots-0995e553-335e-4dd8-b247-323d189a4a9d.json new file mode 100644 index 00000000000..30dd40aa74e --- /dev/null +++ b/change/@fluentui-react-native-use-slots-0995e553-335e-4dd8-b247-323d189a4a9d.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "tighten up typing for framework, fixing the resulting errors", + "packageName": "@fluentui-react-native/use-slots", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-use-styling-563d6e04-7aa2-45a0-b967-6f289375c2b2.json b/change/@fluentui-react-native-use-styling-563d6e04-7aa2-45a0-b967-6f289375c2b2.json new file mode 100644 index 00000000000..6cf42e8089a --- /dev/null +++ b/change/@fluentui-react-native-use-styling-563d6e04-7aa2-45a0-b967-6f289375c2b2.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "tighten up typing for framework, fixing the resulting errors", + "packageName": "@fluentui-react-native/use-styling", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} From 2821ad866c1a1e5f2d341cfcba5f07f682caa368 Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Thu, 29 Jan 2026 12:36:20 -0800 Subject: [PATCH 07/15] fix break from merge --- packages/framework/use-slot/src/useSlot.test.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/framework/use-slot/src/useSlot.test.tsx b/packages/framework/use-slot/src/useSlot.test.tsx index dd01ea32786..8555e7b062f 100644 --- a/packages/framework/use-slot/src/useSlot.test.tsx +++ b/packages/framework/use-slot/src/useSlot.test.tsx @@ -32,8 +32,11 @@ const PluggableText = phasedComponent((props: PluggableTextProps) => { PluggableText.displayName = 'PluggableText'; const useStyledStagedText = (props: PluggableTextProps, baseStyle: TextProps['style'], inner?: React.FunctionComponent) => { + // extract style from props + const { style, ...rest } = props; + // create merged props to pass in to the inner slot - const mergedProps = { ...rest, style: mergeStyles(baseStyle, style), ...(inner && { inner }) }; + const mergedProps = { ...rest, style: mergeStyles(baseStyle, style), ...(inner && { inner }) } as PluggableTextProps; // create a slot based on the pluggable text const InnerText = useSlot(PluggableText, mergedProps); From 1deda6316b72890afb89632f370973502502e887 Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Thu, 29 Jan 2026 12:48:09 -0800 Subject: [PATCH 08/15] add prop extraction helper and remove hack from switch --- packages/components/Switch/src/Switch.tsx | 4 ++-- packages/components/Switch/src/Switch.types.ts | 7 ++++--- .../framework-base/src/component-patterns/render.types.ts | 5 +++++ packages/framework-base/src/index.ts | 1 + 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/components/Switch/src/Switch.tsx b/packages/components/Switch/src/Switch.tsx index 755cbf53b69..c71c0733d2e 100644 --- a/packages/components/Switch/src/Switch.tsx +++ b/packages/components/Switch/src/Switch.tsx @@ -42,8 +42,8 @@ export const Switch = compose({ slots: { root: Pressable, label: Text, - track: Animated.View as unknown as typeof View, // Conversion from View to Animated.View for Animated API to work - thumb: Animated.View as unknown as typeof View, // Conversion from View to Animated.View for Animated API to work + track: Animated.View, + thumb: Animated.View, toggleContainer: View, onOffTextContainer: View, onOffText: Text, diff --git a/packages/components/Switch/src/Switch.types.ts b/packages/components/Switch/src/Switch.types.ts index 5044ce20798..4f05ec0dbbd 100644 --- a/packages/components/Switch/src/Switch.types.ts +++ b/packages/components/Switch/src/Switch.types.ts @@ -1,10 +1,11 @@ import type * as React from 'react'; -import type { ViewStyle, ColorValue, PressableProps } from 'react-native'; +import type { Animated, ViewStyle, ColorValue, PressableProps } from 'react-native'; import type { IViewProps } from '@fluentui-react-native/adapters'; import type { IFocusable, InteractionEvent, PressablePropsExtended, PressableState } from '@fluentui-react-native/interactive-hooks'; import type { TextProps } from '@fluentui-react-native/text'; import type { FontTokens, IBorderTokens, IColorTokens, IShadowTokens, LayoutTokens } from '@fluentui-react-native/tokens'; +import type { PropsOf } from '@fluentui-react-native/framework-base'; export const switchName = 'Switch'; @@ -197,8 +198,8 @@ export interface SwitchInfo { export interface SwitchSlotProps { root: React.PropsWithRef; label: TextProps; - track: IViewProps; - thumb: IViewProps; + track: PropsOf; + thumb: PropsOf; toggleContainer: IViewProps; onOffTextContainer: IViewProps; onOffText: TextProps; diff --git a/packages/framework-base/src/component-patterns/render.types.ts b/packages/framework-base/src/component-patterns/render.types.ts index 914ae429321..9d1f84a9f23 100644 --- a/packages/framework-base/src/component-patterns/render.types.ts +++ b/packages/framework-base/src/component-patterns/render.types.ts @@ -13,6 +13,11 @@ export type RenderType = Parameters[0] | string; */ export type NativeReactType = RenderType; +/** + * Get the props from a react component type + */ +export type PropsOf = TComponent extends React.JSXElementConstructor ? P : never; + /** * DIRECT RENDERING * diff --git a/packages/framework-base/src/index.ts b/packages/framework-base/src/index.ts index ac66d45af60..c1c34c3b415 100644 --- a/packages/framework-base/src/index.ts +++ b/packages/framework-base/src/index.ts @@ -27,6 +27,7 @@ export type { LegacyDirectComponent, PhasedComponent, PhasedRender, + PropsOf, RenderType, RenderResult, StagedRender, From d061b13ab49af30a68831c88558b4a768e20a67b Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Thu, 29 Jan 2026 13:28:47 -0800 Subject: [PATCH 09/15] fix package linting error --- packages/experimental/Checkbox/package.json | 1 - yarn.lock | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/experimental/Checkbox/package.json b/packages/experimental/Checkbox/package.json index 40e39947c21..260dc532e1c 100644 --- a/packages/experimental/Checkbox/package.json +++ b/packages/experimental/Checkbox/package.json @@ -32,7 +32,6 @@ "update-snapshots": "fluentui-scripts jest -u" }, "dependencies": { - "@fluentui-react-native/adapters": "workspace:*", "@fluentui-react-native/checkbox": "workspace:*", "@fluentui-react-native/framework": "workspace:*" }, diff --git a/yarn.lock b/yarn.lock index 6b7367488c2..3c9443a04f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4043,7 +4043,6 @@ __metadata: resolution: "@fluentui-react-native/experimental-checkbox@workspace:packages/experimental/Checkbox" dependencies: "@babel/core": "catalog:" - "@fluentui-react-native/adapters": "workspace:*" "@fluentui-react-native/babel-config": "workspace:*" "@fluentui-react-native/checkbox": "workspace:*" "@fluentui-react-native/eslint-config-rules": "workspace:*" From 6da801f360fa544b96fdc49637e9ec16f84a5003 Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Thu, 29 Jan 2026 15:11:24 -0800 Subject: [PATCH 10/15] fix useSlot implementation --- packages/framework/use-slot/src/useSlot.ts | 60 ++++++++++++++-------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/packages/framework/use-slot/src/useSlot.ts b/packages/framework/use-slot/src/useSlot.ts index c3c7e566abf..6052882938e 100644 --- a/packages/framework/use-slot/src/useSlot.ts +++ b/packages/framework/use-slot/src/useSlot.ts @@ -5,38 +5,58 @@ import type { PropsFilter, FunctionComponent } from '@fluentui-react-native/fram export type ComponentType = React.ComponentType; +type SlotData = { + innerComponent: React.ComponentType; + propsToMerge?: TProps; +}; + /** * useSlot hook function, allows authoring against pluggable slots as well as allowing components to be called as functions rather than * via createElement if they support it. * * @param component - any kind of component that can be rendered as part of the tree - * @param initialProps - props, particularly the portion that includes styles, that should be passed to the component. These will be merged with what are specified in the JSX tree + * @param hookProps - props, particularly the portion that includes styles, that should be passed to the component. These will be merged with what are specified in the JSX tree * @param filter - optional filter that will prune the props before forwarding to the component * @returns */ export function useSlot( component: React.ComponentType, - initialProps: Partial, + hookProps?: Partial, filter?: PropsFilter, ): FunctionComponent { - // filter the initial props if a filter is specified - const filteredProps = filterProps(initialProps, filter); - - // build the secondary processing function and the result holder, done via useMemo so the function identity stays the same. Rebuilding the closure every time would invalidate render - return React.useMemo>(() => { - // extract the phased component function if that pattern is being used, will be undefined if it is a standard component - const phasedRender = getPhasedRender(component as React.ComponentType); + // create this once for this hook instance to hold slot data between phases + const slotData = React.useMemo(() => { + return {} as SlotData; + }, []); - // do the first phase render with the initial props if we are using the staged pattern. This is typically getting - // styles and tokens in place a single time for the component. - const finalRender = phasedRender ? phasedRender(initialProps as TProps) : component; + // see if this component is a phased render component + const phasedRender = getPhasedRender(component); + if (phasedRender) { + // if it is, run the first phase now with the hook props + slotData.innerComponent = phasedRender(hookProps as TProps); + slotData.propsToMerge = undefined; + } else { + // otherwise pass the hook props directly to the component + slotData.innerComponent = component; + slotData.propsToMerge = hookProps as TProps; + } - // now return a direct component function that can be used in JSX/TSX, this pattern is safe since we won't be using - // hooks in this closure - return directComponent((innerProps: TProps) => { - const finalInner = filterProps(innerProps, filter); - const finalProps = phasedRender ? finalInner : mergeProps(filteredProps, finalInner); - return renderForJsxRuntime(finalRender as React.ComponentType, finalProps); - }); - }, [component, filter]); + // build the secondary processing function and the result holder, done via useMemo so the function identity stays the same. Rebuilding the closure every time would invalidate render + return React.useMemo>( + () => + directComponent((innerProps: TProps) => { + const { propsToMerge, innerComponent } = slotData; + if (propsToMerge) { + // merge in props from phase one if they haven't been captured in the phased render + innerProps = mergeProps(propsToMerge, innerProps); + } + if (filter) { + // filter the final props if a filter is specified + innerProps = filterProps(innerProps, filter); + } + // now render the component with the final props + return renderForJsxRuntime(innerComponent, innerProps); + }), + [component, filter, slotData], + ); } From 3974ddcf9a6b7752043782ca203a2bc43de48577 Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Mon, 2 Feb 2026 10:06:53 -0800 Subject: [PATCH 11/15] fix overflow rendering --- .../components/Icon/src/FontIcon/FontIcon.tsx | 6 +++--- packages/components/Icon/src/SvgIcon/SvgIcon.tsx | 6 +++--- packages/components/Icon/src/legacy/Icon.tsx | 6 +++--- packages/components/Menu/src/Menu/Menu.tsx | 6 +++--- .../Menu/src/MenuCallout/MenuCallout.android.tsx | 6 +++--- .../Menu/src/MenuCallout/MenuCallout.tsx | 6 +++--- .../Notification/src/Notification.helper.tsx | 6 +++--- .../TabListAnimatedIndicator.tsx | 6 +++--- .../TabListAnimatedIndicator.win32.tsx | 6 +++--- .../Overflow/src/Overflow/Overflow.tsx | 6 +++--- .../Overflow/src/OverflowItem/OverflowItem.tsx | 6 +++--- .../__snapshots__/Overflow.test.tsx.snap | 15 ++++++--------- packages/experimental/Shadow/src/Shadow.tsx | 6 +++--- packages/experimental/Tooltip/src/Tooltip.tsx | 6 +++--- .../src/component-patterns/phasedComponent.ts | 9 ++------- .../src/component-patterns/render.ts | 5 ++--- packages/framework/use-slot/src/useSlot.test.tsx | 10 +++++----- 17 files changed, 54 insertions(+), 63 deletions(-) diff --git a/packages/components/Icon/src/FontIcon/FontIcon.tsx b/packages/components/Icon/src/FontIcon/FontIcon.tsx index 4e0a84023b6..23e6d1b7687 100644 --- a/packages/components/Icon/src/FontIcon/FontIcon.tsx +++ b/packages/components/Icon/src/FontIcon/FontIcon.tsx @@ -1,6 +1,6 @@ import { Text } from 'react-native'; -import { mergeProps, phasedComponent } from '@fluentui-react-native/framework-base'; +import { mergeProps, directComponent, phasedComponent } from '@fluentui-react-native/framework-base'; import type { FontIconProps } from './FontIcon.types'; import { fontIconName } from './FontIcon.types'; @@ -8,13 +8,13 @@ import { useFontIcon } from './useFontIcon'; export const FontIcon = phasedComponent((props: FontIconProps) => { const fontIconProps = useFontIcon(props); - return (final: FontIconProps) => { + return directComponent((final: FontIconProps) => { const newProps = mergeProps(fontIconProps, final); const { codepoint, ...rest } = newProps; const char = String.fromCharCode(codepoint); return {char}; - }; + }); }); FontIcon.displayName = fontIconName; diff --git a/packages/components/Icon/src/SvgIcon/SvgIcon.tsx b/packages/components/Icon/src/SvgIcon/SvgIcon.tsx index 10085c57433..603a77b7a2d 100644 --- a/packages/components/Icon/src/SvgIcon/SvgIcon.tsx +++ b/packages/components/Icon/src/SvgIcon/SvgIcon.tsx @@ -1,6 +1,6 @@ import { Platform, View } from 'react-native'; -import { mergeProps, phasedComponent } from '@fluentui-react-native/framework-base'; +import { mergeProps, phasedComponent, directComponent } from '@fluentui-react-native/framework-base'; import { SvgUri } from 'react-native-svg'; import type { SvgIconProps } from './SvgIcon.types'; @@ -9,7 +9,7 @@ import { useSvgIcon } from './useSvgIcon'; export const SvgIcon = phasedComponent((props: SvgIconProps) => { const svgProps = useSvgIcon(props); - return (final: SvgIconProps) => { + return directComponent((final: SvgIconProps) => { const { style, height, width, src, uri, viewBox, color, ...rest } = mergeProps(svgProps, final); const svgIconsSupported = Platform.OS !== 'windows'; @@ -22,7 +22,7 @@ export const SvgIcon = phasedComponent((props: SvgIconProps) => { )} ) : null; - }; + }); }); SvgIcon.displayName = svgIconName; diff --git a/packages/components/Icon/src/legacy/Icon.tsx b/packages/components/Icon/src/legacy/Icon.tsx index 2979d53be61..a6e94dbe60b 100644 --- a/packages/components/Icon/src/legacy/Icon.tsx +++ b/packages/components/Icon/src/legacy/Icon.tsx @@ -2,7 +2,7 @@ import { Image, Platform, View } from 'react-native'; import type { ImageStyle, TextStyle, ViewStyle } from 'react-native'; import { mergeStyles, useFluentTheme } from '@fluentui-react-native/framework'; -import { phasedComponent, mergeProps, getMemoCache, getTypedMemoCache } from '@fluentui-react-native/framework-base'; +import { phasedComponent, directComponent, mergeProps, getMemoCache, getTypedMemoCache } from '@fluentui-react-native/framework-base'; import { Text } from '@fluentui-react-native/text'; import type { SvgProps } from 'react-native-svg'; import { SvgUri } from 'react-native-svg'; @@ -95,7 +95,7 @@ function renderSvg(iconProps: IconProps) { export const Icon = phasedComponent((props: IconProps) => { const theme = useFluentTheme(); - return (rest: IconProps) => { + return directComponent((rest: IconProps) => { const color = props.color || theme.colors.buttonText; const accessible = props.accessible ?? true; @@ -115,7 +115,7 @@ export const Icon = phasedComponent((props: IconProps) => { } else { return null; } - }; + }); }); export default Icon; diff --git a/packages/components/Menu/src/Menu/Menu.tsx b/packages/components/Menu/src/Menu/Menu.tsx index 5b18424acff..26d5dac70ea 100644 --- a/packages/components/Menu/src/Menu/Menu.tsx +++ b/packages/components/Menu/src/Menu/Menu.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { phasedComponent } from '@fluentui-react-native/framework-base'; +import { phasedComponent, directComponent } from '@fluentui-react-native/framework-base'; import type { MenuProps } from './Menu.types'; import { menuName } from './Menu.types'; @@ -12,7 +12,7 @@ export const Menu = phasedComponent((props: MenuProps) => { const state = useMenu(props); const contextValue = useMenuContextValue(state); - return (rest: MenuProps) => { + return directComponent((rest: MenuProps) => { const childrenArray = React.Children.toArray(rest.children) as React.ReactElement[]; if (__DEV__) { @@ -21,7 +21,7 @@ export const Menu = phasedComponent((props: MenuProps) => { } } return renderFinalMenu(childrenArray, contextValue, state); - }; + }); }); Menu.displayName = menuName; diff --git a/packages/components/Menu/src/MenuCallout/MenuCallout.android.tsx b/packages/components/Menu/src/MenuCallout/MenuCallout.android.tsx index 097b99b3567..c2305a0673d 100644 --- a/packages/components/Menu/src/MenuCallout/MenuCallout.android.tsx +++ b/packages/components/Menu/src/MenuCallout/MenuCallout.android.tsx @@ -1,6 +1,6 @@ import { Animated, Modal, TouchableWithoutFeedback, View, StyleSheet, ScrollView } from 'react-native'; -import { mergeProps, phasedComponent } from '@fluentui-react-native/framework-base'; +import { mergeProps, phasedComponent, directComponent } from '@fluentui-react-native/framework-base'; import type { MenuCalloutProps } from './MenuCallout.types'; import { menuCalloutName } from './MenuCallout.types'; @@ -11,7 +11,7 @@ const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView); export const MenuCallout = phasedComponent((props: MenuCalloutProps) => { const context = useMenuContext(); - return (innerProps: MenuCalloutProps) => { + return directComponent((innerProps: MenuCalloutProps) => { const { children, ...rest } = mergeProps(props, innerProps); const mergedProps = mergeProps(props, rest); const tokens = props.tokens; @@ -51,7 +51,7 @@ export const MenuCallout = phasedComponent((props: MenuCalloutProps) => { ); - }; + }); }); MenuCallout.displayName = menuCalloutName; diff --git a/packages/components/Menu/src/MenuCallout/MenuCallout.tsx b/packages/components/Menu/src/MenuCallout/MenuCallout.tsx index 48774761396..08ef20a78ad 100644 --- a/packages/components/Menu/src/MenuCallout/MenuCallout.tsx +++ b/packages/components/Menu/src/MenuCallout/MenuCallout.tsx @@ -1,16 +1,16 @@ import { Callout } from '@fluentui-react-native/callout'; -import { mergeProps, phasedComponent } from '@fluentui-react-native/framework-base'; +import { mergeProps, phasedComponent, directComponent } from '@fluentui-react-native/framework-base'; import type { MenuCalloutProps } from './MenuCallout.types'; import { menuCalloutName } from './MenuCallout.types'; export const MenuCallout = phasedComponent((props: MenuCalloutProps) => { - return (innerProps: MenuCalloutProps) => { + return directComponent((innerProps: MenuCalloutProps) => { const { children, ...rest } = innerProps; const mergedProps = mergeProps(props, rest); return {children}; - }; + }); }); MenuCallout.displayName = menuCalloutName; diff --git a/packages/components/Notification/src/Notification.helper.tsx b/packages/components/Notification/src/Notification.helper.tsx index 53470446b2a..d7f0630bb03 100644 --- a/packages/components/Notification/src/Notification.helper.tsx +++ b/packages/components/Notification/src/Notification.helper.tsx @@ -2,7 +2,7 @@ import React from 'react'; import type { ButtonProps, ButtonTokens } from '@fluentui-react-native/button'; import { ButtonV1 as Button } from '@fluentui-react-native/button'; -import { mergeProps, phasedComponent } from '@fluentui-react-native/framework-base'; +import { mergeProps, phasedComponent, directComponent } from '@fluentui-react-native/framework-base'; import type { SvgIconProps } from '@fluentui-react-native/icon'; import { createIconProps } from '@fluentui-react-native/icon'; import { globalTokens } from '@fluentui-react-native/theme-tokens'; @@ -86,9 +86,9 @@ export const NotificationButton = phasedComponent((props: NotificationButtonProp [props.color, props.disabledColor, props.pressedColor], ); - return (final: NotificationButtonProps) => { + return directComponent((final: NotificationButtonProps) => { const { children, ...rest } = final; const mergedProps = mergeProps(props, rest); return {children}; - }; + }); }); diff --git a/packages/components/TabList/src/TabListAnimatedIndicator/TabListAnimatedIndicator.tsx b/packages/components/TabList/src/TabListAnimatedIndicator/TabListAnimatedIndicator.tsx index 69f1bdc461c..4d2a7fb5ad4 100644 --- a/packages/components/TabList/src/TabListAnimatedIndicator/TabListAnimatedIndicator.tsx +++ b/packages/components/TabList/src/TabListAnimatedIndicator/TabListAnimatedIndicator.tsx @@ -1,6 +1,6 @@ import { Animated } from 'react-native'; -import { phasedComponent } from '@fluentui-react-native/framework-base'; +import { phasedComponent, directComponent } from '@fluentui-react-native/framework-base'; import type { AnimatedIndicatorProps } from './TabListAnimatedIndicator.types'; import { tablistAnimatedIndicatorName } from './TabListAnimatedIndicator.types'; @@ -8,9 +8,9 @@ import { useAnimatedIndicatorStyles } from './useAnimatedIndicatorStyles'; export const TabListAnimatedIndicator = phasedComponent((props) => { const styles = useAnimatedIndicatorStyles(props); - return () => { + return directComponent(() => { return ; - }; + }); }); TabListAnimatedIndicator.displayName = tablistAnimatedIndicatorName; diff --git a/packages/components/TabList/src/TabListAnimatedIndicator/TabListAnimatedIndicator.win32.tsx b/packages/components/TabList/src/TabListAnimatedIndicator/TabListAnimatedIndicator.win32.tsx index 7826f7a6bb4..9a81eb3cd67 100644 --- a/packages/components/TabList/src/TabListAnimatedIndicator/TabListAnimatedIndicator.win32.tsx +++ b/packages/components/TabList/src/TabListAnimatedIndicator/TabListAnimatedIndicator.win32.tsx @@ -1,7 +1,7 @@ import { View } from 'react-native'; import type { Animated, ViewProps, ViewStyle } from 'react-native'; -import { phasedComponent, memoize } from '@fluentui-react-native/framework-base'; +import { phasedComponent, memoize, directComponent } from '@fluentui-react-native/framework-base'; import type { AnimatedIndicatorProps } from './TabListAnimatedIndicator.types'; import { tablistAnimatedIndicatorName } from './TabListAnimatedIndicator.types'; @@ -18,10 +18,10 @@ function indicatorPropsWorker(animationClass: string, style: Animated.AnimatedPr */ export const TabListAnimatedIndicator = phasedComponent((props) => { const styles = useAnimatedIndicatorStyles(props); - return () => { + return directComponent(() => { const indicatorProps = getIndicatorProps('Ribbon_TabUnderline', styles); return ; - }; + }); }); TabListAnimatedIndicator.displayName = tablistAnimatedIndicatorName; diff --git a/packages/experimental/Overflow/src/Overflow/Overflow.tsx b/packages/experimental/Overflow/src/Overflow/Overflow.tsx index b1fa373dd66..aabd8231c47 100644 --- a/packages/experimental/Overflow/src/Overflow/Overflow.tsx +++ b/packages/experimental/Overflow/src/Overflow/Overflow.tsx @@ -1,6 +1,6 @@ import { View } from 'react-native'; -import { mergeProps, phasedComponent } from '@fluentui-react-native/framework-base'; +import { mergeProps, phasedComponent, directComponent } from '@fluentui-react-native/framework-base'; import type { OverflowProps } from './Overflow.types'; import { overflowName } from './Overflow.types'; @@ -9,7 +9,7 @@ import { OverflowContext } from '../OverflowContext'; export const Overflow = phasedComponent((initial: OverflowProps) => { const { props, state } = useOverflow(initial); - return (final: OverflowProps) => { + return directComponent((final: OverflowProps) => { const { children, ...rest } = final; const mergedProps = mergeProps(props, rest); return ( @@ -17,7 +17,7 @@ export const Overflow = phasedComponent((initial: OverflowProps) {children}
); - }; + }); }); Overflow.displayName = overflowName; diff --git a/packages/experimental/Overflow/src/OverflowItem/OverflowItem.tsx b/packages/experimental/Overflow/src/OverflowItem/OverflowItem.tsx index 9231a810e3f..e0fb41c2ee9 100644 --- a/packages/experimental/Overflow/src/OverflowItem/OverflowItem.tsx +++ b/packages/experimental/Overflow/src/OverflowItem/OverflowItem.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import type { StyleProp, ViewProps, ViewStyle } from 'react-native'; -import { mergeProps, phasedComponent, memoize, mergeStyles } from '@fluentui-react-native/framework-base'; +import { mergeProps, directComponent, phasedComponent, memoize, mergeStyles } from '@fluentui-react-native/framework-base'; import type { OverflowItemProps } from './OverflowItem.types'; import { overflowItemName } from './OverflowItem.types'; @@ -14,7 +14,7 @@ function overflowItemPropWorker(props: ViewProps, style: StyleProp): export const OverflowItem = phasedComponent((userProps: OverflowItemProps) => { const { props, state } = useOverflowItem(userProps); - return (finalProps: OverflowItemProps) => { + return directComponent((finalProps: OverflowItemProps) => { const { children, ...rest } = finalProps; if (state.layoutDone && !state.visible) { return null; @@ -41,7 +41,7 @@ export const OverflowItem = phasedComponent((userProps: Overf const clone = React.cloneElement(child, viewProps); return clone; - }; + }); }); OverflowItem.displayName = overflowItemName; diff --git a/packages/experimental/Overflow/src/__tests__/__snapshots__/Overflow.test.tsx.snap b/packages/experimental/Overflow/src/__tests__/__snapshots__/Overflow.test.tsx.snap index 90caaf8dc98..49c8f38ee50 100644 --- a/packages/experimental/Overflow/src/__tests__/__snapshots__/Overflow.test.tsx.snap +++ b/packages/experimental/Overflow/src/__tests__/__snapshots__/Overflow.test.tsx.snap @@ -11,15 +11,12 @@ exports[`Overflow component tests Overflow default 1`] = ` } onLayout={[Function]} style={ - [ - undefined, - { - "display": "flex", - "flexDirection": "row", - "opacity": 0, - "padding": undefined, - }, - ] + { + "display": "flex", + "flexDirection": "row", + "opacity": 0, + "padding": undefined, + } } > { - return (final: ShadowProps) => { + return directComponent((final: ShadowProps) => { const { children, ...rest } = final; if (!props.shadowToken) { return <>{children}; @@ -25,7 +25,7 @@ export const Shadow = phasedComponent((props: ShadowProps) => { const childWithInnerShadow = React.cloneElement(children, innerShadowViewProps); return {childWithInnerShadow}; - }; + }); }); const getStylePropsForShadowViews = memoize(getStylePropsForShadowViewsWorker); diff --git a/packages/experimental/Tooltip/src/Tooltip.tsx b/packages/experimental/Tooltip/src/Tooltip.tsx index d0355972a25..08b8180ed3a 100644 --- a/packages/experimental/Tooltip/src/Tooltip.tsx +++ b/packages/experimental/Tooltip/src/Tooltip.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import { findNodeHandle } from 'react-native'; -import { mergeProps, phasedComponent } from '@fluentui-react-native/framework-base'; +import { mergeProps, phasedComponent, directComponent } from '@fluentui-react-native/framework-base'; import type { TooltipProps } from './Tooltip.types'; import { tooltipName } from './Tooltip.types'; @@ -31,14 +31,14 @@ export const Tooltip = phasedComponent((props: TooltipProps) => { } }, [target]); - const TooltipComponent = (innerProps: TooltipProps) => { + const TooltipComponent = directComponent((innerProps: TooltipProps) => { const { children, ...rest } = innerProps; return ( {children} ); - }; + }); return TooltipComponent; }); diff --git a/packages/framework-base/src/component-patterns/phasedComponent.ts b/packages/framework-base/src/component-patterns/phasedComponent.ts index b07f97e1ab3..1fcb817ad15 100644 --- a/packages/framework-base/src/component-patterns/phasedComponent.ts +++ b/packages/framework-base/src/component-patterns/phasedComponent.ts @@ -1,6 +1,6 @@ import React from 'react'; -import { jsx, jsxs } from '../jsx-runtime'; import type { ComposableFunction, PhasedComponent, PhasedRender, FunctionComponent } from './render.types'; +import { renderForJsxRuntime } from './render'; export function getPhasedRender(component: React.ComponentType): PhasedRender | undefined { // only a function component can have a phased render @@ -31,12 +31,7 @@ export function phasedComponent(getInnerPhase: PhasedRender): Fu // pull out children from props const { children, ...outerProps } = props; const Inner = getInnerPhase(outerProps as TProps); - - if (Array.isArray(children) && children.length > 1) { - return jsxs(Inner, { children }); - } else { - return jsx(Inner, { children }); - } + return renderForJsxRuntime(Inner, { children }); }, { _phasedRender: getInnerPhase }, ); diff --git a/packages/framework-base/src/component-patterns/render.ts b/packages/framework-base/src/component-patterns/render.ts index e4a50123976..a5133d779e2 100644 --- a/packages/framework-base/src/component-patterns/render.ts +++ b/packages/framework-base/src/component-patterns/render.ts @@ -38,14 +38,13 @@ export function renderForJsxRuntime( // auto-detect whether to use jsx or jsxs based on number of children, 0 or 1 = jsx, more than 1 = jsxs if (!jsxFn) { - const children = props.children; - if (Array.isArray(children) && children.length > 1) { + if (React.Children.count(props.children) > 1) { jsxFn = ReactJSX.jsxs; } else { jsxFn = ReactJSX.jsx; } } - // now call the appropriate jsx function to + // now call the appropriate jsx function to render the component return jsxFn(type, props, key); } diff --git a/packages/framework/use-slot/src/useSlot.test.tsx b/packages/framework/use-slot/src/useSlot.test.tsx index 8555e7b062f..2a76b5c91b6 100644 --- a/packages/framework/use-slot/src/useSlot.test.tsx +++ b/packages/framework/use-slot/src/useSlot.test.tsx @@ -6,7 +6,7 @@ import { Text, View } from 'react-native'; import { type FunctionComponent, mergeStyles } from '@fluentui-react-native/framework-base'; import * as renderer from 'react-test-renderer'; -import { phasedComponent } from '@fluentui-react-native/framework-base'; +import { phasedComponent, directComponent } from '@fluentui-react-native/framework-base'; import { useSlot } from './useSlot'; type PluggableTextProps = TextProps & { inner?: FunctionComponent }; @@ -23,11 +23,11 @@ const PluggableText = phasedComponent((props: PluggableTextProps) => { const Inner = useSlot(inner || Text, rest); // return a closure for finishing off render - return (extra: TextProps) => { + return directComponent((extra: TextProps) => { // split children from extra props const { children, ...rest } = extra; return {children}; - }; + }); }); PluggableText.displayName = 'PluggableText'; @@ -42,10 +42,10 @@ const useStyledStagedText = (props: PluggableTextProps, baseStyle: TextProps['st const InnerText = useSlot(PluggableText, mergedProps); // return a closure to complete the staged pattern - return (extra: PluggableTextProps) => { + return directComponent((extra: PluggableTextProps) => { const { children, ...rest } = extra; return {children}; - }; + }); }; const HeaderText = phasedComponent((props: PluggableTextProps) => { From 59f9761b2aab4e89bc1eb6f17600490bf3aad91a Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Mon, 2 Feb 2026 10:25:43 -0800 Subject: [PATCH 12/15] ensure old codepaths are consistent in behavior --- .../src/component-patterns/phasedComponent.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/framework-base/src/component-patterns/phasedComponent.ts b/packages/framework-base/src/component-patterns/phasedComponent.ts index 1fcb817ad15..8d285a187dc 100644 --- a/packages/framework-base/src/component-patterns/phasedComponent.ts +++ b/packages/framework-base/src/component-patterns/phasedComponent.ts @@ -1,6 +1,7 @@ import React from 'react'; import type { ComposableFunction, PhasedComponent, PhasedRender, FunctionComponent } from './render.types'; import { renderForJsxRuntime } from './render'; +import type { LegacyDirectComponent } from './render.types'; export function getPhasedRender(component: React.ComponentType): PhasedRender | undefined { // only a function component can have a phased render @@ -13,7 +14,14 @@ export function getPhasedRender(component: React.ComponentType): const staged = (component as ComposableFunction)._staged; return (props: TProps) => { const { children, ...rest } = props as React.PropsWithChildren; - return staged(rest as TProps, ...React.Children.toArray(children)); + const inner = staged(rest as TProps, ...React.Children.toArray(children)); + // staged render functions were not consistently marking contents as composable, though they were treated + // as such in useHook. To maintain compatibility we mark the returned function as composable here. This was + // dangerous, but this shim is necessary for backward compatibility. The newer pattern is explicit about this. + if (typeof inner === 'function' && !(inner as LegacyDirectComponent)._canCompose) { + return Object.assign(inner, { _canCompose: true }); + } + return inner; }; } } From 9ae053ec3bc0088902a13de9e0148c875623512f Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Mon, 2 Feb 2026 11:24:50 -0800 Subject: [PATCH 13/15] update framework-base documentation and fix logic bug --- packages/framework-base/README.md | 19 ++++- .../src/component-patterns/README.md | 69 ++++++++++++++----- .../src/component-patterns/phasedComponent.ts | 10 ++- .../src/component-patterns/render.types.ts | 34 +++++---- packages/framework-base/src/index.ts | 13 +++- 5 files changed, 110 insertions(+), 35 deletions(-) diff --git a/packages/framework-base/README.md b/packages/framework-base/README.md index 6e3d33ed9e0..38de7b66b72 100644 --- a/packages/framework-base/README.md +++ b/packages/framework-base/README.md @@ -16,4 +16,21 @@ The shared patterns for rendering components, as well as the JSX handlers have b ## Type Helpers -- TODO: There are a number of issues with the way types are handled in the larger fluentui-react-native project, helpers and core types will be added here to help solve inference issues, avoid hard typecasts, and help the project eventually move to typescript 5.x. +This package provides several TypeScript utility types: + +- `PropsOf` - Extract props from a React component type +- `FunctionComponent` - A function component type without the children handling complications of React.FC +- `DirectComponent` - A function component marked for direct rendering +- `PhasedComponent` - A component with two-phase rendering support +- `SlotFn` - Slot function type used in the composition framework +- `FinalRender` - The final rendering signature for phased components + +## JSX Runtime + +This package exports a custom JSX runtime at `@fluentui-react-native/framework-base/jsx-runtime`. Use it in your component files with: + +```tsx +/** @jsxImportSource @fluentui-react-native/framework-base */ +``` + +The custom runtime enables automatic element flattening for direct and phased components. diff --git a/packages/framework-base/src/component-patterns/README.md b/packages/framework-base/src/component-patterns/README.md index 18517b4ce8c..426cb32dcee 100644 --- a/packages/framework-base/src/component-patterns/README.md +++ b/packages/framework-base/src/component-patterns/README.md @@ -2,38 +2,75 @@ These are the base component patterns shared across the deprecated or v0 framework (found under packages/deprecated), and the newer framework (found under packages/framework). This also includes the custom JSX handlers required to render them properly. -There are two main patterns exposed here: direct rendering and staged rendering. +There are two main patterns exposed here: direct rendering and phased rendering. ## Direct Rendering -The direct rendering pattern allows a component to be called directly, rather than creating a new entry in the DOM. +The direct rendering pattern allows a component to be called directly, rather than creating a new entry in the render tree. -As an example, if you want to create a wrapper around a component called `MyText` that has `italicize` as one of its props, that always wants to set that value to true. You could define: +As an example, if you want to create a wrapper around a component called `MyText` that has `italicize` as one of its props, that always wants to set that value to true, you could define: ```ts const MyNewText: React.FunctionComponent = (props) => { - return ; -} + return ; +}; ``` -When this is rendered, there is an entry for `MyNewText` which contains a `MyText` (another entry), which might contains `Text` (for react-native usage). The direct rendering pattern is one where a component can denote that it is safe to be called directly as a function, instead operating as a prop transform that gets applied to the underlying component. +When this is rendered, there is an entry for `MyNewText` which contains a `MyText` (another entry), which might contain `Text` (for react-native usage). The direct rendering pattern is one where a component can denote that it is safe to be called directly as a function, instead operating as a prop transform that gets applied to the underlying component. -- For the above to be safe, `MyNewText` should NOT use hooks. In the case of any conditional rendering logic this will break the rule of hooks. +- For the above to be safe, `MyNewText` should NOT use hooks. In the case of any conditional rendering logic this will break the rules of hooks. There are two types of implementations in this folder: -- `DirectComponent` - a functional component that marks itself as direct with a `_callDirect: true` attached property. This will then be called as a normal function component, with children included as part of props. -- `LegacyDirectComponent` - the pattern currently used in this library that should be moved away from. In this case `_canCompose: true` is set as an attached property, and the function component will be called with children split from props. +- `DirectComponent` - a functional component that marks itself as direct with a `_callDirect: true` attached property. This will then be called as a normal function component, with children included as part of props. Use the `directComponent()` helper to create these. +- `LegacyDirectComponent` - the pattern currently used in legacy code that should be moved away from. In this case `_canCompose: true` is set as an attached property, and the function component will be called with children split from props. -The internal logic of the JSX rendering helpers will handle both patterns. In the case of the newer `DirectComponent` pattern, the component will still work, even without any jsx hooks, whereas the `LegacyDirectComponent` pattern will have a somewhat undefined behavior with regards to children. +The internal logic of the JSX rendering helpers (`renderForJsxRuntime` and `renderForClassicRuntime`) will handle both patterns. In the case of the newer `DirectComponent` pattern, the component will still work, even without any jsx hooks, whereas the `LegacyDirectComponent` pattern will have somewhat undefined behavior with regards to children. -## Staged Rendering +### Example: Using directComponent -The issue with the direct component pattern above, is that hooks are integral to writing functional components. The staged rendering pattern is designed to help with this. In this case a component is implemented in two stages, the prep stage where hooks are called, and the rendering stage where the tree is emitted. +```ts +import { directComponent } from '@fluentui-react-native/framework-base'; + +const MyNewText = directComponent((props) => { + return ; +}); +``` + +## Phased Rendering + +The issue with the direct component pattern above is that hooks are integral to writing functional components. The phased rendering pattern is designed to help with this. In this case a component is implemented in two phases: the prep phase where hooks are called, and the rendering phase where the tree is emitted. + +As above there is a newer and older version of the pattern: + +- `PhasedComponent` - the newer version of the pattern, where the returned component function expects children as part of props. Create these using `phasedComponent()`. The attached property is `_phasedRender`. +- `ComposableFunction` (deprecated) - the older "staged" version, where children are split out and JSX hooks are required to render correctly. Create these using the deprecated `stagedComponent()`. The attached property is `_staged`. + +Note that while the newer patterns work without any JSX hooks, the hooks will enable element flattening. + +### Example: Using phasedComponent + +```ts +import { phasedComponent } from '@fluentui-react-native/framework-base'; + +const MyComponent = phasedComponent((props) => { + // Phase 1: Hooks and logic + const theme = useTheme(); + const styles = useStyles(theme, props); + + // Phase 2: Return a component that renders + return (innerProps) => { + return {innerProps.children}; + }; +}); +``` + +## JSX Runtime -As above there is a newer and older version of the pattern. +This package provides a custom JSX runtime (`@fluentui-react-native/framework-base/jsx-runtime`) that automatically handles both direct and phased rendering patterns. When you use the `@jsxImportSource @fluentui-react-native/framework-base` pragma, the custom runtime will: -- `StagedComponent` - the newer version of the pattern, where the returned component function expects children as part of props. -- `StagedRender` - the older version, where children are split out and JSX hooks are required to render correctly. +1. Detect components marked with `_callDirect` or `_canCompose` and call them directly +2. Handle the different children patterns (props vs. rest args) +3. Fall back to standard React rendering for normal components -Note that while the newer patterns work without any JSX hooks, the hooks will enable the element flattening. +This enables element flattening without requiring explicit calls to helper functions. diff --git a/packages/framework-base/src/component-patterns/phasedComponent.ts b/packages/framework-base/src/component-patterns/phasedComponent.ts index 8d285a187dc..6e4052a3181 100644 --- a/packages/framework-base/src/component-patterns/phasedComponent.ts +++ b/packages/framework-base/src/component-patterns/phasedComponent.ts @@ -3,9 +3,17 @@ import type { ComposableFunction, PhasedComponent, PhasedRender, FunctionCompone import { renderForJsxRuntime } from './render'; import type { LegacyDirectComponent } from './render.types'; +/** + * Extract the phased render function from a component, if it has one. + * Handles both the newer PhasedComponent pattern (_phasedRender) and the legacy + * ComposableFunction pattern (_staged) for backward compatibility. + * + * @param component - The component to extract the phased render from + * @returns The phased render function if present, undefined otherwise + */ export function getPhasedRender(component: React.ComponentType): PhasedRender | undefined { // only a function component can have a phased render - if (typeof component !== 'function') { + if (typeof component === 'function') { // if this has a phased render function, return it if ((component as PhasedComponent)._phasedRender) { return (component as PhasedComponent)._phasedRender; diff --git a/packages/framework-base/src/component-patterns/render.types.ts b/packages/framework-base/src/component-patterns/render.types.ts index 9d1f84a9f23..33c4b830054 100644 --- a/packages/framework-base/src/component-patterns/render.types.ts +++ b/packages/framework-base/src/component-patterns/render.types.ts @@ -72,49 +72,53 @@ export type SlotFn = { }; /** - * MULTI-STAGE RENDERING + * PHASED RENDERING (formerly called "staged" or "two-stage" rendering) * - * The above direct rendering pattern is useful for simple components, but it does not allow for hooks or complex logic. The staged render pattern allows - * for a component to be rendered in two stages, allowing for hooks to be used in the first stage and then the second stage to be a simple render function that can + * The above direct rendering pattern is useful for simple components, but it does not allow for hooks or complex logic. The phased render pattern allows + * for a component to be rendered in two phases, allowing for hooks to be used in the first phase and then the second phase to be a simple render function that can * be called directly. * - * In code that respects the pattern the first stage will be called with props (though children will not be present) and will return a function that will be called - * with additional props, this time with children present. This allows for the first stage to handle all the logic and hooks, while the second stage can be a simple render function + * In code that respects the pattern, the first phase will be called with props (though children will not be present) and will return a function that will be called + * with additional props, this time with children present. This allows for the first phase to handle all the logic and hooks, while the second phase can be a simple render function * that can leverage direct rendering if supported. * - * The component itself will be a FunctionComponent, but it will have an attached property that is the staged render function. This allows the component to be used in two + * The component itself will be a FunctionComponent, but it will have an attached property that is the phased render function. This allows the component to be used in two * parts via the useSlot hook, or to be used directly in JSX/TSX as a normal component. */ /** - * This is an updated version of the staged render that handles children and types more consistently. Generally children - * will be passed as part of the props for component rendering, it is inconsistent to have them as a variable argument. + * Phased render function signature. This is the recommended pattern for components that need hooks. * - * The `children` prop will be automatically inferred and typed correctly by the prop type. Hooks are still expected + * Phase 1 receives props (without children) and can use hooks to compute derived state. + * Phase 2 returns a component that will be called with props including children. + * + * Children will be passed as part of the props for component rendering. The `children` prop will be + * automatically inferred and typed correctly by the prop type. */ export type PhasedRender = (props: TProps) => React.ComponentType>; /** - * Component type for a component that can be rendered in two stages, with the attached render function. + * Component type for a component that can be rendered in two phases, with the attached phased render function. + * Use phasedComponent() to create these. */ export type PhasedComponent = FunctionComponent & { _phasedRender?: PhasedRender; }; /** - * The final rendering of the props in a staged render. This is the function component signature that matches that of + * The final rendering of the props in a phased render. This is the function component signature that matches that of * React.createElement, children (if present) will be part of the variable args at the end. */ export type FinalRender = (props: TProps, ...children: React.ReactNode[]) => React.JSX.Element | null; /** - * Signature for a staged render function. - * @deprecated Use TwoStageRender instead + * Legacy staged render function signature. + * @deprecated Use PhasedRender instead. This older pattern splits children from props which is inconsistent with React conventions. */ export type StagedRender = (props: TProps, ...args: any[]) => FinalRender; /** - * Signature for a component that uses the staged render pattern. - * @deprecated Use TwoStageRender instead + * Legacy component type that uses the staged render pattern. + * @deprecated Use PhasedComponent instead. Create with phasedComponent() rather than stagedComponent(). */ export type ComposableFunction = FunctionComponent & { _staged?: StagedRender }; diff --git a/packages/framework-base/src/index.ts b/packages/framework-base/src/index.ts index c1c34c3b415..913b298f250 100644 --- a/packages/framework-base/src/index.ts +++ b/packages/framework-base/src/index.ts @@ -19,11 +19,14 @@ export type { StyleProp } from './merge-props/mergeStyles.types'; export { mergeStyles } from './merge-props/mergeStyles'; export { mergeProps } from './merge-props/mergeProps'; -// component pattern exports +// component pattern exports - rendering utilities export { renderForJsxRuntime, renderSlot, asDirectComponent } from './component-patterns/render'; + +// component pattern exports - core types export type { DirectComponent, FunctionComponent, + FunctionComponentCore, LegacyDirectComponent, PhasedComponent, PhasedRender, @@ -36,10 +39,16 @@ export type { SlotFn, NativeReactType, } from './component-patterns/render.types'; + +// component pattern exports - component builders export { directComponent } from './component-patterns/directComponent'; export { getPhasedRender, phasedComponent } from './component-patterns/phasedComponent'; -export { withSlots } from './component-patterns/withSlots'; export { stagedComponent } from './component-patterns/stagedComponent'; + +// component pattern exports - legacy JSX handlers +export { withSlots } from './component-patterns/withSlots'; + +// jsx runtime exports export { jsx, jsxs } from './jsx-runtime'; // general utilities From e1508e2f226c31444a890e275c07dfb2a910a00d Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Mon, 2 Feb 2026 11:49:59 -0800 Subject: [PATCH 14/15] fix dead links in markdown files --- CONTRIBUTING.md | 6 +++--- packages/components/Avatar/SPEC.md | 2 +- packages/components/Badge/SPEC.md | 2 +- packages/components/Button/SPEC.md | 2 +- packages/components/Button/src/CompoundButton/SPEC.md | 2 +- packages/components/Button/src/FAB/SPEC.md | 2 +- packages/components/Button/src/ToggleButton/SPEC.md | 2 +- packages/components/Checkbox/SPEC.md | 2 +- packages/components/Chip/SPEC.md | 2 +- packages/components/Icon/SPEC.md | 2 +- packages/components/Input/SPEC.md | 2 +- packages/components/Link/SPEC.md | 2 +- packages/components/RadioGroup/SPEC.md | 2 +- packages/components/Switch/SPEC.md | 2 +- packages/components/TabList/SPEC.md | 2 +- packages/components/Text/SPEC.md | 2 +- packages/experimental/Shadow/SPEC.md | 2 +- packages/experimental/Shimmer/SPEC.md | 2 +- packages/experimental/Tooltip/SPEC.md | 2 +- 19 files changed, 21 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 47598bb0d32..1915869ff8c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ This guide assumes you: - Have read through the [React Native Docs](https://reactnative.dev/docs/getting-started). In particular: - Understand classes vs function components (we use the latter) and [hooks](https://reactjs.org/docs/hooks-intro.html). Here's a good [video](https://www.youtube.com/watch?v=dpw9EHDh2bM) that explains function components and hooks for traditional OOP developers. - - Understand [Native Modules](https://reactnative.dev/docs/0.74/native-modules-intro). + - Understand [Native Modules](https://reactnative.dev/docs/turbo-native-modules-introduction). - Have a local fork of FluentUI React Native and have run the test app. ## Understanding the Repository Structure @@ -44,7 +44,7 @@ Tokens help us achieve simpler customization for complex higher order components This section covers creating and adding a new component package to FluentUI React Native's monorepo. If you are instead working on an existing component and adding a native module, skip to the next two sections. -Most components should use the compose framework as it offers the comprehensive set of patterns like tokens and slots, but if you're creating a simple component that doesn't require those patterns, there's a lighter pattern called [stagedComponent](./packages/framework/use-slot/src/stagedComponent.ts). The stagedComponent pattern splits up the render function into two stages. Stage 1 handles building props and hook calls (best to separate the hook calls from the render tree since they rely on call order). Stage 2 returns the actual element tree, any conditional branching should happen here (Icon is a good example of using stagedCompoenent). +Most components should use the compose framework as it offers the comprehensive set of patterns like tokens and slots, but if you're creating a simple component that doesn't require those patterns, there's a lighter pattern called [stagedComponent](./packages/framework-base/src/component-patterns/stagedComponent.ts). The stagedComponent pattern splits up the render function into two stages. Stage 1 handles building props and hook calls (best to separate the hook calls from the render tree since they rely on call order). Stage 2 returns the actual element tree, any conditional branching should happen here (Icon is a good example of using stagedCompoenent). 1. Create a new directory in of these two locations, depending on your component: @@ -82,7 +82,7 @@ Reach out to Samuel Freiberg with any questions related to E2E testing. ## Adding native code to your new component -Through the power of [Native Modules](https://reactnative.dev/docs/0.74/native-modules-intro), we are able to create components that are comprised of native platform code, rather than JS. This is particularly useful if you want platform specific behavior, or if you want a component that feels much more aligned to it's specific platform. The downside is you must implement the Native module for every platform you wish to support. It's worth investigating whether you truly need a native module, or if a more cross platform JS implementation is the better approach. +Through the power of [Native Modules](https://reactnative.dev/docs/turbo-native-modules-introduction), we are able to create components that are comprised of native platform code, rather than JS. This is particularly useful if you want platform specific behavior, or if you want a component that feels much more aligned to it's specific platform. The downside is you must implement the Native module for every platform you wish to support. It's worth investigating whether you truly need a native module, or if a more cross platform JS implementation is the better approach. There are a few caveats to know of adding a native module to a FluentUI React Native component: diff --git a/packages/components/Avatar/SPEC.md b/packages/components/Avatar/SPEC.md index dbf2645ec4e..408d6771ae8 100644 --- a/packages/components/Avatar/SPEC.md +++ b/packages/components/Avatar/SPEC.md @@ -20,7 +20,7 @@ Basic examples: ``` -More examples on the [Test pages for the Avatar](../../../apps/fluent-tester/src/TestComponents/Avatar). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). +More examples on the [Test pages for the Avatar](../../../apps/tester-core/src/TestComponents/Avatar). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). ## Variants diff --git a/packages/components/Badge/SPEC.md b/packages/components/Badge/SPEC.md index 0e63e67f57f..ead4bd354cc 100644 --- a/packages/components/Badge/SPEC.md +++ b/packages/components/Badge/SPEC.md @@ -24,7 +24,7 @@ Basic examples: ``` -More examples on the [Test pages for the Badge](../../../apps/fluent-tester/src/TestComponents/Badge). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). +More examples on the [Test pages for the Badge](../../../apps/tester-core/src/TestComponents/Badge). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). ## Visual Examples diff --git a/packages/components/Button/SPEC.md b/packages/components/Button/SPEC.md index c6863970b84..e0220ddfa3c 100644 --- a/packages/components/Button/SPEC.md +++ b/packages/components/Button/SPEC.md @@ -28,7 +28,7 @@ Basic examples: ``` -More examples on the [Test pages for the Button](../../../apps/fluent-tester/src/TestComponents/Button). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). +More examples on the [Test pages for the Button](../../../apps/tester-core/src/TestComponents/Button). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). ## Visual Examples diff --git a/packages/components/Button/src/CompoundButton/SPEC.md b/packages/components/Button/src/CompoundButton/SPEC.md index f87cb6f0b8e..8bd4fbb0eec 100644 --- a/packages/components/Button/src/CompoundButton/SPEC.md +++ b/packages/components/Button/src/CompoundButton/SPEC.md @@ -22,7 +22,7 @@ Basic examples: Text ``` -More examples on the [Test pages for the Button](../../../../../apps/fluent-tester/src/TestComponents/Button). Instructions on running the tester app can be found [here](../../../../../apps/fluent-tester/README.md). +More examples on the [Test pages for the Button](../../../../../apps/tester-core/src/TestComponents/Button). Instructions on running the tester app can be found [here](../../../../../apps/fluent-tester/README.md). ## Visual Examples diff --git a/packages/components/Button/src/FAB/SPEC.md b/packages/components/Button/src/FAB/SPEC.md index 23fa6974d23..5cd769f5230 100644 --- a/packages/components/Button/src/FAB/SPEC.md +++ b/packages/components/Button/src/FAB/SPEC.md @@ -25,7 +25,7 @@ const flipFABcontent = React.useCallback(() => setShowFABText(!showFABText), [sh Text ``` -More examples on the [Test pages for the Button](../../../../../apps/fluent-tester/src/TestComponents/Button). Instructions on running the tester app can be found [here](../../../../../apps/fluent-tester/README.md). +More examples on the [Test pages for the Button](../../../../../apps/tester-core/src/TestComponents/Button). Instructions on running the tester app can be found [here](../../../../../apps/fluent-tester/README.md). ## Visual Examples diff --git a/packages/components/Button/src/ToggleButton/SPEC.md b/packages/components/Button/src/ToggleButton/SPEC.md index f35047a9b28..f0475947922 100644 --- a/packages/components/Button/src/ToggleButton/SPEC.md +++ b/packages/components/Button/src/ToggleButton/SPEC.md @@ -24,7 +24,7 @@ Basic examples: Text ``` -More examples on the [Test pages for the Button](../../../../../apps/fluent-tester/src/TestComponents/Button). Instructions on running the tester app can be found [here](../../../../../apps/fluent-tester/README.md). +More examples on the [Test pages for the Button](../../../../../apps/tester-core/src/TestComponents/Button). Instructions on running the tester app can be found [here](../../../../../apps/fluent-tester/README.md). ## Visual Examples diff --git a/packages/components/Checkbox/SPEC.md b/packages/components/Checkbox/SPEC.md index da1aff58d0b..4af8ca687eb 100644 --- a/packages/components/Checkbox/SPEC.md +++ b/packages/components/Checkbox/SPEC.md @@ -26,7 +26,7 @@ Basic examples: ``` -More examples on the [Test pages for the Checkbox](../../../apps/fluent-tester/src/TestComponents/CheckboxV1). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). +More examples on the [Test pages for the Checkbox](../../../apps/tester-core/src/TestComponents/CheckboxV1). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). ## Visual Examples diff --git a/packages/components/Chip/SPEC.md b/packages/components/Chip/SPEC.md index 34eab28b9f0..499907ca2e3 100644 --- a/packages/components/Chip/SPEC.md +++ b/packages/components/Chip/SPEC.md @@ -18,7 +18,7 @@ Basic examples: ``` -More examples on the [Test pages for the Chip](../../../apps/fluent-tester/src/TestComponents/Chip). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). +More examples on the [Test pages for the Chip](../../../apps/tester-core/src/TestComponents/Chip). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). ## Visual Examples diff --git a/packages/components/Icon/SPEC.md b/packages/components/Icon/SPEC.md index ecf9370a198..da17a293360 100644 --- a/packages/components/Icon/SPEC.md +++ b/packages/components/Icon/SPEC.md @@ -42,7 +42,7 @@ const svgSrcProps: SvgIconProps = { ``` -More examples on the [Test pages for the Icon](../../../apps/fluent-tester/src/TestComponents/Icon). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). +More examples on the [Test pages for the Icon](../../../apps/tester-core/src/TestComponents/Icon). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). ## Visual Examples diff --git a/packages/components/Input/SPEC.md b/packages/components/Input/SPEC.md index 96b5eeaf2ab..39a1ac265a9 100644 --- a/packages/components/Input/SPEC.md +++ b/packages/components/Input/SPEC.md @@ -24,7 +24,7 @@ Basic examples: /> ``` -More examples on the [Test pages for the Input](../../../apps/fluent-tester/src/TestComponents/Input). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). +More examples on the [Test pages for the Input](../../../apps/tester-core/src/TestComponents/Input). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). ## Visual Examples diff --git a/packages/components/Link/SPEC.md b/packages/components/Link/SPEC.md index 1428edea563..b1d1b0efdda 100644 --- a/packages/components/Link/SPEC.md +++ b/packages/components/Link/SPEC.md @@ -25,7 +25,7 @@ Basic example: Click to Navigate. ``` -More examples on the [Test pages for Link](../../../apps/fluent-tester/src/TestComponents/LinkV1). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). +More examples on the [Test pages for Link](../../../apps/tester-core/src/TestComponents/LinkV1). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). ## Visual Examples diff --git a/packages/components/RadioGroup/SPEC.md b/packages/components/RadioGroup/SPEC.md index 14218ce3f59..cddb63c7fac 100644 --- a/packages/components/RadioGroup/SPEC.md +++ b/packages/components/RadioGroup/SPEC.md @@ -23,7 +23,7 @@ const radiogroup = ( ); ``` -More examples on the [Test pages for RadioGroup](../../../apps/fluent-tester/src/TestComponents/RadioGroupV1). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). +More examples on the [Test pages for RadioGroup](../../../apps/tester-core/src/TestComponents/RadioGroupV1). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). ## Visual Examples diff --git a/packages/components/Switch/SPEC.md b/packages/components/Switch/SPEC.md index 28d69944722..e5ac00f29fd 100644 --- a/packages/components/Switch/SPEC.md +++ b/packages/components/Switch/SPEC.md @@ -16,7 +16,7 @@ Basic example: ``` -More examples on the [Test pages for the Switch](../../../apps/fluent-tester/src/TestComponents/Switch). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). +More examples on the [Test pages for the Switch](../../../apps/tester-core/src/TestComponents/Switch). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). ## Visual Examples diff --git a/packages/components/TabList/SPEC.md b/packages/components/TabList/SPEC.md index c56e2ac1816..4909b65afe9 100644 --- a/packages/components/TabList/SPEC.md +++ b/packages/components/TabList/SPEC.md @@ -22,7 +22,7 @@ const tablist = ( ); ``` -More examples on the [Test pages for TabList](../../../apps/fluent-tester/src/TestComponents/TabList). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). +More examples on the [Test pages for TabList](../../../apps/tester-core/src/TestComponents/TabList). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). ## Visual Examples diff --git a/packages/components/Text/SPEC.md b/packages/components/Text/SPEC.md index d58ddcf3771..a7a2de18f28 100644 --- a/packages/components/Text/SPEC.md +++ b/packages/components/Text/SPEC.md @@ -24,7 +24,7 @@ Basic example: Hello World ``` -More examples on the [Test pages for Text](../../../apps/fluent-tester/src/TestComponents/TextV1). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). +More examples on the [Test pages for Text](../../../apps/tester-core/src/TestComponents/TextV1). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). ## Visual Examples diff --git a/packages/experimental/Shadow/SPEC.md b/packages/experimental/Shadow/SPEC.md index 2c6388c8f25..49f18853b4f 100644 --- a/packages/experimental/Shadow/SPEC.md +++ b/packages/experimental/Shadow/SPEC.md @@ -33,7 +33,7 @@ Examples adding some Shadows to a Button: ``` -For more examples of using Shadow, please see the [ShadowTest test page](https://github.com/microsoft/fluentui-react-native/tree/main/apps/fluent-tester/src/TestComponents/Shadow) in the [Fluent Tester app](https://github.com/microsoft/fluentui-react-native/blob/main/apps/fluent-tester/README.md). +For more examples of using Shadow, please see the [ShadowTest test page](https://github.com/microsoft/fluentui-react-native/tree/main/apps/tester-core/src/TestComponents/Shadow) in the [Fluent Tester app](https://github.com/microsoft/fluentui-react-native/blob/main/apps/fluent-tester/README.md). For an example of adding a Shadow as a slot to a Fluent component, please see the [FAB component](https://github.com/microsoft/fluentui-react-native/tree/main/packages/components/Button/src/FAB) - this component exists on both iOS and Android, but currently only the iOS version uses the Shadow component. The [Notification component](https://github.com/microsoft/fluentui-react-native/tree/main/packages/components/Notification) is another example that uses the Shadow component. diff --git a/packages/experimental/Shimmer/SPEC.md b/packages/experimental/Shimmer/SPEC.md index 4df2ea43707..2f58da54cdc 100644 --- a/packages/experimental/Shimmer/SPEC.md +++ b/packages/experimental/Shimmer/SPEC.md @@ -57,7 +57,7 @@ function shimmerRects(): Array { ; ``` -More examples on the [Test pages for the Shimmer](../../../apps/fluent-tester/src/TestComponents/Shimmer). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). +More examples on the [Test pages for the Shimmer](../../../apps/tester-core/src/TestComponents/Shimmer). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). ## Visual Examples diff --git a/packages/experimental/Tooltip/SPEC.md b/packages/experimental/Tooltip/SPEC.md index 872ddec43c2..62425fe9c03 100644 --- a/packages/experimental/Tooltip/SPEC.md +++ b/packages/experimental/Tooltip/SPEC.md @@ -16,7 +16,7 @@ const tooltip = ( ); ``` -More examples on the [Test pages for Tooltip](../../../apps/fluent-tester/src/TestComponents/Tooltip). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). +More examples on the [Test pages for Tooltip](../../../apps/tester-core/src/TestComponents/Tooltip). Instructions on running the tester app can be found [here](../../../apps/fluent-tester/README.md). ## Visual Examples From e043dddbfd8cece74408da4e309a2d08ce713f9b Mon Sep 17 00:00:00 2001 From: Jason Morse Date: Mon, 2 Feb 2026 11:54:15 -0800 Subject: [PATCH 15/15] Change files --- ...native-avatar-689eeb48-a17f-4e9e-b79f-0851a62daeb1.json | 7 +++++++ ...-native-badge-c8e784a3-a099-423c-9b00-394902c91768.json | 7 +++++++ ...tive-checkbox-f7259c8f-e095-4e14-bba6-308783257d5a.json | 7 +++++++ ...t-native-chip-75152240-0686-467f-91ab-065fe4a758bb.json | 7 +++++++ ...ental-shimmer-2dc223cd-30f0-49d6-89e5-55a93f329af2.json | 7 +++++++ ...-native-input-2c14e3bd-0d66-406a-9a03-e69d6f966661.json | 7 +++++++ ...t-native-link-9380078e-205f-4cb3-9fb0-ae48cc0b919c.json | 7 +++++++ ...e-radio-group-498dca51-c0bd-40d9-8dc7-857fc6a801c2.json | 7 +++++++ ...t-native-text-56972ac3-eb80-4ea3-9803-94b9a069b787.json | 7 +++++++ 9 files changed, 63 insertions(+) create mode 100644 change/@fluentui-react-native-avatar-689eeb48-a17f-4e9e-b79f-0851a62daeb1.json create mode 100644 change/@fluentui-react-native-badge-c8e784a3-a099-423c-9b00-394902c91768.json create mode 100644 change/@fluentui-react-native-checkbox-f7259c8f-e095-4e14-bba6-308783257d5a.json create mode 100644 change/@fluentui-react-native-chip-75152240-0686-467f-91ab-065fe4a758bb.json create mode 100644 change/@fluentui-react-native-experimental-shimmer-2dc223cd-30f0-49d6-89e5-55a93f329af2.json create mode 100644 change/@fluentui-react-native-input-2c14e3bd-0d66-406a-9a03-e69d6f966661.json create mode 100644 change/@fluentui-react-native-link-9380078e-205f-4cb3-9fb0-ae48cc0b919c.json create mode 100644 change/@fluentui-react-native-radio-group-498dca51-c0bd-40d9-8dc7-857fc6a801c2.json create mode 100644 change/@fluentui-react-native-text-56972ac3-eb80-4ea3-9803-94b9a069b787.json diff --git a/change/@fluentui-react-native-avatar-689eeb48-a17f-4e9e-b79f-0851a62daeb1.json b/change/@fluentui-react-native-avatar-689eeb48-a17f-4e9e-b79f-0851a62daeb1.json new file mode 100644 index 00000000000..62520383717 --- /dev/null +++ b/change/@fluentui-react-native-avatar-689eeb48-a17f-4e9e-b79f-0851a62daeb1.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "fix dead links in markdown files", + "packageName": "@fluentui-react-native/avatar", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-badge-c8e784a3-a099-423c-9b00-394902c91768.json b/change/@fluentui-react-native-badge-c8e784a3-a099-423c-9b00-394902c91768.json new file mode 100644 index 00000000000..5a9170abe06 --- /dev/null +++ b/change/@fluentui-react-native-badge-c8e784a3-a099-423c-9b00-394902c91768.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "fix dead links in markdown files", + "packageName": "@fluentui-react-native/badge", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-checkbox-f7259c8f-e095-4e14-bba6-308783257d5a.json b/change/@fluentui-react-native-checkbox-f7259c8f-e095-4e14-bba6-308783257d5a.json new file mode 100644 index 00000000000..7fb9a0bf7d7 --- /dev/null +++ b/change/@fluentui-react-native-checkbox-f7259c8f-e095-4e14-bba6-308783257d5a.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "fix dead links in markdown files", + "packageName": "@fluentui-react-native/checkbox", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-chip-75152240-0686-467f-91ab-065fe4a758bb.json b/change/@fluentui-react-native-chip-75152240-0686-467f-91ab-065fe4a758bb.json new file mode 100644 index 00000000000..eac0aa4e6ff --- /dev/null +++ b/change/@fluentui-react-native-chip-75152240-0686-467f-91ab-065fe4a758bb.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "fix dead links in markdown files", + "packageName": "@fluentui-react-native/chip", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-experimental-shimmer-2dc223cd-30f0-49d6-89e5-55a93f329af2.json b/change/@fluentui-react-native-experimental-shimmer-2dc223cd-30f0-49d6-89e5-55a93f329af2.json new file mode 100644 index 00000000000..2eed07832b2 --- /dev/null +++ b/change/@fluentui-react-native-experimental-shimmer-2dc223cd-30f0-49d6-89e5-55a93f329af2.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "fix dead links in markdown files", + "packageName": "@fluentui-react-native/experimental-shimmer", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-input-2c14e3bd-0d66-406a-9a03-e69d6f966661.json b/change/@fluentui-react-native-input-2c14e3bd-0d66-406a-9a03-e69d6f966661.json new file mode 100644 index 00000000000..0c9de6905c5 --- /dev/null +++ b/change/@fluentui-react-native-input-2c14e3bd-0d66-406a-9a03-e69d6f966661.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "fix dead links in markdown files", + "packageName": "@fluentui-react-native/input", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-link-9380078e-205f-4cb3-9fb0-ae48cc0b919c.json b/change/@fluentui-react-native-link-9380078e-205f-4cb3-9fb0-ae48cc0b919c.json new file mode 100644 index 00000000000..2bfccf40ba3 --- /dev/null +++ b/change/@fluentui-react-native-link-9380078e-205f-4cb3-9fb0-ae48cc0b919c.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "fix dead links in markdown files", + "packageName": "@fluentui-react-native/link", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-radio-group-498dca51-c0bd-40d9-8dc7-857fc6a801c2.json b/change/@fluentui-react-native-radio-group-498dca51-c0bd-40d9-8dc7-857fc6a801c2.json new file mode 100644 index 00000000000..4142f4d0bba --- /dev/null +++ b/change/@fluentui-react-native-radio-group-498dca51-c0bd-40d9-8dc7-857fc6a801c2.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "fix dead links in markdown files", + "packageName": "@fluentui-react-native/radio-group", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-text-56972ac3-eb80-4ea3-9803-94b9a069b787.json b/change/@fluentui-react-native-text-56972ac3-eb80-4ea3-9803-94b9a069b787.json new file mode 100644 index 00000000000..4886e4d1d3d --- /dev/null +++ b/change/@fluentui-react-native-text-56972ac3-eb80-4ea3-9803-94b9a069b787.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "fix dead links in markdown files", + "packageName": "@fluentui-react-native/text", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +}