((userProps: Overf
const clone = React.cloneElement(child, viewProps);
return clone;
- };
+ });
});
OverflowItem.displayName = overflowItemName;
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/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,
+ }
}
>
```
-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/Shadow/package.json b/packages/experimental/Shadow/package.json
index ee4d5ac055d..19a26f0a05a 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..b54f972aa27 100644
--- a/packages/experimental/Shadow/src/Shadow.tsx
+++ b/packages/experimental/Shadow/src/Shadow.tsx
@@ -2,39 +2,30 @@ 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, directComponent } 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 directComponent((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};
- };
+ });
});
const getStylePropsForShadowViews = memoize(getStylePropsForShadowViewsWorker);
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/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
diff --git a/packages/experimental/Tooltip/package.json b/packages/experimental/Tooltip/package.json
index 79e1b0ddce2..9ead4289d61 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": "catalog:",
diff --git a/packages/experimental/Tooltip/src/Tooltip.tsx b/packages/experimental/Tooltip/src/Tooltip.tsx
index e20acf27f20..08b8180ed3a 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, directComponent } 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,13 +31,14 @@ export const Tooltip = stagedComponent((props: TooltipProps) => {
}
}, [target]);
- const TooltipComponent = (rest: TooltipProps, children: React.ReactNode) => {
+ const TooltipComponent = directComponent((innerProps: TooltipProps) => {
+ const { children, ...rest } = innerProps;
return (
{children}
);
- };
+ });
return TooltipComponent;
});
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/directComponent.ts b/packages/framework-base/src/component-patterns/directComponent.ts
new file mode 100644
index 00000000000..4c085fc73db
--- /dev/null
+++ b/packages/framework-base/src/component-patterns/directComponent.ts
@@ -0,0 +1,9 @@
+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: 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
new file mode 100644
index 00000000000..6e4052a3181
--- /dev/null
+++ b/packages/framework-base/src/component-patterns/phasedComponent.ts
@@ -0,0 +1,54 @@
+import React from 'react';
+import type { ComposableFunction, PhasedComponent, PhasedRender, FunctionComponent } from './render.types';
+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 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;
+ 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;
+ };
+ }
+ }
+ return undefined;
+}
+
+/**
+ * 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): FunctionComponent {
+ return Object.assign(
+ (props: React.PropsWithChildren) => {
+ // pull out children from props
+ const { children, ...outerProps } = props;
+ const Inner = getInnerPhase(outerProps as TProps);
+ 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 80f0848f2e3..a5133d779e2 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,23 @@ 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) {
+ if (React.Children.count(props.children) > 1) {
+ jsxFn = ReactJSX.jsxs;
+ } else {
+ jsxFn = ReactJSX.jsx;
+ }
+ }
+ // now call the appropriate jsx function to render the component
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/component-patterns/render.types.ts b/packages/framework-base/src/component-patterns/render.types.ts
index f86b39bbfe8..33c4b830054 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
*
@@ -29,12 +34,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;
};
@@ -59,52 +72,55 @@ 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 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.
+ * 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 StagedComponent = React.FunctionComponent & {
- _twoStageRender?: TwoStageRender;
+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 = 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.
@@ -113,6 +129,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..913b298f250 100644
--- a/packages/framework-base/src/index.ts
+++ b/packages/framework-base/src/index.ts
@@ -19,22 +19,38 @@ export type { StyleProp } from './merge-props/mergeStyles.types';
export { mergeStyles } from './merge-props/mergeStyles';
export { mergeProps } from './merge-props/mergeProps';
-// component pattern exports
-export { renderForClassicRuntime, renderForJsxRuntime, renderSlot } from './component-patterns/render';
+// component pattern exports - rendering utilities
+export { renderForJsxRuntime, renderSlot, asDirectComponent } from './component-patterns/render';
+
+// component pattern exports - core types
export type {
DirectComponent,
- DirectComponentFunction,
+ FunctionComponent,
+ FunctionComponentCore,
LegacyDirectComponent,
- StagedComponent,
- StagedRender,
- TwoStageRender,
+ PhasedComponent,
+ PhasedRender,
+ PropsOf,
RenderType,
RenderResult,
+ StagedRender,
ComposableFunction,
FinalRender,
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 { stagedComponent } from './component-patterns/stagedComponent';
+
+// component pattern exports - legacy JSX handlers
export { withSlots } from './component-patterns/withSlots';
-export { stagedComponent, twoStageComponent } from './component-patterns/stagedComponent';
+
+// jsx runtime exports
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/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 561ed093c6c..2a76b5c91b6 100644
--- a/packages/framework/use-slot/src/useSlot.test.tsx
+++ b/packages/framework/use-slot/src/useSlot.test.tsx
@@ -3,19 +3,18 @@ import * as React from 'react';
import type { TextProps, TextStyle } 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 type { NativeReactType } from '@fluentui-react-native/framework-base';
-import { stagedComponent } from '@fluentui-react-native/framework-base';
+import { phasedComponent, directComponent } from '@fluentui-react-native/framework-base';
import { useSlot } from './useSlot';
-type PluggableTextProps = React.PropsWithChildren & { inner?: NativeReactType | 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.
*/
-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,29 +23,32 @@ 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 directComponent((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,
-) => {
- // split out any passed in style
+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);
// return a closure to complete the staged pattern
- return (extra: PluggableTextProps, children: React.ReactNode) => {children};
+ return directComponent((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' }), []);
@@ -63,7 +65,7 @@ const CaptionText = stagedComponent((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 d473437ac02..6052882938e 100644
--- a/packages/framework/use-slot/src/useSlot.ts
+++ b/packages/framework/use-slot/src/useSlot.ts
@@ -1,72 +1,62 @@
import * as React from 'react';
-import { mergeProps } from '@fluentui-react-native/framework-base';
+import { mergeProps, getPhasedRender, directComponent, renderForJsxRuntime, filterProps } from '@fluentui-react-native/framework-base';
+import type { PropsFilter, FunctionComponent } 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';
+export type ComponentType = React.ComponentType;
-/**
- *
- * @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;
-}
+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 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 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: 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,
+ hookProps?: Partial,
+ filter?: PropsFilter,
+): FunctionComponent {
+ // create this once for this hook instance to hold slot data between phases
+ const slotData = React.useMemo(() => {
+ return {} as SlotData;
+ }, []);
+
+ // 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;
+ }
// 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];
- }, [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;
+ 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],
+ );
}
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;
}
diff --git a/yarn.lock b/yarn.lock
index 67325583316..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:*"
@@ -4201,6 +4200,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:*"
@@ -4467,6 +4467,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:*"
@@ -4882,6 +4883,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:*"
@@ -5913,7 +5915,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:*"