diff --git a/.changeset/nine-bees-relate.md b/.changeset/nine-bees-relate.md new file mode 100644 index 00000000000..c17423da94a --- /dev/null +++ b/.changeset/nine-bees-relate.md @@ -0,0 +1,25 @@ +--- +"@uifabricshared/foundation-composable": minor +"@fluentui-react-native/themed-stylesheet": minor +"@fluentui-react-native/focus-trap-zone": minor +"@fluentui-react-native/persona-coin": minor +"@fluentui-react-native/menu-button": minor +"@fluentui-react-native/overflow": minor +"@fluentui-react-native/composition": minor +"@fluentui-react-native/use-styling": minor +"@fluentui-react-native/focus-zone": minor +"@fluentui-react-native/experimental-shimmer": minor +"@fluentui-react-native/use-tokens": minor +"@fluentui-react-native/dependency-profiles": minor +"@fluentui-react-native/framework": minor +"@fluentui-react-native/use-slots": minor +"@fluentui-react-native/persona": minor +"@fluentui-react-native/avatar": minor +"@fluentui-react-native/stack": minor +"@fluentui-react-native/menu": minor +"@fluentui-react-native/theme": minor +"@fluentui-react-native/framework-base": minor +"@fluentui-react-native/adapters": minor +--- + +rework framework rendering to prepare for new component structure diff --git a/AGENTS.md b/AGENTS.md index 19fb94bc8dc..76dbdce9446 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -155,7 +155,7 @@ The composition framework uses precise types for better type safety: - `MIGRATION.md` - Migration guide (for V1 components) - `tsconfig.json`, `babel.config.js`, `jest.config.js`, `eslint.config.js` -**Using Composition Framework**: Use `@fluentui-react-native/composition` for new components. For simpler components without slots/tokens, use the `stagedComponent` pattern from `@fluentui-react-native/use-slot`. +**Using Composition Framework**: Use `@fluentui-react-native/composition` for new components. For simpler components without slots/tokens, use the `stagedComponent` pattern from `@fluentui-react-native/framework-base`. **JSX Runtime**: All components use the modern automatic JSX runtime: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cedd4aa560b..59ab6118208 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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-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). +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/phased.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: - `fluentui-react-native/packages/components/` @@ -63,8 +63,7 @@ Reach out to Samuel Freiberg with any questions related to E2E testing. 1. Create a `src/` subdirectory in your component directory with a minimum of two files (listed below). You may optionally choose to subdivide your code however you wish; there are plenty of examples in the other components of FluentUI React Native. 1. `index.ts` - This is the file listed as `main` inside your package.json and simply exports other files. - 1. `.tsx` - This is the file that will actually define your function component, and compose it into a higher order component with slots, theming, and design tokens. - Note that we need the comment `/** @jsxRuntime classic */ -/** @jsx withSlots */` at the top of this file. An explanation can be found in the comment at `packages/experimental/use-slots/src/withSlots.tsx` + 1. `.tsx` - This is the file that will actually define your function component, and compose it into a higher order component with slots, theming, and design tokens. - Note that we need the comment `/** @jsxImportSource @fluentui-react-native/framework-base */` as the first line of this file. This opts the file into the custom JSX runtime that handles slot rendering; an explanation can be found in `packages/framework-base/src/component-patterns/README.md`. Packages using this pragma must include `@fluentui-react-native/framework-base` in their `devDependencies`. 1. `..tsx` (Optional) - Optional extra files to subdivide your code however you see fit. You can also add platform specific files as you see fit. diff --git a/apps/component-generator/component-templates/ComponentTemplate/package.json b/apps/component-generator/component-templates/ComponentTemplate/package.json index f669d064962..2f69c0e6e17 100644 --- a/apps/component-generator/component-templates/ComponentTemplate/package.json +++ b/apps/component-generator/component-templates/ComponentTemplate/package.json @@ -26,6 +26,7 @@ }, "dependencies": {}, "devDependencies": { + "@fluentui-react-native/framework-base": "workspace:*", "@fluentui-react-native/scripts": "workspace:*", "@fluentui-react-native/test-tools": "workspace:*", "react": "18.2.0", diff --git a/apps/component-generator/component-templates/ComponentTemplate/src/ComponentName.tsx b/apps/component-generator/component-templates/ComponentTemplate/src/ComponentName.tsx index 0a1b0d3a436..8589b9cec52 100644 --- a/apps/component-generator/component-templates/ComponentTemplate/src/ComponentName.tsx +++ b/apps/component-generator/component-templates/ComponentTemplate/src/ComponentName.tsx @@ -1,11 +1,10 @@ -/** @jsxRuntime classic */ -/** @jsx withSlots */ +/** @jsxImportSource @fluentui-react-native/framework-base */ import * as React from 'react'; import { View } from 'react-native'; import { componentName, ComponentNameType, ComponentNameProps } from './ComponentName.types'; import { TextV1 as Text } from '@fluentui-react-native/text'; import { stylingSettings } from './ComponentName.styling'; -import { compose, mergeProps, withSlots, UseSlots } from '@fluentui-react-native/framework'; +import { compose, mergeProps, UseSlots } from '@fluentui-react-native/framework'; import { useComponentName } from './useComponentName'; /** * A function which determines if a set of styles should be applied to the component given the current state and props of the component-name. diff --git a/apps/fluent-tester/macos/Podfile.lock b/apps/fluent-tester/macos/Podfile.lock index def1409b608..6c57fc16799 100644 --- a/apps/fluent-tester/macos/Podfile.lock +++ b/apps/fluent-tester/macos/Podfile.lock @@ -4,10 +4,10 @@ PODS: - fast_float (8.0.0) - FBLazyVector (0.81.2) - fmt (11.0.2) - - FRNAvatar (0.22.4): + - FRNAvatar (0.22.5): - MicrosoftFluentUI (= 0.13.1) - React - - FRNCallout (0.28.4): + - FRNCallout (0.28.5): - boost - DoubleConversion - fast_float @@ -36,13 +36,13 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - FRNCheckbox (0.18.4): + - FRNCheckbox (0.18.5): - React - - FRNMenuButton (0.14.4): + - FRNMenuButton (0.14.5): - React - - FRNRadioButton (0.22.4): + - FRNRadioButton (0.22.5): - React - - FRNVibrancyView (0.4.4): + - FRNVibrancyView (0.4.5): - React - glog (0.3.5) - hermes-engine (0.81.5): @@ -137,7 +137,7 @@ PODS: - fmt (= 11.0.2) - glog - RCTDeprecation (0.81.2) - - RCTFocusZone (0.22.4): + - RCTFocusZone (0.22.5): - React - RCTRequired (0.81.2) - RCTTypeSafety (0.81.2): @@ -2667,18 +2667,18 @@ SPEC CHECKSUMS: fast_float: 20817c22759af6ac8d4d67e6e059b8b499953656 FBLazyVector: 0e3076dbb16169b0afc2d701a24a526423b5e76b fmt: 24e7591456deb60b4a77518f83d9a916ac84223f - FRNAvatar: 586585a440e2b23c9ac872fd1fa2ddb7819cd7bd - FRNCallout: f3234f07e6f49334c0f1de7cb7185494f2915db5 - FRNCheckbox: a5afb9bb4e6405d39a30f997f7e69d6e5672533d - FRNMenuButton: e9e49a3afa5eb5ecbe4a36c41047bce763f661c7 - FRNRadioButton: 6f7db3424604a413b4cca73c5a22033c79f92d43 - FRNVibrancyView: a9cf18fcaee2a7d9f9ea292924ebf1b3acb0a5b2 + FRNAvatar: c4baf9356785232c547bb76a9ccd290e94e0fda9 + FRNCallout: ee98ce83b788ff3d3716463269560487b787f658 + FRNCheckbox: 2e74a528f6328ea79bf9eaa2a1d466dd8f30be4f + FRNMenuButton: bb6913908137c530f1a3af2d8d7d5c6a5fae4e19 + FRNRadioButton: adf6b247f1143c9e79f63221405e9b94ef7bef6d + FRNVibrancyView: ccff736e2c309eba2ed56f1b803a0e55b8a3067f glog: ba31c1afa7dcf1915a109861bccdb4421be6175b hermes-engine: 9f4dfe93326146a1c99eb535b1cb0b857a3cd172 MicrosoftFluentUI: dde98d8ed3fc306d9ddd0a6f0bc0c1f24fe5275e RCT-Folly: c803cf33238782d5fd21a5e02d44f64068e0e130 RCTDeprecation: b60b889eafa75f46c3d6be5332681efbb16ad0c7 - RCTFocusZone: a6d62c46c530e790479915bc9faa578137e9e2a6 + RCTFocusZone: f4199120b2260bc5710359852686307a0f8d8b0e RCTRequired: 070d7f0ef937e7a93aab20761cef55419f16b5de RCTTypeSafety: dd2ad1eb0c0c01ba41222d115be141f9a348c636 React: 304df3d34c8c0281fd1fd3a506e69f234095425d diff --git a/apps/tester-core/src/TestComponents/Shadow/ShadowDepthTestSection.tsx b/apps/tester-core/src/TestComponents/Shadow/ShadowDepthTestSection.tsx index 78fba881a80..4f2f8dd45e3 100644 --- a/apps/tester-core/src/TestComponents/Shadow/ShadowDepthTestSection.tsx +++ b/apps/tester-core/src/TestComponents/Shadow/ShadowDepthTestSection.tsx @@ -3,7 +3,8 @@ import { View, type ViewStyle } from 'react-native'; import { Text } from '@fluentui/react-native'; import { Shadow, getShadowTokenStyleSet } from '@fluentui-react-native/experimental-shadow'; -import { mergeStyles, useFluentTheme } from '@fluentui-react-native/framework'; +import { useFluentTheme } from '@fluentui-react-native/framework'; +import { mergeStyles } from '@fluentui-react-native/framework-base'; import type { ShadowToken, Theme } from '@fluentui-react-native/theme-types'; import { useTheme } from '@fluentui-react-native/theme-types'; import { themedStyleSheet } from '@fluentui-react-native/themed-stylesheet'; diff --git a/docs/pages/Theming/Tokens/UsageWithComponentTokens.md b/docs/pages/Theming/Tokens/UsageWithComponentTokens.md index 15346e5d555..d571aca713e 100644 --- a/docs/pages/Theming/Tokens/UsageWithComponentTokens.md +++ b/docs/pages/Theming/Tokens/UsageWithComponentTokens.md @@ -7,9 +7,8 @@ This page covers how to access color tokens to fill out a component's tokens. Most components in FURN build a component's styling out of the tokens and theme that are fed into the component. The `compose` framework takes in an object with a `tokens` property, which you can use to define how a component's tokens are filled out. The `tokens` property can be assigned to a function which takes a `Theme` object as an argument and returns an object that is the component's `Token` type. Inside the function, you can access the alias function from the theme as you would if you had the theme inside a component's render function. ```tsx -/** @jsxRuntime classic */ -/** @jsx withSlots */ -import { Theme, TokenSettings, buildProps, compose, mergeProps, withSlots, UseSlots } from '@fluentui-react-native/framework'; +/** @jsxImportSource @fluentui-react-native/framework-base */ +import { Theme, TokenSettings, buildProps, compose, mergeProps, UseSlots } from '@fluentui-react-native/framework'; import { fontStyles } from '@fluentui-react-native/tokens'; import { Text } from '@fluentui-react-native/experimental-text'; diff --git a/packages/agentic-components/package.json b/packages/agentic-components/package.json index d249d873b8d..fc6dcf881b6 100644 --- a/packages/agentic-components/package.json +++ b/packages/agentic-components/package.json @@ -31,6 +31,9 @@ "test": "fluentui-scripts jest", "update-snapshots": "fluentui-scripts jest -u" }, + "dependencies": { + "@fluentui-react-native/framework-base": "workspace:*" + }, "devDependencies": { "@babel/core": "catalog:", "@fluentui-react-native/scripts": "workspace:*", diff --git a/packages/agentic-components/src/components/button/button.tsx b/packages/agentic-components/src/components/button/button.tsx index f2d4e108bea..c2316572b11 100644 --- a/packages/agentic-components/src/components/button/button.tsx +++ b/packages/agentic-components/src/components/button/button.tsx @@ -1,6 +1,6 @@ -import * as React from 'react'; import { Pressable, StyleSheet, Text } from 'react-native'; import type { GestureResponderEvent, StyleProp, ViewStyle } from 'react-native'; +import type { FurnJSX } from '@fluentui-react-native/framework-base'; /** * Props for the simple {@link Button} component. @@ -47,7 +47,7 @@ const styles = StyleSheet.create({ /** * A simple cross-platform button built directly on react-native primitives. */ -export function Button(props: ButtonProps): React.JSX.Element { +export function Button(props: ButtonProps): FurnJSX.Element { const { title, onPress, disabled, style } = props; return ( {initials} ) : avatarProps.icon ? ( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - TODO, fix typing error ) : ( svgIconsEnabled && ( diff --git a/packages/components/Badge/src/Badge.tsx b/packages/components/Badge/src/Badge.tsx index d91b5906d5f..6513456a4af 100644 --- a/packages/components/Badge/src/Badge.tsx +++ b/packages/components/Badge/src/Badge.tsx @@ -5,7 +5,8 @@ import { View, I18nManager } from 'react-native'; import { Shadow } from '@fluentui-react-native/experimental-shadow'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import { Icon, createIconProps } from '@fluentui-react-native/icon'; import { TextV1 as Text } from '@fluentui-react-native/text'; diff --git a/packages/components/Badge/src/CounterBadge/CounterBadge.tsx b/packages/components/Badge/src/CounterBadge/CounterBadge.tsx index 38a0a6f783b..1079d92e7e8 100644 --- a/packages/components/Badge/src/CounterBadge/CounterBadge.tsx +++ b/packages/components/Badge/src/CounterBadge/CounterBadge.tsx @@ -5,7 +5,8 @@ import { View, I18nManager } from 'react-native'; import { Shadow } from '@fluentui-react-native/experimental-shadow'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import { Icon, createIconProps } from '@fluentui-react-native/icon'; import { TextV1 as Text } from '@fluentui-react-native/text'; diff --git a/packages/components/Button/src/Button.tsx b/packages/components/Button/src/Button.tsx index 79a4c7a0383..7de08d24635 100644 --- a/packages/components/Button/src/Button.tsx +++ b/packages/components/Button/src/Button.tsx @@ -4,7 +4,8 @@ import { Platform, Pressable, View } from 'react-native'; import { ActivityIndicator } from '@fluentui-react-native/experimental-activity-indicator'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, memoize, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { memoize, mergeProps } from '@fluentui-react-native/framework-base'; import { Icon, createIconProps } from '@fluentui-react-native/icon'; import type { IPressableState } from '@fluentui-react-native/interactive-hooks'; import { TextV1 as Text } from '@fluentui-react-native/text'; diff --git a/packages/components/Button/src/CompoundButton/CompoundButton.tsx b/packages/components/Button/src/CompoundButton/CompoundButton.tsx index 46239b4c659..0320ef77797 100644 --- a/packages/components/Button/src/CompoundButton/CompoundButton.tsx +++ b/packages/components/Button/src/CompoundButton/CompoundButton.tsx @@ -4,7 +4,8 @@ import { Platform, Pressable, View } from 'react-native'; import { ActivityIndicator } from '@fluentui-react-native/experimental-activity-indicator'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import { Icon, createIconProps } from '@fluentui-react-native/icon'; import { TextV1 as Text } from '@fluentui-react-native/text'; diff --git a/packages/components/Button/src/ExtractStyle.android.ts b/packages/components/Button/src/ExtractStyle.android.ts index db14be02f9c..9bb5653a623 100644 --- a/packages/components/Button/src/ExtractStyle.android.ts +++ b/packages/components/Button/src/ExtractStyle.android.ts @@ -1,6 +1,6 @@ import type { ViewStyle } from 'react-native'; -import { memoize } from '@fluentui-react-native/framework'; +import { memoize } from '@fluentui-react-native/framework-base'; /** * React Native's Pressable does not support curved edges. diff --git a/packages/components/Button/src/FAB/FAB.mobile.tsx b/packages/components/Button/src/FAB/FAB.mobile.tsx index df6b305ff57..a3dce8aa2d3 100644 --- a/packages/components/Button/src/FAB/FAB.mobile.tsx +++ b/packages/components/Button/src/FAB/FAB.mobile.tsx @@ -4,7 +4,8 @@ import { Platform, Pressable, View } from 'react-native'; import { Shadow } from '@fluentui-react-native/experimental-shadow'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import { Icon, createIconProps } from '@fluentui-react-native/icon'; import type { IPressableState } from '@fluentui-react-native/interactive-hooks'; import { TextV1 as Text } from '@fluentui-react-native/text'; diff --git a/packages/components/Button/src/ToggleButton/ToggleButton.tsx b/packages/components/Button/src/ToggleButton/ToggleButton.tsx index a3fa7d89f4d..f3e570daa23 100644 --- a/packages/components/Button/src/ToggleButton/ToggleButton.tsx +++ b/packages/components/Button/src/ToggleButton/ToggleButton.tsx @@ -4,7 +4,8 @@ import { Platform, Pressable, View } from 'react-native'; import { ActivityIndicator } from '@fluentui-react-native/experimental-activity-indicator'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import { Icon, createIconProps } from '@fluentui-react-native/icon'; import { TextV1 as Text } from '@fluentui-react-native/text'; diff --git a/packages/components/Button/src/ToggleButton/useToggleButton.ts b/packages/components/Button/src/ToggleButton/useToggleButton.ts index 60e01c04872..f346e256bf9 100644 --- a/packages/components/Button/src/ToggleButton/useToggleButton.ts +++ b/packages/components/Button/src/ToggleButton/useToggleButton.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import type { AccessibilityState } from 'react-native'; -import { memoize } from '@fluentui-react-native/framework'; +import { memoize } from '@fluentui-react-native/framework-base'; import { useAsToggleWithEvent } from '@fluentui-react-native/interactive-hooks'; import type { ToggleButtonProps, ToggleButtonInfo } from './ToggleButton.types'; diff --git a/packages/components/Checkbox/src/Checkbox.tsx b/packages/components/Checkbox/src/Checkbox.tsx index 78834f4a95a..78690f7fcd9 100644 --- a/packages/components/Checkbox/src/Checkbox.tsx +++ b/packages/components/Checkbox/src/Checkbox.tsx @@ -3,7 +3,8 @@ import * as React from 'react'; import { Pressable, Platform } from 'react-native'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import { TextV1 as Text } from '@fluentui-react-native/text'; import { Svg, Path } from 'react-native-svg'; diff --git a/packages/components/Checkbox/src/useCheckbox.ts b/packages/components/Checkbox/src/useCheckbox.ts index 036f16814cd..a48345b92a6 100644 --- a/packages/components/Checkbox/src/useCheckbox.ts +++ b/packages/components/Checkbox/src/useCheckbox.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import type { AccessibilityActionEvent, AccessibilityState } from 'react-native'; -import { memoize } from '@fluentui-react-native/framework'; +import { memoize } from '@fluentui-react-native/framework-base'; import { usePressableState, useKeyProps, diff --git a/packages/components/Chip/src/Chip.mobile.tsx b/packages/components/Chip/src/Chip.mobile.tsx index aeec473fc3e..b87304737d6 100644 --- a/packages/components/Chip/src/Chip.mobile.tsx +++ b/packages/components/Chip/src/Chip.mobile.tsx @@ -4,7 +4,8 @@ import { Children } from 'react'; import { Pressable, I18nManager } from 'react-native'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import { Icon } from '@fluentui-react-native/icon'; import { TextV1 as Text } from '@fluentui-react-native/text'; diff --git a/packages/components/Chip/src/useChip.ts b/packages/components/Chip/src/useChip.ts index 5f7fe3416f7..f9bc95c73de 100644 --- a/packages/components/Chip/src/useChip.ts +++ b/packages/components/Chip/src/useChip.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import type { AccessibilityState } from 'react-native'; -import { memoize } from '@fluentui-react-native/framework'; +import { memoize } from '@fluentui-react-native/framework-base'; import { createIconProps } from '@fluentui-react-native/icon'; import { usePressableState, useAsToggleWithEvent } from '@fluentui-react-native/interactive-hooks'; import type { InteractionEvent } from '@fluentui-react-native/interactive-hooks'; diff --git a/packages/components/Divider/src/Divider.styling.ts b/packages/components/Divider/src/Divider.styling.ts index ec1dc181eea..ff9de54f971 100644 --- a/packages/components/Divider/src/Divider.styling.ts +++ b/packages/components/Divider/src/Divider.styling.ts @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import type { ViewProps, ColorValue, StyleProp, ViewStyle } from 'react-native'; import { Platform } from 'react-native'; -import { memoize, mergeStyles } from '@fluentui-react-native/framework'; +import { memoize, mergeStyles } from '@fluentui-react-native/framework-base'; import type { Theme } from '@fluentui-react-native/framework'; import type { IconPropsV1 as IconProps } from '@fluentui-react-native/icon'; import type { TextProps } from '@fluentui-react-native/text'; diff --git a/packages/components/Divider/src/Divider.tsx b/packages/components/Divider/src/Divider.tsx index dbdbb270643..db5665bec28 100644 --- a/packages/components/Divider/src/Divider.tsx +++ b/packages/components/Divider/src/Divider.tsx @@ -3,7 +3,8 @@ import React from 'react'; import { View } from 'react-native'; import type { ViewProps } from 'react-native'; -import { compressible, useSlot, useFluentTheme, patchTokens } from '@fluentui-react-native/framework'; +import { compressible, useFluentTheme, patchTokens } from '@fluentui-react-native/framework'; +import { useSlot } from '@fluentui-react-native/framework-base'; import type { UseTokens } from '@fluentui-react-native/framework'; import { IconV1 as Icon } from '@fluentui-react-native/icon'; import type { IconPropsV1 as IconProps } from '@fluentui-react-native/icon'; diff --git a/packages/components/FocusTrapZone/src/FocusTrapZone.ts b/packages/components/FocusTrapZone/src/FocusTrapZone.ts index ab75fc6e98c..93aec9ada11 100644 --- a/packages/components/FocusTrapZone/src/FocusTrapZone.ts +++ b/packages/components/FocusTrapZone/src/FocusTrapZone.ts @@ -12,8 +12,12 @@ import { mergeSettings } from '@uifabricshared/foundation-settings'; import type { IFocusTrapZoneProps, IFocusTrapZoneSlotProps, IFocusTrapZoneType } from './FocusTrapZone.types'; import RCTFocusTrapZone from './FocusTrapZoneNativeComponent'; -export function filterOutComponentRef(propName: string): boolean { - return propName !== 'componentRef'; +export function filterOutComponentRef(props: T): T { + if (props && typeof props === 'object' && 'componentRef' in props) { + const { componentRef: _componentRef, ...rest } = props as Record; + return rest as T; + } + return props; } export const FocusTrapZone = composable({ diff --git a/packages/components/FocusZone/src/FocusZone.ts b/packages/components/FocusZone/src/FocusZone.ts index 3735b09ad86..24d4d3886c9 100644 --- a/packages/components/FocusZone/src/FocusZone.ts +++ b/packages/components/FocusZone/src/FocusZone.ts @@ -15,7 +15,13 @@ import { mergeSettings } from '@uifabricshared/foundation-settings'; import type { FocusZoneProps, FocusZoneSlotProps, FocusZoneType } from './FocusZone.types'; import RCTFocusZone from './FocusZoneNativeComponent'; -const filterOutComponentRef = (propName) => propName !== 'componentRef'; +const filterOutComponentRef = (props: T): T => { + if (props && typeof props === 'object' && 'componentRef' in props) { + const { componentRef: _componentRef, ...rest } = props as Record; + return rest as T; + } + return props; +}; export const FocusZone = composable({ usePrepareProps: (userProps: FocusZoneProps, useStyling: IUseStyling) => { diff --git a/packages/components/Icon/src/FontIcon/useFontIcon.ts b/packages/components/Icon/src/FontIcon/useFontIcon.ts index 3ce7c1ef4ec..3470b716e85 100644 --- a/packages/components/Icon/src/FontIcon/useFontIcon.ts +++ b/packages/components/Icon/src/FontIcon/useFontIcon.ts @@ -1,7 +1,7 @@ import type { TextStyle } from 'react-native'; import { Image, Platform } from 'react-native'; -import { getMemoCache, mergeStyles } from '@fluentui-react-native/framework'; +import { getMemoCache, mergeStyles } from '@fluentui-react-native/framework-base'; import type { FontIconProps } from './FontIcon.types'; diff --git a/packages/components/Icon/src/SvgIcon/useSvgIcon.ts b/packages/components/Icon/src/SvgIcon/useSvgIcon.ts index aa23677b63e..404b3a6dd3f 100644 --- a/packages/components/Icon/src/SvgIcon/useSvgIcon.ts +++ b/packages/components/Icon/src/SvgIcon/useSvgIcon.ts @@ -1,4 +1,4 @@ -import { getMemoCache, mergeStyles } from '@fluentui-react-native/framework'; +import { getMemoCache, mergeStyles } from '@fluentui-react-native/framework-base'; import type { SvgIconProps } from './SvgIcon.types'; import type { ImageStyle } from 'react-native'; diff --git a/packages/components/Icon/src/legacy/Icon.tsx b/packages/components/Icon/src/legacy/Icon.tsx index 182d8f21176..a241dbfa469 100644 --- a/packages/components/Icon/src/legacy/Icon.tsx +++ b/packages/components/Icon/src/legacy/Icon.tsx @@ -1,8 +1,15 @@ 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, directComponent, mergeProps, getMemoCache, getTypedMemoCache } from '@fluentui-react-native/framework-base'; +import { useFluentTheme } from '@fluentui-react-native/framework'; +import { + phasedComponent, + directComponent, + mergeProps, + getMemoCache, + getTypedMemoCache, + mergeStyles, +} 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'; diff --git a/packages/components/Input/src/Input.mobile.tsx b/packages/components/Input/src/Input.mobile.tsx index 5040b4a2809..2eae8aab67b 100644 --- a/packages/components/Input/src/Input.mobile.tsx +++ b/packages/components/Input/src/Input.mobile.tsx @@ -3,7 +3,8 @@ import { Fragment } from 'react'; import { Pressable, ScrollView, TextInput, View } from 'react-native'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import { createIconProps } from '@fluentui-react-native/icon'; import { Icon } from '@fluentui-react-native/icon'; import type { FocusState } from '@fluentui-react-native/interactive-hooks'; diff --git a/packages/components/Link/src/Link.tsx b/packages/components/Link/src/Link.tsx index c15c763e25c..f58ac4997e1 100644 --- a/packages/components/Link/src/Link.tsx +++ b/packages/components/Link/src/Link.tsx @@ -3,7 +3,8 @@ import React from 'react'; import { Platform, View } from 'react-native'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import { TextV1 as Text } from '@fluentui-react-native/text'; import { stylingSettings } from './Link.styling'; diff --git a/packages/components/Menu/src/MenuGroup/MenuGroup.tsx b/packages/components/Menu/src/MenuGroup/MenuGroup.tsx index bd3c4789cd6..f9ea374312e 100644 --- a/packages/components/Menu/src/MenuGroup/MenuGroup.tsx +++ b/packages/components/Menu/src/MenuGroup/MenuGroup.tsx @@ -4,8 +4,8 @@ import { Platform, View } from 'react-native'; import type { IViewProps } from '@fluentui-react-native/adapters'; import { FocusZone } from '@fluentui-react-native/focus-zone'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; -import { extractChildren, extractProps } from '@fluentui-react-native/framework-base'; +import { compose } from '@fluentui-react-native/framework'; +import { extractChildren, extractProps, mergeProps } from '@fluentui-react-native/framework-base'; import type { UseSlots } from '@fluentui-react-native/framework'; import type { MenuGroupProps, MenuGroupType } from './MenuGroup.types'; diff --git a/packages/components/Menu/src/MenuGroupHeader/MenuGroupHeader.tsx b/packages/components/Menu/src/MenuGroupHeader/MenuGroupHeader.tsx index f2ea3abf0a6..1eb906b7a3f 100644 --- a/packages/components/Menu/src/MenuGroupHeader/MenuGroupHeader.tsx +++ b/packages/components/Menu/src/MenuGroupHeader/MenuGroupHeader.tsx @@ -1,7 +1,8 @@ /** @jsxImportSource @fluentui-react-native/framework-base */ import React from 'react'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import type { UseSlots } from '@fluentui-react-native/framework'; import { TextV1 as Text } from '@fluentui-react-native/text'; diff --git a/packages/components/Menu/src/MenuItem/MenuItem.tsx b/packages/components/Menu/src/MenuItem/MenuItem.tsx index f59928f95f0..bc54d927c14 100644 --- a/packages/components/Menu/src/MenuItem/MenuItem.tsx +++ b/packages/components/Menu/src/MenuItem/MenuItem.tsx @@ -3,7 +3,8 @@ import React from 'react'; import { I18nManager, Image, Pressable, View } from 'react-native'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, memoize, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { memoize, mergeProps } from '@fluentui-react-native/framework-base'; import { IconV1 as Icon } from '@fluentui-react-native/icon'; import { TextV1 as Text } from '@fluentui-react-native/text'; import { SvgXml } from 'react-native-svg'; diff --git a/packages/components/Menu/src/MenuItem/useMenuItem.ts b/packages/components/Menu/src/MenuItem/useMenuItem.ts index 146f4d46994..7364567e474 100644 --- a/packages/components/Menu/src/MenuItem/useMenuItem.ts +++ b/packages/components/Menu/src/MenuItem/useMenuItem.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import type { AccessibilityState } from 'react-native'; import { I18nManager, Platform } from 'react-native'; -import { memoize } from '@fluentui-react-native/framework'; +import { memoize } from '@fluentui-react-native/framework-base'; import type { InteractionEvent } from '@fluentui-react-native/interactive-hooks'; import { isKeyPressEvent, usePressableState, useKeyDownProps, useViewCommandFocus } from '@fluentui-react-native/interactive-hooks'; diff --git a/packages/components/Menu/src/MenuItemCheckbox/MenuItemCheckbox.tsx b/packages/components/Menu/src/MenuItemCheckbox/MenuItemCheckbox.tsx index a08da2b84d8..7cf36afdae9 100644 --- a/packages/components/Menu/src/MenuItemCheckbox/MenuItemCheckbox.tsx +++ b/packages/components/Menu/src/MenuItemCheckbox/MenuItemCheckbox.tsx @@ -2,11 +2,13 @@ import React from 'react'; import { Image, Platform, Pressable, View } from 'react-native'; -import type { Slots, UseSlots, FinalRender } from '@fluentui-react-native/framework'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import type { Slots, UseSlots } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import { IconV1 as Icon } from '@fluentui-react-native/icon'; import { TextV1 as Text } from '@fluentui-react-native/text'; import { SvgXml } from 'react-native-svg'; +import type { LegacyFunctionComponent } from '@fluentui-react-native/framework-base'; import { stylingSettings } from './MenuItemCheckbox.styling'; import type { @@ -42,7 +44,7 @@ export const MenuItemCheckbox = compose({ export const menuItemFinalRender = ( menuItem: MenuItemCheckboxInfo, Slots: Slots, -): FinalRender => { +): LegacyFunctionComponent => { return (final: MenuItemCheckboxProps, ...children: React.ReactNode[]) => { const { accessibilityLabel, icon, tooltip, ...mergedProps } = mergeProps(menuItem.props, final); diff --git a/packages/components/Menu/src/MenuItemCheckbox/useMenuItemCheckbox.ts b/packages/components/Menu/src/MenuItemCheckbox/useMenuItemCheckbox.ts index fd4094b47d3..70573ad7834 100644 --- a/packages/components/Menu/src/MenuItemCheckbox/useMenuItemCheckbox.ts +++ b/packages/components/Menu/src/MenuItemCheckbox/useMenuItemCheckbox.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import type { AccessibilityActionEvent, AccessibilityState } from 'react-native'; import { I18nManager, Platform } from 'react-native'; -import { memoize } from '@fluentui-react-native/framework'; +import { memoize } from '@fluentui-react-native/framework-base'; import type { InteractionEvent, KeyPressEvent } from '@fluentui-react-native/interactive-hooks'; import { usePressableState, useKeyDownProps, useOnPressWithFocus, useViewCommandFocus } from '@fluentui-react-native/interactive-hooks'; diff --git a/packages/components/Menu/src/MenuItemRadio/MenuItemRadio.tsx b/packages/components/Menu/src/MenuItemRadio/MenuItemRadio.tsx index 90c1731d5ae..cacf2b21faa 100644 --- a/packages/components/Menu/src/MenuItemRadio/MenuItemRadio.tsx +++ b/packages/components/Menu/src/MenuItemRadio/MenuItemRadio.tsx @@ -2,8 +2,10 @@ import React from 'react'; import { Image, Platform, Pressable, View } from 'react-native'; -import type { Slots, UseSlots, FinalRender } from '@fluentui-react-native/framework'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import type { Slots, UseSlots } from '@fluentui-react-native/framework'; +import type { LegacyFunctionComponent } from '@fluentui-react-native/framework-base'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import { IconV1 as Icon } from '@fluentui-react-native/icon'; import { TextV1 as Text } from '@fluentui-react-native/text'; import { SvgXml } from 'react-native-svg'; @@ -38,7 +40,7 @@ export const MenuItemRadio = compose({ export const menuItemRadioFinalRender = ( menuItem: MenuItemRadioInfo, Slots: Slots, -): FinalRender => { +): LegacyFunctionComponent => { return (final: MenuItemRadioProps, ...children: React.ReactNode[]) => { const { accessibilityLabel, icon, tooltip, ...mergedProps } = mergeProps(menuItem.props, final); diff --git a/packages/components/Menu/src/MenuPopover/MenuPopover.tsx b/packages/components/Menu/src/MenuPopover/MenuPopover.tsx index e4f14fdbb06..98421699326 100644 --- a/packages/components/Menu/src/MenuPopover/MenuPopover.tsx +++ b/packages/components/Menu/src/MenuPopover/MenuPopover.tsx @@ -2,7 +2,8 @@ import React from 'react'; import { Platform, View } from 'react-native'; import type { UseTokens } from '@fluentui-react-native/framework'; -import { compressible, mergeProps, patchTokens, useFluentTheme } from '@fluentui-react-native/framework'; +import { compressible, patchTokens, useFluentTheme } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import type { MenuPopoverProps, MenuPopoverTokens } from './MenuPopover.types'; import { menuPopoverName } from './MenuPopover.types'; diff --git a/packages/components/MenuButton/src/MenuButton.tsx b/packages/components/MenuButton/src/MenuButton.tsx index cbd44823dfd..ea26c457b37 100644 --- a/packages/components/MenuButton/src/MenuButton.tsx +++ b/packages/components/MenuButton/src/MenuButton.tsx @@ -9,6 +9,7 @@ import type { IUseComposeStyling } from '@uifabricshared/foundation-compose'; import { compose } from '@uifabricshared/foundation-compose'; import { mergeSettings } from '@uifabricshared/foundation-settings'; import { SvgXml } from 'react-native-svg'; +import type { FurnJSX } from '@fluentui-react-native/framework-base'; import { defaultIconColor, primaryIconColor } from './MenuButton.style'; import type { @@ -133,7 +134,7 @@ export const MenuButton = compose({ }, }); -const SubMenuItem: React.FunctionComponent = (props: MenuButtonItemProps): React.JSX.Element => { +const SubMenuItem: React.FunctionComponent = (props: MenuButtonItemProps): FurnJSX.Element => { const [showSubmenuState, setShowSubmenu] = React.useState(false); const toggleShowSubmenu = React.useCallback(() => { setShowSubmenu(!showSubmenuState); diff --git a/packages/components/Notification/src/Notification.tsx b/packages/components/Notification/src/Notification.tsx index 8bd336f263c..3a6c7ecd0e2 100644 --- a/packages/components/Notification/src/Notification.tsx +++ b/packages/components/Notification/src/Notification.tsx @@ -6,7 +6,8 @@ import type { SizeClassIOS } from '@fluentui-react-native/experimental-appearanc import { useHorizontalSizeClass } from '@fluentui-react-native/experimental-appearance-additions'; import { Shadow } from '@fluentui-react-native/experimental-shadow'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps, memoize } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps, memoize } from '@fluentui-react-native/framework-base'; import { Icon, createIconProps } from '@fluentui-react-native/icon'; import type { InteractionEvent } from '@fluentui-react-native/interactive-hooks'; import { Pressable } from '@fluentui-react-native/pressable'; diff --git a/packages/components/Persona/src/Persona.tsx b/packages/components/Persona/src/Persona.tsx index a8b982d16be..9aa649c97f7 100644 --- a/packages/components/Persona/src/Persona.tsx +++ b/packages/components/Persona/src/Persona.tsx @@ -16,6 +16,7 @@ import { buildStackStyle } from './Persona.tokens.stack'; import { buildTextStyle, buildTertiaryStyle, buildOptionalStyle, buildSecondaryStyle } from './Persona.tokens.texts'; import { personaName } from './Persona.types'; import type { IPersonaType, IPersonaSlotProps, IPersonaProps, IPersonaRenderData } from './Persona.types'; +import type { FurnJSX } from '@fluentui-react-native/framework-base'; function usePrepareForProps(props: IPersonaProps, useStyling: IUseComposeStyling): IRenderData { const { @@ -49,7 +50,7 @@ function usePrepareForProps(props: IPersonaProps, useStyling: IUseComposeStyling }; } -const render = (Slots: ISlots, renderData: IPersonaRenderData): React.JSX.Element | null => { +const render = (Slots: ISlots, renderData: IPersonaRenderData): FurnJSX.Element | null => { if (!renderData.state) { return null; } diff --git a/packages/components/PersonaCoin/src/PersonaCoin.tsx b/packages/components/PersonaCoin/src/PersonaCoin.tsx index 8cc5f34d14e..875ecfa72f6 100644 --- a/packages/components/PersonaCoin/src/PersonaCoin.tsx +++ b/packages/components/PersonaCoin/src/PersonaCoin.tsx @@ -7,6 +7,7 @@ import type { ISlots, IRenderData } from '@uifabricshared/foundation-composable' import { compose } from '@uifabricshared/foundation-compose'; import type { IUseComposeStyling } from '@uifabricshared/foundation-compose'; import { mergeSettings } from '@uifabricshared/foundation-settings'; +import type { FurnJSX } from '@fluentui-react-native/framework-base'; import { getPresenceIconSource } from './PersonaCoin.helpers'; import { settings } from './PersonaCoin.settings'; @@ -61,7 +62,7 @@ function usePrepareForProps( }; } -const render = (Slots: ISlots, renderData: IPersonaCoinRenderData): React.JSX.Element | null => { +const render = (Slots: ISlots, renderData: IPersonaCoinRenderData): FurnJSX.Element | null => { if (!renderData.state) { return null; } diff --git a/packages/components/RadioGroup/src/Radio/Radio.ios.tsx b/packages/components/RadioGroup/src/Radio/Radio.ios.tsx index 719c7970d07..de1a416fa7f 100644 --- a/packages/components/RadioGroup/src/Radio/Radio.ios.tsx +++ b/packages/components/RadioGroup/src/Radio/Radio.ios.tsx @@ -2,7 +2,8 @@ import { Platform, Pressable, View } from 'react-native'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import type { PressableState } from '@fluentui-react-native/interactive-hooks'; import { TextV1 as Text } from '@fluentui-react-native/text'; import { Svg, Path } from 'react-native-svg'; diff --git a/packages/components/RadioGroup/src/Radio/Radio.tsx b/packages/components/RadioGroup/src/Radio/Radio.tsx index 04eebafbd55..babb192cfda 100644 --- a/packages/components/RadioGroup/src/Radio/Radio.tsx +++ b/packages/components/RadioGroup/src/Radio/Radio.tsx @@ -2,7 +2,8 @@ import { Platform, Pressable, View } from 'react-native'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import type { PressableState } from '@fluentui-react-native/interactive-hooks'; import { TextV1 as Text } from '@fluentui-react-native/text'; diff --git a/packages/components/RadioGroup/src/Radio/useRadio.ts b/packages/components/RadioGroup/src/Radio/useRadio.ts index 2036924178f..7e310dfd4dc 100644 --- a/packages/components/RadioGroup/src/Radio/useRadio.ts +++ b/packages/components/RadioGroup/src/Radio/useRadio.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import type { AccessibilityState } from 'react-native'; -import { memoize } from '@fluentui-react-native/framework'; +import { memoize } from '@fluentui-react-native/framework-base'; import { usePressableState, useOnPressWithFocus, useViewCommandFocus } from '@fluentui-react-native/interactive-hooks'; import type { RadioProps, RadioInfo } from './Radio.types'; diff --git a/packages/components/RadioGroup/src/Radio/useRadio.win32.ts b/packages/components/RadioGroup/src/Radio/useRadio.win32.ts index c99d57561ed..e2ba4077d0a 100644 --- a/packages/components/RadioGroup/src/Radio/useRadio.win32.ts +++ b/packages/components/RadioGroup/src/Radio/useRadio.win32.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import type { AccessibilityState } from 'react-native'; import { I18nManager } from 'react-native'; -import { memoize } from '@fluentui-react-native/framework'; +import { memoize } from '@fluentui-react-native/framework-base'; import type { KeyPressEvent } from '@fluentui-react-native/interactive-hooks'; import { usePressableState, useOnPressWithFocus, useViewCommandFocus, useKeyDownProps } from '@fluentui-react-native/interactive-hooks'; diff --git a/packages/components/RadioGroup/src/RadioGroup/RadioGroup.tsx b/packages/components/RadioGroup/src/RadioGroup/RadioGroup.tsx index fc73041ea4f..16e0cdda501 100644 --- a/packages/components/RadioGroup/src/RadioGroup/RadioGroup.tsx +++ b/packages/components/RadioGroup/src/RadioGroup/RadioGroup.tsx @@ -4,7 +4,8 @@ import { Platform, View } from 'react-native'; import { FocusZone } from '@fluentui-react-native/focus-zone'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import { TextV1 as Text } from '@fluentui-react-native/text'; import { stylingSettings } from './RadioGroup.styling'; diff --git a/packages/components/RadioGroup/src/RadioGroup/useRadioGroup.ts b/packages/components/RadioGroup/src/RadioGroup/useRadioGroup.ts index 64c1a36c822..cc81867ca72 100644 --- a/packages/components/RadioGroup/src/RadioGroup/useRadioGroup.ts +++ b/packages/components/RadioGroup/src/RadioGroup/useRadioGroup.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import type { View } from 'react-native'; import type { AccessibilityState } from 'react-native'; -import { memoize } from '@fluentui-react-native/framework'; +import { memoize } from '@fluentui-react-native/framework-base'; import { useSelectedKey as useValue } from '@fluentui-react-native/interactive-hooks'; import type { RadioGroupInfo, RadioGroupProps, RadioGroupState } from './RadioGroup.types'; diff --git a/packages/components/Separator/src/Separator.tsx b/packages/components/Separator/src/Separator.tsx index 87631255393..db2fa658e03 100644 --- a/packages/components/Separator/src/Separator.tsx +++ b/packages/components/Separator/src/Separator.tsx @@ -2,7 +2,8 @@ import { View } from 'react-native'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import { stylingSettings } from './Separator.styling'; import type { SeparatorProps, SeparatorType } from './Separator.types'; diff --git a/packages/components/Stack/src/Stack.tsx b/packages/components/Stack/src/Stack.tsx index 3deba9fdde3..83bcae642fb 100644 --- a/packages/components/Stack/src/Stack.tsx +++ b/packages/components/Stack/src/Stack.tsx @@ -9,6 +9,7 @@ import type { ISlots } from '@uifabricshared/foundation-composable'; import type { IUseComposeStyling } from '@uifabricshared/foundation-compose'; import { compose } from '@uifabricshared/foundation-compose'; import { mergeSettings } from '@uifabricshared/foundation-settings'; +import type { FurnJSX } from '@fluentui-react-native/framework-base'; import { settings } from './Stack.settings'; import { buildStackRootStyles, buildStackInnerStyles } from './Stack.tokens'; @@ -35,7 +36,7 @@ function _mixinStyle(style: StyleProp | undefined, mixin: ObjectBase const _styleKey = 'style'; -const render = (Slots: ISlots, renderData: IStackRenderData, ...children: React.ReactNode[]): React.JSX.Element => { +const render = (Slots: ISlots, renderData: IStackRenderData, ...children: React.ReactNode[]): FurnJSX.Element => { const { gap, horizontal, wrap } = renderData.state!; if (gap && gap > 0 && children && globalThis.__jsiExecutorDescription !== 'ChakraRuntime') { diff --git a/packages/components/Switch/src/Switch.tsx b/packages/components/Switch/src/Switch.tsx index d14ef9c64a6..909f32ae2bc 100644 --- a/packages/components/Switch/src/Switch.tsx +++ b/packages/components/Switch/src/Switch.tsx @@ -3,8 +3,8 @@ import type { StyleProp, TextStyle } from 'react-native'; import { View, AccessibilityInfo, Pressable, Animated, Platform } from 'react-native'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, memoize, mergeProps } from '@fluentui-react-native/framework'; -import { extractStyle } from '@fluentui-react-native/framework-base'; +import { compose } from '@fluentui-react-native/framework'; +import { extractStyle, memoize, mergeProps } from '@fluentui-react-native/framework-base'; import { Text } from '@fluentui-react-native/text'; import { stylingSettings } from './Switch.styling'; diff --git a/packages/components/Switch/src/useSwitch.ts b/packages/components/Switch/src/useSwitch.ts index a3e0930b7bc..9193673ae49 100644 --- a/packages/components/Switch/src/useSwitch.ts +++ b/packages/components/Switch/src/useSwitch.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import type { AccessibilityState, AccessibilityActionEvent } from 'react-native'; import { Animated, Platform, I18nManager } from 'react-native'; -import { memoize } from '@fluentui-react-native/framework'; +import { memoize } from '@fluentui-react-native/framework-base'; import { usePressableState, useKeyProps, useOnPressWithFocus, useViewCommandFocus } from '@fluentui-react-native/interactive-hooks'; import { useAsToggleWithEvent } from '@fluentui-react-native/interactive-hooks'; diff --git a/packages/components/TabList/src/Tab/Tab.tsx b/packages/components/TabList/src/Tab/Tab.tsx index 1004e83021b..ae01ddfe775 100644 --- a/packages/components/TabList/src/Tab/Tab.tsx +++ b/packages/components/TabList/src/Tab/Tab.tsx @@ -4,7 +4,8 @@ import { View, Pressable } from 'react-native'; import type { ViewProps } from 'react-native'; import type { UseTokens } from '@fluentui-react-native/framework'; -import { compressible, useSlot, useFluentTheme, applyTokenLayers, mergeProps } from '@fluentui-react-native/framework'; +import { compressible, useFluentTheme, applyTokenLayers } from '@fluentui-react-native/framework'; +import { useSlot, mergeProps } from '@fluentui-react-native/framework-base'; import { IconV1 as Icon } from '@fluentui-react-native/icon'; import type { IconPropsV1 as IconProps } from '@fluentui-react-native/icon'; import type { PressablePropsExtended } from '@fluentui-react-native/interactive-hooks'; diff --git a/packages/components/TabList/src/Tab/useTab.ts b/packages/components/TabList/src/Tab/useTab.ts index cd6b4f25015..b0e09568dbf 100644 --- a/packages/components/TabList/src/Tab/useTab.ts +++ b/packages/components/TabList/src/Tab/useTab.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import type { AccessibilityActionEvent, AccessibilityState } from 'react-native'; -import { memoize } from '@fluentui-react-native/framework'; +import { memoize } from '@fluentui-react-native/framework-base'; import type { IFocusable } from '@fluentui-react-native/interactive-hooks'; import { usePressableState, useKeyProps, useOnPressWithFocus, useViewCommandFocus } from '@fluentui-react-native/interactive-hooks'; diff --git a/packages/components/TabList/src/Tab/useTab.win32.ts b/packages/components/TabList/src/Tab/useTab.win32.ts index 9d1409049f6..09e3395f1ae 100644 --- a/packages/components/TabList/src/Tab/useTab.win32.ts +++ b/packages/components/TabList/src/Tab/useTab.win32.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import type { AccessibilityActionEvent, AccessibilityState } from 'react-native'; -import { memoize } from '@fluentui-react-native/framework'; +import { memoize } from '@fluentui-react-native/framework-base'; import type { IFocusable } from '@fluentui-react-native/interactive-hooks'; import { usePressableState, useKeyProps, useViewCommandFocus } from '@fluentui-react-native/interactive-hooks'; diff --git a/packages/components/TabList/src/TabList/TabList.tsx b/packages/components/TabList/src/TabList/TabList.tsx index 450a906dba8..ff13178e6ec 100644 --- a/packages/components/TabList/src/TabList/TabList.tsx +++ b/packages/components/TabList/src/TabList/TabList.tsx @@ -4,7 +4,8 @@ import { View } from 'react-native'; import { FocusZone } from '@fluentui-react-native/focus-zone'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import { stylingSettings } from './TabList.styling'; import type { TabListType, TabListProps } from './TabList.types'; diff --git a/packages/components/TabList/src/TabList/useTabList.ts b/packages/components/TabList/src/TabList/useTabList.ts index fe1a860b9b3..ac311b46171 100644 --- a/packages/components/TabList/src/TabList/useTabList.ts +++ b/packages/components/TabList/src/TabList/useTabList.ts @@ -3,7 +3,7 @@ import { Platform } from 'react-native'; import type { View, AccessibilityState, LayoutRectangle, LayoutChangeEvent } from 'react-native'; import type { NativeKeyEvent } from '@fluentui-react-native/adapters'; -import { memoize, mergeStyles } from '@fluentui-react-native/framework'; +import { memoize, mergeStyles } from '@fluentui-react-native/framework-base'; import { useSelectedKey } from '@fluentui-react-native/interactive-hooks'; import type { TabListInfo, TabListProps } from './TabList.types'; diff --git a/packages/components/Text/src/Text.tsx b/packages/components/Text/src/Text.tsx index 63c3c098ac7..22545c3312b 100644 --- a/packages/components/Text/src/Text.tsx +++ b/packages/components/Text/src/Text.tsx @@ -3,7 +3,8 @@ import React from 'react'; import { I18nManager, Platform, Text as RNText } from 'react-native'; import type { UseTokens, FontWeightValue } from '@fluentui-react-native/framework'; -import { fontStyles, useFluentTheme, mergeStyles, compressible, patchTokens } from '@fluentui-react-native/framework'; +import { fontStyles, useFluentTheme, compressible, patchTokens } from '@fluentui-react-native/framework'; +import { mergeStyles } from '@fluentui-react-native/framework-base'; import { useKeyProps } from '@fluentui-react-native/interactive-hooks'; import { globalTokens } from '@fluentui-react-native/theme-tokens'; diff --git a/packages/dependency-profiles/src/index.js b/packages/dependency-profiles/src/index.js index b0559c63276..16d6dfd7b77 100644 --- a/packages/dependency-profiles/src/index.js +++ b/packages/dependency-profiles/src/index.js @@ -226,10 +226,6 @@ module.exports = { "name": "@fluentui-react-native/themed-stylesheet", "version": "1.9.1" }, - "@fluentui-react-native/use-slot": { - "name": "@fluentui-react-native/use-slot", - "version": "0.8.1" - }, "@fluentui-react-native/use-slots": { "name": "@fluentui-react-native/use-slots", "version": "0.12.1" diff --git a/packages/deprecated/foundation-composable/README.md b/packages/deprecated/foundation-composable/README.md index fe9625dc367..a7cc6018c00 100644 --- a/packages/deprecated/foundation-composable/README.md +++ b/packages/deprecated/foundation-composable/README.md @@ -140,12 +140,10 @@ The three parameters are: - `renderData` - is the object returned by `usePrepareProps`, this generally contains `slotProps` and `state`. - `...children` - this is the children property from initial input props for the component. It can be handled as is standard for a react component. -The `Slots` parameter allows plugging different react types into a complex component. These can be output by either using `renderSlot` or by using the `withSlots` jsx helper. Usage would be something like: +The `Slots` parameter allows plugging different react types into a complex component. These can be output by either using `renderSlot` or by rendering the slot directly through the custom JSX runtime. Usage would be something like: ```tsx -/** @jsxRuntime classic */ -/* @jsx withSlots */ -import { withSlots } from '@uifabricshared/foundation-composable'; +/** @jsxImportSource @fluentui-react-native/framework-base */ render: (Slots: ISlots, renderData, ...children) => { // do some work @@ -176,7 +174,7 @@ Using composable, the normal flow is broken into two primary parts: `usePrepareP ### How Slots work -The `Slots` parameter passed to `render` is an object containing functions which mimic the shape of `React.createElement`. When used with the @jsx helper `withSlots`, These functions: +The `Slots` parameter passed to `render` is an object containing functions which mimic the shape of `React.createElement`. When rendered through the custom JSX runtime (via the `@jsxImportSource @fluentui-react-native/framework-base` pragma), these functions: 1. Are created once. This is essential because creating a new closure on every render pass will cause the tree to remount. They are stored in a state hook but then never updated over the lifetime of the component. 1. Include references to the `slotProps` returned as part of `usePrepareProps`. This allows for filters to act upon the props and avoids having to manually write `` every time. It also allows additional props to be mixed in directly to the JSX tree with the merge happening implicitly before render. diff --git a/packages/deprecated/foundation-composable/docs/GuideHOC.md b/packages/deprecated/foundation-composable/docs/GuideHOC.md index 7464541dc7f..3c5d870458b 100644 --- a/packages/deprecated/foundation-composable/docs/GuideHOC.md +++ b/packages/deprecated/foundation-composable/docs/GuideHOC.md @@ -113,8 +113,7 @@ Here we will use the same interfaces and helpers but implement `ButtonBase` and We will use the optional TState to pass additional arguments between `usePrepareProps` and `render`. Note that the state can be anything, it is simply a payload to be passed from one function to the next. ```tsx -/** @jsxRuntime classic */ -/** @jsx withSlots */ +/** @jsxImportSource @fluentui-react-native/framework-base */ export interface IButtonState { disabled?: boolean; diff --git a/packages/deprecated/foundation-composable/src/Composable.slots.ts b/packages/deprecated/foundation-composable/src/Composable.slots.ts index 3f6c970c749..55650f821b5 100644 --- a/packages/deprecated/foundation-composable/src/Composable.slots.ts +++ b/packages/deprecated/foundation-composable/src/Composable.slots.ts @@ -1,13 +1,17 @@ /* eslint-disable */ import * as React from 'react'; -import { IRenderData, ISlotWithFilter, IComposable, IWithComposable, ISlots, IPropFilter, INativeSlotType } from './Composable.types'; +import { IRenderData, ISlotWithFilter, IComposable, IWithComposable, ISlots, IPropFilter } from './Composable.types'; import { mergeSettings, ISlotProps } from '@uifabricshared/foundation-settings'; -import { mergeProps } from '@fluentui-react-native/framework-base'; +import { + mergeProps, + legacyDirectComponent, + directComponent, + type LegacyDirectComponent, + renderJsx, +} from '@fluentui-react-native/framework-base'; -export type ISlotFn = React.FunctionComponent & { - _canCompose?: boolean; -}; +export type ISlotFn = LegacyDirectComponent; interface ISlotRenderInfo { composable: IComposable; @@ -21,26 +25,12 @@ function _mergeAndFilterProps(propsBase: TProps, propsExtra: TProps, fil // do a basic merge, not mutating if nothing changed let props = mergeProps(propsBase, propsExtra); if (filter && props) { - const removeMask = {}; - Object.getOwnPropertyNames(props).forEach((key) => { - if (!filter(key)) { - removeMask[key] = undefined; - } - }); - props = mergeProps(props, removeMask as TProps); + // the filter is a prop transform: it takes the merged props and returns a filtered set of props + props = filter(props); } return props; } -/** - * Helper function to add the _canCompose settings to a given render function - * @param fn - function to decorate with _canCompose - */ -function _createSlotRenderFunction(fn: React.FunctionComponent): React.FunctionComponent { - (fn as ISlotFn)._canCompose = true; - return fn; -} - function createSlotRenderInfo( composable: IComposable, slotInfo?: ISlotWithFilter, @@ -59,7 +49,7 @@ function createSlotRenderInfo( const childRenderInfo = (childInfo[slot as string] = createSlotRenderInfo(composable, slots[slot])); if (composable) { // create the actual closure for rendering handing it a reference to the render info - Slots[slot] = _createSlotRenderFunction((extraProps: object, ...children: React.ReactNode[]) => { + Slots[slot] = legacyDirectComponent((extraProps: object, ...children: React.ReactNode[]) => { const { renderData, Slots } = childRenderInfo; if (filter || extraProps) { const toMerge = { root: _mergeAndFilterProps(renderData.slotProps.root, extraProps, filter) }; @@ -69,9 +59,9 @@ function createSlotRenderInfo( }); } else { // non-composable components should just render directly - Slots[slot] = _createSlotRenderFunction((extraProps: object, ...children: React.ReactNode[]) => { + Slots[slot] = directComponent((extraProps: object) => { const props = _mergeAndFilterProps(childRenderInfo.renderData.slotProps.root, extraProps, filter); - return React.createElement(slotType as INativeSlotType, props, ...children); + return renderJsx(slotType, props); }); } }); diff --git a/packages/deprecated/foundation-composable/src/Composable.ts b/packages/deprecated/foundation-composable/src/Composable.ts index b931dd04708..2d025ea6f14 100644 --- a/packages/deprecated/foundation-composable/src/Composable.ts +++ b/packages/deprecated/foundation-composable/src/Composable.ts @@ -17,15 +17,16 @@ import { ISlotWithFilter, } from './Composable.types'; import { useCompoundPrepare } from './Composable.slots'; -import { renderSlot, type FurnJSX } from '@fluentui-react-native/framework-base'; +import { renderJsx, type FurnJSX, setPropsChildren } from '@fluentui-react-native/framework-base'; import { ISlotProps, mergeSettings } from '@uifabricshared/foundation-settings'; export function atomicRender( Slots: ISlots>, _renderData: IRenderData, TState>, - ...children: React.ReactNode[] + children: React.ReactNode, ): FurnJSX.Element | null { - return renderSlot(Slots.root, undefined, ...children); + const props = setPropsChildren({} as TProps, children); + return renderJsx(Slots.root, props); } export function atomicUsePrepareProps, TState = object>( diff --git a/packages/deprecated/foundation-composable/src/Composable.types.ts b/packages/deprecated/foundation-composable/src/Composable.types.ts index de9159d8127..87a8d942c13 100644 --- a/packages/deprecated/foundation-composable/src/Composable.types.ts +++ b/packages/deprecated/foundation-composable/src/Composable.types.ts @@ -146,10 +146,11 @@ export type IWithComposable = T & { export type INativeSlotType = React.ElementType | string; /** - * Optional function to filter the properties that will be passed to the component. If no props are to be - * removed it should return the same object. Otherwise it should return a new object with props filtered + * Optional transform to filter the properties that will be passed to the component. It receives the + * merged props and returns a filtered set of props. If no props are to be removed it should return the + * same object. */ -export type IPropFilter = (propName: string) => boolean; +export type IPropFilter = (props: TProps) => TProps; /** * In the case where a filter needs to be applied to props the slot can be set to an object which contains the slotType diff --git a/packages/experimental/ActivityIndicator/src/ActivityIndicator.mobile.tsx b/packages/experimental/ActivityIndicator/src/ActivityIndicator.mobile.tsx index ed43745ce45..47832914730 100644 --- a/packages/experimental/ActivityIndicator/src/ActivityIndicator.mobile.tsx +++ b/packages/experimental/ActivityIndicator/src/ActivityIndicator.mobile.tsx @@ -3,7 +3,8 @@ import { useRef, useEffect, useCallback } from 'react'; import { Animated, Easing, View } from 'react-native'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps, buildUseStyling } from '@fluentui-react-native/framework'; +import { compose, buildUseStyling } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import { Svg, Path } from 'react-native-svg'; import type { SvgProps } from 'react-native-svg'; diff --git a/packages/experimental/AppearanceAdditions/package.json b/packages/experimental/AppearanceAdditions/package.json index 1672a502aa6..8fc60a48545 100644 --- a/packages/experimental/AppearanceAdditions/package.json +++ b/packages/experimental/AppearanceAdditions/package.json @@ -32,12 +32,11 @@ "update-snapshots": "fluentui-scripts jest -u" }, "dependencies": { - "@fluentui-react-native/framework": "workspace:*", + "@fluentui-react-native/framework-base": "workspace:*", "use-subscription": "^1.11.0" }, "devDependencies": { "@babel/core": "catalog:", - "@fluentui-react-native/framework": "workspace:*", "@fluentui-react-native/scripts": "workspace:*", "@react-native-community/cli": "^20.0.0", "@react-native-community/cli-platform-android": "^20.0.0", diff --git a/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ios.ts b/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ios.ts index aeffcba10dc..0d4bccb2794 100644 --- a/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ios.ts +++ b/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ios.ts @@ -1,6 +1,6 @@ import { NativeEventEmitter } from 'react-native'; -import { memoize } from '@fluentui-react-native/framework'; +import { memoize } from '@fluentui-react-native/framework-base'; import NativeAppearanceAdditions from './NativeAppearanceAdditions'; import type { AppearanceAdditions, SizeClass, UserInterfaceLevel, AccessibilityContrastOption } from './NativeAppearanceAdditions.types'; diff --git a/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ts b/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ts index ecf41c6acbb..5341575a353 100644 --- a/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ts +++ b/packages/experimental/AppearanceAdditions/src/appearanceAdditions.ts @@ -1,4 +1,4 @@ -import { memoize } from '@fluentui-react-native/framework'; +import { memoize } from '@fluentui-react-native/framework-base'; import type { AppearanceAdditions } from './NativeAppearanceAdditions.types'; diff --git a/packages/experimental/AppearanceAdditions/tsconfig.json b/packages/experimental/AppearanceAdditions/tsconfig.json index be88a1fa63a..256f00585b8 100644 --- a/packages/experimental/AppearanceAdditions/tsconfig.json +++ b/packages/experimental/AppearanceAdditions/tsconfig.json @@ -9,7 +9,7 @@ "include": ["src"], "references": [ { - "path": "../../framework/framework/tsconfig.json" + "path": "../../framework-base/tsconfig.json" }, { "path": "../../../scripts/tsconfig.json" diff --git a/packages/experimental/Avatar/src/NativeAvatar.tsx b/packages/experimental/Avatar/src/NativeAvatar.tsx index b3dfc65a9ed..add94fe8891 100644 --- a/packages/experimental/Avatar/src/NativeAvatar.tsx +++ b/packages/experimental/Avatar/src/NativeAvatar.tsx @@ -8,7 +8,8 @@ import type { ImageSource, ViewProps, ColorValue } from 'react-native'; import { NativeModules, TurboModuleRegistry } from 'react-native'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, buildProps, mergeProps } from '@fluentui-react-native/framework'; +import { compose, buildProps } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; const avatarName = 'NativeAvatar'; diff --git a/packages/experimental/Checkbox/src/Checkbox.macos.tsx b/packages/experimental/Checkbox/src/Checkbox.macos.tsx index eff1a7751ab..f977f1083e9 100644 --- a/packages/experimental/Checkbox/src/Checkbox.macos.tsx +++ b/packages/experimental/Checkbox/src/Checkbox.macos.tsx @@ -7,7 +7,8 @@ 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 { compose, buildProps } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import NativeCheckboxView from './MacOSCheckboxNativeComponent'; import type { NativeProps } from './MacOSCheckboxNativeComponent'; diff --git a/packages/experimental/Drawer/src/Drawer.tsx b/packages/experimental/Drawer/src/Drawer.tsx index c94e088193f..77581b64d6e 100644 --- a/packages/experimental/Drawer/src/Drawer.tsx +++ b/packages/experimental/Drawer/src/Drawer.tsx @@ -2,7 +2,8 @@ import { Animated, Modal, TouchableWithoutFeedback, View } from 'react-native'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import { stylingSettings } from './Drawer.styling'; import type { DrawerType, DrawerProps } from './Drawer.types'; diff --git a/packages/experimental/Dropdown/src/Dropdown/Dropdown.tsx b/packages/experimental/Dropdown/src/Dropdown/Dropdown.tsx index c9a309ab49b..9593c07afe4 100644 --- a/packages/experimental/Dropdown/src/Dropdown/Dropdown.tsx +++ b/packages/experimental/Dropdown/src/Dropdown/Dropdown.tsx @@ -5,7 +5,8 @@ import { View } from 'react-native'; import type { ButtonProps } from '@fluentui-react-native/button'; import { ButtonV1 as Button } from '@fluentui-react-native/button'; import type { UseTokens } from '@fluentui-react-native/framework'; -import { buildUseTokens, compressible, useSlot } from '@fluentui-react-native/framework'; +import { buildUseTokens, compressible } from '@fluentui-react-native/framework'; +import { useSlot } from '@fluentui-react-native/framework-base'; import type { PressablePropsExtended } from '@fluentui-react-native/interactive-hooks'; import type { SvgProps } from 'react-native-svg'; import { Path, Svg } from 'react-native-svg'; diff --git a/packages/experimental/Dropdown/src/Listbox/Listbox.tsx b/packages/experimental/Dropdown/src/Listbox/Listbox.tsx index 572beef5aee..01163d01c95 100644 --- a/packages/experimental/Dropdown/src/Listbox/Listbox.tsx +++ b/packages/experimental/Dropdown/src/Listbox/Listbox.tsx @@ -6,7 +6,8 @@ import type { IViewProps } from '@fluentui-react-native/adapters'; import type { ICalloutProps } from '@fluentui-react-native/callout'; import { Callout } from '@fluentui-react-native/callout'; import type { UseTokens } from '@fluentui-react-native/framework'; -import { buildUseTokens, compressible, useSlot } from '@fluentui-react-native/framework'; +import { buildUseTokens, compressible } from '@fluentui-react-native/framework'; +import { useSlot } from '@fluentui-react-native/framework-base'; import type { ListboxProps, ListboxTokens } from './Listbox.types'; import { listboxName } from './Listbox.types'; diff --git a/packages/experimental/Dropdown/src/Option/Option.tsx b/packages/experimental/Dropdown/src/Option/Option.tsx index a6382d8a9fc..015b4328e55 100644 --- a/packages/experimental/Dropdown/src/Option/Option.tsx +++ b/packages/experimental/Dropdown/src/Option/Option.tsx @@ -3,7 +3,8 @@ import React from 'react'; import { View } from 'react-native'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps } from '@fluentui-react-native/framework'; +import { compose } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import { TextV1 as Text } from '@fluentui-react-native/text'; import { Path, Svg } from 'react-native-svg'; diff --git a/packages/experimental/Expander/src/Expander.tsx b/packages/experimental/Expander/src/Expander.tsx index cc03e968d23..46737c88388 100644 --- a/packages/experimental/Expander/src/Expander.tsx +++ b/packages/experimental/Expander/src/Expander.tsx @@ -8,7 +8,8 @@ import * as React from 'react'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps, buildProps } from '@fluentui-react-native/framework'; +import { compose, buildProps } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import type { ExpanderType, ExpanderProps, ExpanderViewProps } from './Expander.types'; import { expanderName } from './Expander.types'; diff --git a/packages/experimental/Overflow/package.json b/packages/experimental/Overflow/package.json index de665abbf64..11155486c80 100644 --- a/packages/experimental/Overflow/package.json +++ b/packages/experimental/Overflow/package.json @@ -31,7 +31,6 @@ "update-snapshots": "fluentui-scripts jest -u" }, "dependencies": { - "@fluentui-react-native/framework": "workspace:*", "@fluentui-react-native/framework-base": "workspace:*" }, "devDependencies": { diff --git a/packages/experimental/Overflow/src/Overflow/useOverflow.ts b/packages/experimental/Overflow/src/Overflow/useOverflow.ts index cb6a39cfb31..8fb5db4c313 100644 --- a/packages/experimental/Overflow/src/Overflow/useOverflow.ts +++ b/packages/experimental/Overflow/src/Overflow/useOverflow.ts @@ -17,6 +17,7 @@ import type { OverflowManager, OverflowUpdatePayload, } from '../overflowManager.types'; +import { mergeStyles } from '@fluentui-react-native/framework-base'; type PartialOverflowState = Pick; @@ -181,15 +182,13 @@ export function useOverflow(props: OverflowProps): OverflowInfo { }, [initialLayoutDone, onReady]); const overflowStyles = React.useMemo>( - () => [ - style, - { + () => + mergeStyles(style, { display: 'flex', flexDirection: 'row', opacity: dontHideBeforeReady || initialLayoutDone ? 1 : 0, padding: padding, - }, - ], + }), [dontHideBeforeReady, initialLayoutDone, padding, style], ); @@ -197,11 +196,11 @@ export function useOverflow(props: OverflowProps): OverflowInfo { state: { ...overflowState, containerSize, - dontHideBeforeReady: dontHideBeforeReady, + dontHideBeforeReady, initialOverflowLayoutDone: initialLayoutDone, - overflowMenuRef: overflowMenuRef, - register: register, - disconnect: disconnect, + overflowMenuRef, + register, + disconnect, setLayoutState: userSetLayoutState, updateOverflow: overflowManager.update, updateItem: handleItemUpdate, diff --git a/packages/experimental/Overflow/src/OverflowItem/useOverflowItem.ts b/packages/experimental/Overflow/src/OverflowItem/useOverflowItem.ts index 7dd15ae31bb..99aaea94d5f 100644 --- a/packages/experimental/Overflow/src/OverflowItem/useOverflowItem.ts +++ b/packages/experimental/Overflow/src/OverflowItem/useOverflowItem.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import type { ViewStyle, LayoutChangeEvent } from 'react-native'; -import { mergeStyles } from '@fluentui-react-native/framework'; +import { mergeStyles } from '@fluentui-react-native/framework-base'; import type { OverflowItemInfo, OverflowItemProps } from './OverflowItem.types'; import type { LayoutSize, OverflowItemChangePayload } from '../Overflow/Overflow.types'; diff --git a/packages/experimental/Overflow/tsconfig.json b/packages/experimental/Overflow/tsconfig.json index b45be32a9fe..bc17ad196cf 100644 --- a/packages/experimental/Overflow/tsconfig.json +++ b/packages/experimental/Overflow/tsconfig.json @@ -14,9 +14,6 @@ { "path": "../../framework-base/tsconfig.json" }, - { - "path": "../../framework/framework/tsconfig.json" - }, { "path": "../../components/Menu/tsconfig.json" }, diff --git a/packages/experimental/Shadow/src/__tests__/Shadow.test.tsx b/packages/experimental/Shadow/src/__tests__/Shadow.test.tsx index 7c7ee1820cd..c8afe7c3327 100644 --- a/packages/experimental/Shadow/src/__tests__/Shadow.test.tsx +++ b/packages/experimental/Shadow/src/__tests__/Shadow.test.tsx @@ -2,7 +2,8 @@ import * as React from 'react'; import { act } from 'react'; import { Text, View, type ViewStyle } from 'react-native'; -import { mergeStyles, useFluentTheme } from '@fluentui-react-native/framework'; +import { useFluentTheme } from '@fluentui-react-native/framework'; +import { mergeStyles } from '@fluentui-react-native/framework-base'; import { Pressable } from '@fluentui-react-native/pressable'; import * as renderer from 'react-test-renderer'; diff --git a/packages/experimental/Shadow/src/shadowStyle.ts b/packages/experimental/Shadow/src/shadowStyle.ts index 03513ce91c0..a509f1b5059 100644 --- a/packages/experimental/Shadow/src/shadowStyle.ts +++ b/packages/experimental/Shadow/src/shadowStyle.ts @@ -1,7 +1,7 @@ import type { ColorValue } from 'react-native'; import { Platform } from 'react-native'; -import { memoize } from '@fluentui-react-native/framework'; +import { memoize } from '@fluentui-react-native/framework-base'; import type { ShadowToken } from '@fluentui-react-native/theme-types'; /** diff --git a/packages/experimental/Shimmer/src/Shimmer.tsx b/packages/experimental/Shimmer/src/Shimmer.tsx index 38b75da3410..395b4e67123 100644 --- a/packages/experimental/Shimmer/src/Shimmer.tsx +++ b/packages/experimental/Shimmer/src/Shimmer.tsx @@ -4,12 +4,13 @@ import type { ScaleXTransform, TranslateXTransform } from 'react-native'; import { Animated, I18nManager } from 'react-native'; import type { UseSlots } from '@fluentui-react-native/framework'; -import { compose, mergeProps, buildUseStyling } from '@fluentui-react-native/framework'; +import { compose, buildUseStyling } from '@fluentui-react-native/framework'; +import { mergeProps } from '@fluentui-react-native/framework-base'; import assertNever from 'assert-never'; import { Circle, ClipPath, Defs, LinearGradient, Rect, Stop, Svg, G } from 'react-native-svg'; import { stylingSettings } from './Shimmer.styling'; -import type { ShimmerProps, ShimmerType } from './Shimmer.types'; +import type { ShimmerElementTypes, ShimmerProps, ShimmerType } from './Shimmer.types'; import { shimmerName } from './Shimmer.types'; const useStyling = buildUseStyling(stylingSettings); @@ -71,7 +72,7 @@ export const Shimmer = compose({ if (elements) { for (let i = 0; i < elements.length; i++) { - const element = elements[i]; + const element: ShimmerElementTypes = elements[i]; if (element.type == 'rect') { rows.push( ` - 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 +- `PhasedRender` - The render signature for phased components (phase one returns the phase two renderer) +- `SlotComponent` - Slot component type used in the composition framework +- `PropsTransform` - A transform that maps one set of props to another ## JSX Runtime -This package exports a custom JSX runtime at `@fluentui-react-native/framework-base/jsx-runtime`. Use it in your component files with: +This package exports a custom JSX runtime at `@fluentui-react-native/framework-base/jsx-runtime`. Use it in your component files by adding this pragma as the **first line** of the file: ```tsx /** @jsxImportSource @fluentui-react-native/framework-base */ ``` -The custom runtime enables automatic element flattening for direct and phased components. +The custom runtime enables automatic element flattening for direct and phased components. Any package using this pragma must include `@fluentui-react-native/framework-base` in its `devDependencies`. + +> **Note:** This is the current pattern and replaces the legacy `/** @jsx withSlots */` directive (which required importing `withSlots` explicitly). The exported `withSlots` helper remains available only for the classic runtime used by deprecated framework code. diff --git a/packages/framework-base/package.json b/packages/framework-base/package.json index bee6dfb0dae..558f9e587c7 100644 --- a/packages/framework-base/package.json +++ b/packages/framework-base/package.json @@ -42,11 +42,18 @@ "devDependencies": { "@babel/core": "catalog:", "@fluentui-react-native/scripts": "workspace:*", + "@react-native-community/cli": "^20.0.0", + "@react-native-community/cli-platform-android": "^20.0.0", + "@react-native-community/cli-platform-ios": "^20.0.0", + "@react-native/babel-preset": "^0.81.0", + "@react-native/metro-config": "^0.81.0", "@types/jest": "^29.0.0", "@types/node": "catalog:", "@types/react": "~19.1.4", + "@types/react-test-renderer": "^19.1.0", "react": "19.1.4", - "react-native": "^0.81.6" + "react-native": "^0.81.6", + "react-test-renderer": "19.1.4" }, "peerDependencies": { "@types/react": "~18.2.0 || ~19.0.0 || ~19.1.4", @@ -58,15 +65,18 @@ } }, "furn": { - "jestPlatform": "react" + "jestPlatform": "ios" }, "rnx-kit": { "extends": "@fluentui-react-native/scripts/kit-config", "kitType": "library", "alignDeps": { "capabilities": [ + "core-android-dev-only", "core-dev-only", - "react" + "core-ios-dev-only", + "react", + "react-test-renderer" ] } } diff --git a/packages/framework/use-slot/src/__snapshots__/useSlot.test.tsx.snap b/packages/framework-base/src/component-patterns/__snapshots__/useSlot.test.tsx.snap similarity index 67% rename from packages/framework/use-slot/src/__snapshots__/useSlot.test.tsx.snap rename to packages/framework-base/src/component-patterns/__snapshots__/useSlot.test.tsx.snap index d5b401d6f97..66720f9c52d 100644 --- a/packages/framework/use-slot/src/__snapshots__/useSlot.test.tsx.snap +++ b/packages/framework-base/src/component-patterns/__snapshots__/useSlot.test.tsx.snap @@ -1,5 +1,36 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`slot intrinsic attribute acceptance accepts the key attribute on a slot created from a class component in a keyed list 1`] = ` + + + class + + + class + + + class + + +`; + +exports[`slot intrinsic attribute acceptance accepts the key attribute on slots created from any supported component type 1`] = ` + + + + + fn + + + class + + + + +`; + exports[`useSlot tests Header and caption text render as expected 1`] = ` (component: FunctionComponent): DirectComponent { + (component as DirectComponent)[SLOT_RENDER_TYPE_KEY] = 'callable'; + return component as DirectComponent; +} + +/** + * Checks if a component is a direct component. + * @param component the component to check + * @return true if the component is a direct component, false otherwise + */ +export function isDirectComponent( + component: FunctionComponent | LegacyFunctionComponent | React.ElementType, +): component is DirectComponent { + return (component as DirectComponent)[SLOT_RENDER_TYPE_KEY] === 'callable'; +} + +/** + * Creates a legacy direct component from a legacy function component. + * @param component the legacy function component to convert + * @return the same component with the legacy direct component flag set + * @deprecated Prefer the directComponent or slot patterns if writing new code. + */ +export function legacyDirectComponent(component: LegacyFunctionComponent): LegacyDirectComponent { + (component as LegacyDirectComponent)[SLOT_RENDER_TYPE_KEY] = 'legacy'; + return component as LegacyDirectComponent; +} + +/** + * Checks if a component is a legacy direct component + * @param component the component to check + * @return true if the component is a legacy direct component, false otherwise + * @deprecated Prefer the directComponent or slot patterns if writing new code. + */ +export function isLegacyDirectComponent( + component: FunctionComponent | LegacyFunctionComponent | React.ElementType, +): component is LegacyDirectComponent { + return (component as LegacyDirectComponent)[SLOT_RENDER_TYPE_KEY] === 'legacy'; +} + +/** + * Checks if a component is a direct component or a legacy direct component. + * @param component the component to check + * @return true if the component is a direct component or a legacy direct component, false otherwise + * @internal + */ +export function isDirectComponentType( + component: FunctionComponent | LegacyFunctionComponent | React.ElementType, +): component is DirectComponent | LegacyDirectComponent { + return isDirectComponent(component) || isLegacyDirectComponent(component); +} + +/** + * Renders a direct component or a legacy direct component. + * @param component the component to render + * @param props the props to pass to the component + * @return the result of the component + * @throws {Error} if the component is not a direct component or a legacy direct component, use isDirectComponentType to check + * @internal + */ +export function renderDirectComponent( + component: DirectComponent | LegacyDirectComponent, + props: TProps, +): ReturnType { + if (isDirectComponent(component)) { + return component(props); + } else if (isLegacyDirectComponent(component)) { + const [rest, childrenProps] = splitPropsAndChildren(props); + if (childrenProps?.children && Array.isArray(childrenProps.children)) { + return component(rest as TProps, ...childrenProps.children); + } else { + return component(rest as TProps, childrenProps?.children); + } + } + throw new Error('Invalid direct component'); +} diff --git a/packages/framework-base/src/component-patterns/directComponent.ts b/packages/framework-base/src/component-patterns/directComponent.ts deleted file mode 100644 index 32dfcc5e3fb..00000000000 --- a/packages/framework-base/src/component-patterns/directComponent.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { FunctionComponent } from './render.types.ts'; - -/** - * @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/identify.ts b/packages/framework-base/src/component-patterns/identify.ts new file mode 100644 index 00000000000..5a925f395b9 --- /dev/null +++ b/packages/framework-base/src/component-patterns/identify.ts @@ -0,0 +1,74 @@ +import { SLOT_COMPONENT_KEY, SLOT_RENDER_TYPE_KEY } from '../const'; +import type { + DirectComponent, + FunctionComponent, + LegacyDirectComponent, + LegacyFunctionComponent, + PhasedComponent, + SlotComponent, + StagedComponent, +} from '../types/render.types'; + +/** + * Checks if a component is a direct component. + * @param component the component to check + * @return true if the component is a direct component, false otherwise + */ +export function isDirectComponent( + component: FunctionComponent | LegacyFunctionComponent | React.ElementType, +): component is DirectComponent { + return (component as DirectComponent)[SLOT_RENDER_TYPE_KEY] === 'callable'; +} + +/** + * Checks if a component is a legacy direct component + * @param component the component to check + * @return true if the component is a legacy direct component, false otherwise + * @deprecated Prefer the directComponent or slot patterns if writing new code. + */ +export function isLegacyDirectComponent( + component: FunctionComponent | LegacyFunctionComponent | React.ElementType, +): component is LegacyDirectComponent { + return (component as LegacyDirectComponent)[SLOT_RENDER_TYPE_KEY] === 'legacy'; +} + +/** + * Checks if a component is a direct component or a legacy direct component. + * @param component the component to check + * @return true if the component is a direct component or a legacy direct component, false otherwise + * @internal + */ +export function isDirectComponentType( + component: FunctionComponent | LegacyFunctionComponent | React.ElementType, +): component is DirectComponent | LegacyDirectComponent { + return isDirectComponent(component) || isLegacyDirectComponent(component); +} + +/** + * Determine if the component is a phased component + */ +export function isPhasedComponent(component: unknown): component is PhasedComponent { + return component != null && (component as PhasedComponent)[SLOT_RENDER_TYPE_KEY] === 'phased'; +} + +/** + * Determine if the component is a staged component, the legacy phased pattern + */ +export function isStagedComponent(component: unknown): component is StagedComponent { + return component != null && (component as StagedComponent)[SLOT_RENDER_TYPE_KEY] === 'phased-legacy'; +} + +/** + * Check if a component is a slot component. Note that it explicitly rules out phased/staged as that uses the same + * key for the component function but with a different signature. + * @param component - The component to check. + * @returns True if the component is a slot component, false otherwise. + */ +export function isSlotComponent(component: unknown): component is SlotComponent { + return ( + component != null && + (component as SlotComponent)[SLOT_COMPONENT_KEY] != null && + !isPhasedComponent(component) && + !isStagedComponent(component) + ); +} diff --git a/packages/framework-base/src/component-patterns/phased.ts b/packages/framework-base/src/component-patterns/phased.ts new file mode 100644 index 00000000000..0417b4a3ddb --- /dev/null +++ b/packages/framework-base/src/component-patterns/phased.ts @@ -0,0 +1,83 @@ +import React from 'react'; +import type { PhasedComponent, StagedComponent, LegacyFunctionComponent } from '../types/render.types'; +import { renderForJsxRuntime } from './render'; +import { SLOT_COMPONENT_KEY, SLOT_RENDER_TYPE_KEY } from '../const'; +import { splitPropsAndChildren } from '../utilities/typeUtils'; + +/** + * PHASED RENDERING (formerly called "staged" or "two-stage" rendering) + * + * The 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 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 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. + */ + +/** + * 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: (props: TProps) => React.ComponentType): PhasedComponent { + return Object.assign( + (props: TProps) => { + // pull out children from props + const { children, ...outerProps } = props as React.PropsWithChildren; + const Inner = getInnerPhase(outerProps as TProps); + return renderForJsxRuntime(Inner, { children }); + }, + { + [SLOT_COMPONENT_KEY]: getInnerPhase, + [SLOT_RENDER_TYPE_KEY]: 'phased', + }, + ) as PhasedComponent; +} + +/** + * Determine if the component is a phased component + */ +export function isPhasedComponent(component: unknown): component is PhasedComponent { + return component != null && (component as PhasedComponent)[SLOT_RENDER_TYPE_KEY] === 'phased'; +} + +/** + * 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: (props: Partial) => LegacyFunctionComponent, + memo?: boolean, +): StagedComponent { + // component wrapper that will render in the case that this component is not + // used as a slot + const component = (props: TProps) => { + const [rest, childrenProp] = splitPropsAndChildren(props); + const final = staged(rest as TProps); + return Array.isArray(childrenProp?.children) + ? final({} as TProps, ...childrenProp.children) + : final({} as TProps, childrenProp?.children); + }; + + // memoize the component if requested and attach the required information to make this a staged component + const result = memo ? React.memo(component) : component; + return Object.assign(result, { + [SLOT_COMPONENT_KEY]: staged, + [SLOT_RENDER_TYPE_KEY]: 'phased-legacy', + }) as StagedComponent; +} + +/** + * Determine if the component is a staged component, the legacy phased pattern + */ +export function isStagedComponent(component: unknown): component is StagedComponent { + return component != null && (component as StagedComponent)[SLOT_RENDER_TYPE_KEY] === 'phased-legacy'; +} diff --git a/packages/framework-base/src/component-patterns/phasedComponent.ts b/packages/framework-base/src/component-patterns/phasedComponent.ts deleted file mode 100644 index 8614211e5d9..00000000000 --- a/packages/framework-base/src/component-patterns/phasedComponent.ts +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import type { ComposableFunction, PhasedComponent, PhasedRender, FunctionComponent } from './render.types.ts'; -import { renderForJsxRuntime } from './render.ts'; -import type { LegacyDirectComponent } from './render.types.ts'; - -/** - * 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 { - // for backward compatibility check for staged render and return a wrapper that maps the signature - const staged = (component as ComposableFunction)._staged; - if (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: TProps) => { - // pull out children from props - const { children, ...outerProps } = props as React.PropsWithChildren; - 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 ceef1ad405e..2b7340a7c93 100644 --- a/packages/framework-base/src/component-patterns/render.ts +++ b/packages/framework-base/src/component-patterns/render.ts @@ -1,62 +1,95 @@ import React from 'react'; import * as ReactJSX from 'react/jsx-runtime'; -import type { RenderType, RenderResult, DirectComponent, LegacyDirectComponent } from './render.types.ts'; -import { extractChildren, splitPropsAndChildren } from '../utilities/typeUtils.ts'; +import type { RenderType, RenderResult, SlotComponent, PropsTransform } from '../types/render.types'; +import { renderDirectComponent } from './direct'; +import { isDirectComponentType, isSlotComponent } from './identify'; +import { prepareSlotProps, setSlotStatics } from './slot'; +import { SLOT_COMPONENT_KEY } from '../const'; +import { getPropsChildren, setPropsChildren } from '../utilities/typeUtils'; export type CustomRender = () => RenderResult; -export function asDirectComponent(type: RenderType): DirectComponent | undefined { - if (typeof type === 'function' && (type as DirectComponent)._callDirect) { - return type as DirectComponent; - } - return undefined; -} - -function asLegacyDirectComponent(type: RenderType): LegacyDirectComponent | undefined { - if (typeof type === 'function' && (type as LegacyDirectComponent)._canCompose) { - return type as LegacyDirectComponent; - } - return undefined; -} - +/** + * Root jsx render function, used for both jsx and jsxs calls. This handles all of our custom rendering patterns + * and slot components. + * @param type the component type to render + * @param props the props for the component + * @param key optional key for the component, this is parallel to props and injected at the framework level + * @param jsxFn optional jsx function to use for rendering, set when called by _jsx, _jsxs but will auto-detect for non-framework callers + */ export function renderForJsxRuntime( type: React.ElementType, props: TProps, key?: React.Key, jsxFn?: typeof ReactJSX.jsx, ): RenderResult { - const legacyDirect = asLegacyDirectComponent(type); - if (legacyDirect) { - const [rest, children] = splitPropsAndChildren(props); - const newProps = { ...rest, key }; - return legacyDirect(newProps, ...React.Children.toArray(children)) as RenderResult; + // If the type is a direct component type, render it directly + if (isDirectComponentType(type)) { + const jsxResult = renderDirectComponent(type, props); + // If a key is provided, clone the element with the key + return key != null ? React.cloneElement(jsxResult, { key }) : jsxResult; } - const directComponent = asDirectComponent(type); - if (directComponent) { - const newProps = { ...props, key }; - return directComponent(newProps); + + // with a slot component use the internal type and props to render directly + if (isSlotComponent(type)) { + const slotType = type[SLOT_COMPONENT_KEY]; + const slotProps = prepareSlotProps(type, props); + // now re-enter with the inner type to handle direct/etc + return renderForJsxRuntime(slotType, slotProps, key, jsxFn); } // 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(extractChildren(props)) > 1) { + if (React.Children.count(getPropsChildren(props)) > 1) { jsxFn = ReactJSX.jsxs; } else { jsxFn = ReactJSX.jsx; } } - // Extract key from props to avoid React 19 warning about spreading key prop - const { key: propsKey, ...propsWithoutKey } = props as any; - // Use explicitly passed key, or fall back to key from props - const finalKey = key ?? propsKey; // now call the appropriate jsx function to render the component - return jsxFn(type, propsWithoutKey, finalKey); + return jsxFn(type, props, key); +} + +/** + * Public signature to render a component as appropriate with our internal runtime. + * @param type the component type to render + * @param props the props for the component + * @return the rendered result, either a React element or a custom render result + */ +export function renderJsx(type: React.ElementType, props: TProps): RenderResult { + return renderForJsxRuntime(type, props); +} + +/** + * Creates a slot component with the given base component, props, and options. Implemented here as it needs to call + * the renderForJsxFunction directly. + * @param component inner component type + * @param props props targeting that component + * @param transform optional transform function for the slot + * @return a slot component + */ +export function createSlotComponent( + component: React.ComponentType, + props: Partial, + transform?: PropsTransform, +): SlotComponent { + const slot: SlotComponent = Object.assign( + (props: TProps) => { + props = prepareSlotProps(slot, props); + return renderForJsxRuntime(component, props); + }, + setSlotStatics({}, component, props, transform), + ); + return slot; } -export function renderForClassicRuntime(type: RenderType, props: TProps, ...children: React.ReactNode[]): RenderResult { - // if it is a non-string type with _canCompose set just call the function directly, otherwise call createElement as normal - const propsWithChildren = { children, ...props }; +/** + * Render signature matching the old createElement pattern from the pre-jsx runtime. Will call through to the new runtime. + */ +export function renderForClassicRuntime(type: RenderType, props: TProps, children: React.ReactNode[]): RenderResult { + // route this through to the new runtime + const propsWithChildren = setPropsChildren({ ...props }, children); return renderForJsxRuntime(type as React.ElementType, propsWithChildren); } diff --git a/packages/framework-base/src/component-patterns/render.types.ts b/packages/framework-base/src/component-patterns/render.types.ts deleted file mode 100644 index 8079be58e66..00000000000 --- a/packages/framework-base/src/component-patterns/render.types.ts +++ /dev/null @@ -1,139 +0,0 @@ -import type React from 'react'; -//import type ReactJSX from 'react/jsx-runtime'; -import type { FurnJSX } from '../jsx-namespace.ts'; - -/** - * Base types for rendering components in a react application, extracted from react types - */ -export type RenderResult = FurnJSX.Element; -//export type RenderResult = ReturnType; -export type RenderType = FurnJSX.ElementType; -//export type RenderType = Parameters[0] | string; - -/** - * The standard element type inputs for react and react-native. This might be View or Button, or it might be 'div' in web. Effectively - * it is what react accepts for React.createElement - */ -export type NativeReactType = RenderType; - -/** - * Get the props from a react component type - */ -export type PropsOf = TComponent extends React.JSXElementConstructor ? P : never; - -/** - * DIRECT RENDERING - * - * This is a pattern where a function can by called directly to render a component, bypassing creating additional layers of the - * rendering tree. This is useful for higher order components that mainly need to do simple prop manipulation but want to - * compartmentalize the logic. - * - * Note that for this to be safe, hooks cannot be used in the function. This is the reason why function component is redefined, - * to help linting tools catch bad usage. - * - * The newer DirectComponent type should be used, as it will handle children consistently. - */ - -/** - * type of the render function, not a FunctionComponent to help prevent hook usage - */ -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 = FunctionComponentCore & { - displayName?: string; - _callDirect?: boolean; -}; - -type LegacyComponentFunction = (props: TProps, ...children: React.ReactNode[]) => RenderResult; - -/** - * Legacy slot function type, this allows the rendering handlers to bypass the normal JSX rendering and call the function - * directly. This expects the function to have children as the last argument of the call which isn't consistent with standard - * react usage, where children are passed as a prop. If writing new components use the DirectComponent type instead. - * @deprecated use DirectComponent instead - */ -export type LegacyDirectComponent = LegacyComponentFunction & { - _canCompose?: boolean; -}; - -/** - * Slot function type used in the composition framework. Slot functions return React elements (not arbitrary ReactNode values) - * since they always either call staged render functions or React.createElement. - */ -export type SlotFn = { - (props: TProps, ...children: React.ReactNode[]): React.ReactElement | null; - _canCompose?: boolean; -}; - -/** - * 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 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 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 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. - */ - -/** - * Phased render function signature. This is the recommended pattern for components that need hooks. - * - * 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 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 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[]) => FurnJSX.Element | null; - -/** - * 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; - -/** - * 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 }; - -/** - * A type aggregating all the custom types that can be used in the render process. - * @internal only used in this package, should not be exported - */ -export type AnyCustomType = - | React.FunctionComponent - | DirectComponent - | PhasedComponent - | ComposableFunction - | LegacyDirectComponent; diff --git a/packages/framework-base/src/component-patterns/slot.ts b/packages/framework-base/src/component-patterns/slot.ts new file mode 100644 index 00000000000..4a20f0f4f0f --- /dev/null +++ b/packages/framework-base/src/component-patterns/slot.ts @@ -0,0 +1,46 @@ +import type React from 'react'; +import type { PropsTransform, SlotComponent } from '../types/render.types'; +import { SLOT_COMPONENT_KEY, SLOT_PROPS_KEY, SLOT_PROP_TRANSFORM_KEY } from '../const'; +import { mergeProps } from '../merge-props/mergeProps'; + +/** + * Convenience type, just referencing the statics of the component + */ +export type SlotComponentStatics = Pick< + SlotComponent, + typeof SLOT_COMPONENT_KEY | typeof SLOT_PROPS_KEY | typeof SLOT_PROP_TRANSFORM_KEY | 'displayName' +>; + +/** + * Set the slot statics for a new component, or to re-use an existing component + * @param statics - The statics object to update. + * @param component - The component to set as the slot component. + * @param props - The base props for the slot component. + * @param options - Optional settings for creating the slot. + */ +export function setSlotStatics( + statics: Partial>, + component: React.ComponentType, + props: Partial, + transform?: PropsTransform, +): SlotComponentStatics { + statics[SLOT_COMPONENT_KEY] = component; + statics[SLOT_PROPS_KEY] = props; + statics[SLOT_PROP_TRANSFORM_KEY] = transform; + if (component.displayName) { + statics.displayName = component.displayName; + } + return statics as SlotComponentStatics; +} + +/** + * Given a set of userProps, prepare the final props for a slot component + * - merging with base props + * - running the transform if present + */ +export function prepareSlotProps(slotInfo: SlotComponentStatics, userProps?: TProps): TProps { + const baseProps = slotInfo[SLOT_PROPS_KEY]; + const transform = slotInfo[SLOT_PROP_TRANSFORM_KEY]; + const mergedProps = mergeProps(baseProps, userProps) ?? ({} as TProps); + return transform ? transform(mergedProps) : mergedProps; +} diff --git a/packages/framework-base/src/component-patterns/stagedComponent.ts b/packages/framework-base/src/component-patterns/stagedComponent.ts deleted file mode 100644 index 7761546c958..00000000000 --- a/packages/framework-base/src/component-patterns/stagedComponent.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react'; - -import type { StagedRender, ComposableFunction } from './render.types.ts'; - -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/use-slot/src/useSlot.test.tsx b/packages/framework-base/src/component-patterns/useSlot.test.tsx similarity index 51% rename from packages/framework/use-slot/src/useSlot.test.tsx rename to packages/framework-base/src/component-patterns/useSlot.test.tsx index 49615739ef2..9a5ac51ac3b 100644 --- a/packages/framework/use-slot/src/useSlot.test.tsx +++ b/packages/framework-base/src/component-patterns/useSlot.test.tsx @@ -3,12 +3,15 @@ import * as React from 'react'; import type { TextProps, TextStyle } from 'react-native'; import { Text, View } from 'react-native'; -import { type FunctionComponent, mergeStyles } from '@fluentui-react-native/framework-base'; +import type { FunctionComponent } from '../types/render.types'; +import { mergeStyles } from '../merge-props/mergeStyles'; import * as renderer from 'react-test-renderer'; import { act } from 'react'; -import { phasedComponent, directComponent } from '@fluentui-react-native/framework-base'; -import { useSlot } from './useSlot.ts'; +import { phasedComponent, stagedComponent } from './phased'; +import { directComponent } from './direct'; +import { useSlot } from './useSlot'; +import type { FurnJSX } from '../types/react.types'; type PluggableTextProps = TextProps & { inner?: FunctionComponent }; @@ -70,7 +73,7 @@ const HeaderCaptionText1 = (props: TextProps) => { const { children, ...rest } = props; const baseStyle = React.useMemo(() => ({ fontSize: 24, fontWeight: 'bold' }), []); const mergedProps = { ...rest, style: mergeStyles(baseStyle, props.style) }; - const InnerText = useSlot(CaptionText, mergedProps); + const InnerText = useSlot(CaptionText, mergedProps); return {children}; }; @@ -125,3 +128,99 @@ describe('useSlot tests', () => { expect(tree1!['HeaderCaptionText1']).toEqual(tree2!['HeaderCaptionText2']); }); }); + +/** + * INTRINSIC ATTRIBUTE ACCEPTANCE + * + * A slot produced by useSlot must behave like a real component in the JSX tree, which means it has to + * accept React's intrinsic attributes (most importantly `key`) regardless of the kind of component the + * slot was created from. These tests are as much a compile-time contract as a runtime one: the file is + * type-checked by the check pass (targets/tsconfig.check.json includes *.test.* files), so if a slot type + * ever stopped accepting `key` the `` usages below would fail the build. The runtime + * renders confirm the slots still produce output when an intrinsic attribute is supplied. + */ + +type AcceptProps = { title?: string; children?: React.ReactNode }; + +// A representative sampling of the component kinds useSlot supports as an input type. +const FnAccept: React.FunctionComponent = (props) => {props.title}; + +/** + * A class component shaped like the ones exported by react-native-svg (e.g. Rect/Circle): it extends + * React.Component, declares static defaultProps, and its render() is typed to return React.JSX.Element. + * Class components receive `key`/`ref` through JSX.IntrinsicClassAttributes rather than + * JSX.IntrinsicAttributes, so they are worth covering explicitly to prove a slot built from a class still + * accepts intrinsic attributes. + */ +class ClassAccept extends React.Component { + public static defaultProps: Partial = { title: '' }; + public override render(): FurnJSX.Element { + return {this.props.title}; + } +} + +const PhasedAccept = phasedComponent((_props: AcceptProps) => directComponent((extra: AcceptProps) => )); + +const StagedAccept = stagedComponent((_props) => (rest, ...children) => {children}); + +/** + * Builds a slot for every supported component kind and renders each one with a `key` intrinsic attribute. + * The mere fact that this component type-checks is the core of the acceptance test; rendering it verifies + * the slots remain usable at runtime. + */ +const IntrinsicAttributeConsumer: React.FunctionComponent = () => { + const HostSlot = useSlot(View, {}); + const TextSlot = useSlot(Text, { title: 'text' } as TextProps); + const FnSlot = useSlot(FnAccept, { title: 'fn' }); + const ClassSlot = useSlot(ClassAccept, { title: 'class' }); + const PhasedSlot = useSlot(PhasedAccept, { title: 'phased' }); + const StagedSlot = useSlot(StagedAccept, { title: 'staged' }); + + return ( + + + + + + + + + ); +}; + +/** + * Focused on class components specifically. A raw class component in a consumer package can lose `key` + * under a custom jsxImportSource because class attributes flow through JSX.IntrinsicClassAttributes; + * routing the class through useSlot normalizes it to a SlotComponent (a function type) which reliably + * accepts `key`. This mirrors how react-native-svg's class-based elements (Rect/Circle) should be + * consumed via slots. + */ +const ClassSlotConsumer: React.FunctionComponent = () => { + const ClassSlot = useSlot(ClassAccept, { title: 'class' }); + return ( + + {['a', 'b', 'c'].map((k) => ( + + ))} + + ); +}; + +describe('slot intrinsic attribute acceptance', () => { + it('accepts the key attribute on slots created from any supported component type', () => { + let component: renderer.ReactTestRenderer; + act(() => { + component = renderer.create(); + }); + // If any slot rejected `key`, the file would not have compiled; this confirms the tree still renders. + expect(component!.toJSON()).toMatchSnapshot(); + }); + + it('accepts the key attribute on a slot created from a class component in a keyed list', () => { + let component: renderer.ReactTestRenderer; + act(() => { + component = renderer.create(); + }); + expect(component!.toJSON()).toMatchSnapshot(); + }); +}); diff --git a/packages/framework-base/src/component-patterns/useSlot.ts b/packages/framework-base/src/component-patterns/useSlot.ts new file mode 100644 index 00000000000..0817c9957c9 --- /dev/null +++ b/packages/framework-base/src/component-patterns/useSlot.ts @@ -0,0 +1,97 @@ +import * as React from 'react'; + +import type { SlotComponent, UseSlot, UseOptionalSlot, PropsTransform } from '../types/render.types'; +import { setSlotStatics } from './slot'; +import { createSlotComponent } from './render'; +import { isPhasedComponent, isStagedComponent, isLegacyDirectComponent } from './identify'; +import { SLOT_COMPONENT_KEY } from '../const'; +import { legacyDirectComponent } from './direct'; +import { splitPropsAndChildren } from '../utilities/typeUtils'; + +/** + * The core useSlot hook implementation, while the return result will always be a SlotComponent, the implementation will fork + * based on whether the component implements special rendering patterns. + * + * - component: Standard component (View, Text, function, class, etc.) + * This will create a slot, with the options and initial props configured, with the type as the base type. + * On render, any props added via jsx will be merged with the initial props, and our internal jsx runtime will render it + * directly. In essence if this slot is of type View that is what will appear in the render tree. + * + * - component: Direct component (created via one of our internal patterns) + * This will do all the work mentioned above, but when rendered via our internal runtime, it will call the function + * directly. E.g.: if component is a custom direct component called MyWrapper that itself contains a View, MyWrapper will + * be omitted from the render tree and only the View will be rendered. + * + * - component: Phased/Staged component + * In this case, the component implements a phased render pattern and since this function is itself a hook, we can deterministically call the + * inner hook component to get to the inner element. This effectively extends the direct component patterns to work with hooks as + * well. Allowing things like picking up context for theming without having to create unnecessary wrapper layers. + * + * @param component - any kind of component that can be rendered as part of the tree + * @param hookProps - props, either full or partial that should be embedded in the component + * @param transform - an optional transform function for filtering props or doing other last minute transitions + */ +export const useSlot: UseSlot = ( + component: React.ComponentType, + hookProps: Partial = {}, + transform?: PropsTransform, +): SlotComponent => { + // handle the component being a phased/staged render + if (isPhasedComponent(component)) { + // phased components can pass through children given that it is carried along with the props + component = component[SLOT_COMPONENT_KEY](hookProps); + hookProps = {}; + } else if (isStagedComponent(component)) { + // the first stage consumes the incoming props (captured via closure), so only children should be carried + // forward to the second stage. Resetting hookProps here prevents the already-consumed props from being + // re-applied to the inner component at render time (which would leak props like 'variant' onto the slot). + const [props, childrenProp] = splitPropsAndChildren(hookProps); + // call the first stage and get the inner component, which will be a LegacyFunctionComponent + const inner = component[SLOT_COMPONENT_KEY](props as Partial); + // attach the type signifier if necessary as legacy consumers aren't reliable about this + component = isLegacyDirectComponent(inner) ? inner : legacyDirectComponent(inner); + // carry only children forward (force cast, if it has children we know it is in the TProps type) + hookProps = (childrenProp ?? {}) as Partial; + } + // now onto the slot creation itself, use a ref to get per-instance storage for the slot + const slotRef = React.useRef | null>(null); + if (slotRef.current == null) { + slotRef.current = createSlotComponent(component, hookProps, transform); + } else { + // update the existing slot with new props and transform if necessary + setSlotStatics(slotRef.current, component, hookProps, transform); + } + return slotRef.current; +}; + +/** + * The optional slot pattern effectively handles having a null or undefined component type passed in + * and handles that changing at runtime. + * + * To not violate the rule of hooks this does not resolve staged/phased components, instead using their standard entry + * point for when they aren't resolved early. + * + * @param component - react component type which may or may not be specified + * @param hookProps - props to be passed to the component + * @param transform - an optional transform function for filtering props or doing other last minute transitions + * @returns The slot component if the component is defined, otherwise null + */ +export const useOptionalSlot: UseOptionalSlot = ( + component: React.ComponentType | undefined | null, + hookProps: Partial = {}, + transform?: PropsTransform, +): SlotComponent | null => { + // just create the hook itself + const slotRef = React.useRef | null>(null); + if (component != null) { + if (slotRef.current == null) { + slotRef.current = createSlotComponent(component, hookProps, transform); + } else { + // update the existing slot with new props and transform if necessary + setSlotStatics(slotRef.current, component, hookProps, transform); + } + } else { + slotRef.current = null; + } + return slotRef.current; +}; diff --git a/packages/framework-base/src/component-patterns/withSlots.tsx b/packages/framework-base/src/component-patterns/withSlots.tsx index 2f053a6a2d2..56f7b8ce677 100644 --- a/packages/framework-base/src/component-patterns/withSlots.tsx +++ b/packages/framework-base/src/component-patterns/withSlots.tsx @@ -1,21 +1,23 @@ import type React from 'react'; -import { renderForClassicRuntime } from './render.ts'; +import { renderForClassicRuntime } from './render'; /** - * This function is required for any module that uses slots. + * Legacy JSX handler for the classic runtime. New code should NOT use this. + * + * Prefer the custom automatic JSX runtime instead by adding the jsxImportSource pragma pointing at + * "@fluentui-react-native/framework-base" as the first line of a file. That runtime resolves slots + * automatically without importing any helper. See the package README for the exact pragma to use. * * This function is a slot resolver that automatically evaluates slot functions to generate React elements. * A byproduct of this resolver is that it removes slots from the React hierarchy by bypassing React.createElement. * - * To use this function on a per-file basis, use the jsx directive targeting withSlots. - * This directive must be the FIRST LINE in the file to work correctly. - * Usage of this pragma also requires withSlots import statement. + * To use this legacy helper on a per-file basis, add the classic-runtime jsx directive targeting withSlots + * as the first line of the file, and import withSlots. This is only retained for deprecated framework code. * * See React.createElement */ // Can't use typeof on React.createElement since it's overloaded. Approximate createElement's signature for now and widen as needed. export function withSlots(reactType: Parameters[0], props?: unknown, ...children: React.ReactNode[]) { - // if it is a non-string type with _canCompose set just call the function directly, otherwise call createElement as normal - return renderForClassicRuntime(reactType, props, ...children); + return renderForClassicRuntime(reactType, props, children); } diff --git a/packages/framework-base/src/const.ts b/packages/framework-base/src/const.ts new file mode 100644 index 00000000000..55cd59b9edb --- /dev/null +++ b/packages/framework-base/src/const.ts @@ -0,0 +1,27 @@ +/** + * Symbols used as keys for slot-related metadata in Furn components. + */ + +/** + * Key for the inner component for a slot, or the staged/phased render function for a phased component + * @internal + */ +export const SLOT_COMPONENT_KEY = Symbol.for('furn.slotComponent'); + +/** + * Key for attached props for a component that should be added to the final render output + * @internal + */ +export const SLOT_PROPS_KEY = Symbol.for('furn.slotProps'); + +/** + * Key for an attached optional final prop transform for the component + * @internal + */ +export const SLOT_PROP_TRANSFORM_KEY = Symbol.for('furn.slotPropTransform'); + +/** + * Key for the render type of a component, used to determine how the component should be rendered + * @internal + */ +export const SLOT_RENDER_TYPE_KEY = Symbol.for('furn.slotRenderType'); diff --git a/packages/framework-base/src/hooks/usePressableState.test.tsx b/packages/framework-base/src/hooks/usePressableState.test.tsx new file mode 100644 index 00000000000..16f2d1d8428 --- /dev/null +++ b/packages/framework-base/src/hooks/usePressableState.test.tsx @@ -0,0 +1,132 @@ +import * as React from 'react'; +import { act } from 'react'; +import * as renderer from 'react-test-renderer'; +import type { PressableProps } from 'react-native'; + +import { usePressableState } from './usePressableState'; +import type { UsePressableResult } from './usePressableState'; +import type { PressableState } from '../types/interactive.types'; + +/** + * Render harness that runs usePressableState in a component and exposes the latest result so tests can + * inspect the returned props/state and fire the augmented event handlers. usePressableState returns a + * [props, state] tuple; the getters below always read the most recent tuple after a re-render. + */ +function renderPressableState(props: PressableProps = {}, stateKeys?: (keyof PressableState)[]) { + const latest: { current: UsePressableResult } = { current: undefined as unknown as UsePressableResult }; + const Harness: React.FunctionComponent = () => { + latest.current = usePressableState(props, stateKeys); + return null; + }; + act(() => { + renderer.create(); + }); + return { + get props(): PressableProps { + return latest.current[0]; + }, + get state(): PressableState { + return latest.current[1]; + }, + }; +} + +// events are opaque to the hook, so a cast placeholder is sufficient +const evt = {} as never; + +describe('usePressableState', () => { + it('returns a [props, state] tuple', () => { + const latest: { current: UsePressableResult } = { current: undefined as unknown as UsePressableResult }; + const Harness: React.FunctionComponent = () => { + latest.current = usePressableState({}); + return null; + }; + act(() => { + renderer.create(); + }); + expect(Array.isArray(latest.current)).toBe(true); + expect(latest.current).toHaveLength(2); + const [props, state] = latest.current; + expect(typeof props.onPressIn).toBe('function'); + expect(state).toEqual({ pressed: false, hovered: false, focused: false }); + }); + + it('tracks all three states by default, starting false', () => { + const result = renderPressableState(); + // state is seeded to false for every tracked key + expect(result.state).toEqual({ pressed: false, hovered: false, focused: false }); + // handlers for all three states are wired + expect(typeof result.props.onPressIn).toBe('function'); + expect(typeof result.props.onHoverIn).toBe('function'); + expect(typeof result.props.onFocus).toBe('function'); + }); + + it('toggles pressed on press in/out', () => { + const result = renderPressableState(); + act(() => result.props.onPressIn?.(evt)); + expect(result.state.pressed).toBe(true); + act(() => result.props.onPressOut?.(evt)); + expect(result.state.pressed).toBe(false); + }); + + it('toggles hovered on hover in/out', () => { + const result = renderPressableState(); + act(() => result.props.onHoverIn?.(evt)); + expect(result.state.hovered).toBe(true); + act(() => result.props.onHoverOut?.(evt)); + expect(result.state.hovered).toBe(false); + }); + + it('toggles focused on focus/blur', () => { + const result = renderPressableState(); + act(() => result.props.onFocus?.(evt)); + expect(result.state.focused).toBe(true); + act(() => result.props.onBlur?.(evt)); + expect(result.state.focused).toBe(false); + }); + + it('forwards the original handlers supplied in props', () => { + const onPressIn = jest.fn(); + const onHoverIn = jest.fn(); + const onFocus = jest.fn(); + const result = renderPressableState({ onPressIn, onHoverIn, onFocus }); + act(() => result.props.onPressIn?.(evt)); + act(() => result.props.onHoverIn?.(evt)); + act(() => result.props.onFocus?.(evt)); + expect(onPressIn).toHaveBeenCalledTimes(1); + expect(onHoverIn).toHaveBeenCalledTimes(1); + expect(onFocus).toHaveBeenCalledTimes(1); + expect(result.state).toEqual({ pressed: true, hovered: true, focused: true }); + }); + + it('only tracks the requested state keys', () => { + const result = renderPressableState({}, ['pressed']); + // state is seeded to false only for the requested keys + expect(result.state).toEqual({ pressed: false }); + // only pressed handlers are wired; hover/focus handlers are not added + expect(typeof result.props.onPressIn).toBe('function'); + expect(result.props.onHoverIn).toBeUndefined(); + expect(result.props.onFocus).toBeUndefined(); + + act(() => result.props.onPressIn?.(evt)); + expect(result.state).toEqual({ pressed: true }); + expect('hovered' in result.state).toBe(false); + expect('focused' in result.state).toBe(false); + }); + + it('passes untracked handlers through without wrapping (no state added)', () => { + const onHoverIn = jest.fn(); + // only track pressed, so hovered handlers should be the originals and hovered state should not appear + const result = renderPressableState({ onHoverIn }, ['pressed']); + expect(result.props.onHoverIn).toBe(onHoverIn); + act(() => result.props.onHoverIn?.(evt)); + expect(onHoverIn).toHaveBeenCalledTimes(1); + expect('hovered' in result.state).toBe(false); + }); + + it('preserves non-event props on the augmented props', () => { + const result = renderPressableState({ accessibilityLabel: 'test', disabled: true }); + expect(result.props.accessibilityLabel).toBe('test'); + expect(result.props.disabled).toBe(true); + }); +}); diff --git a/packages/framework-base/src/hooks/usePressableState.ts b/packages/framework-base/src/hooks/usePressableState.ts new file mode 100644 index 00000000000..e6574be9b71 --- /dev/null +++ b/packages/framework-base/src/hooks/usePressableState.ts @@ -0,0 +1,85 @@ +import * as React from 'react'; +import type { PressableProps } from 'react-native'; +import type { PressableState } from '../types/interactive.types'; + +const DEFAULT_STATE_KEYS: (keyof PressableState)[] = ['pressed', 'hovered', 'focused']; + +export type UsePressableResult = [props: PressableProps, state: PressableState]; + +/** + * A hook that augments a set of PressableProps with state tracking for some set of pressed, focused, and hovered states. This makes it + * easier to use a pressable component and track its interactive state without having to manually manage the state for each of the events. + * + * For each tracked state key the matching event handlers are wrapped so that they update the tracked state and then forward to any handler + * supplied in the incoming props. Handlers for states that are not tracked are passed through unchanged. + * + * @param props The PressableProps to augment with state tracking + * @param stateKeys The keys of the PressableState to track. Defaults to all three keys. + * @returns A tuple of the augmented props to spread onto a Pressable and the current interactive state for the tracked keys. + */ +export function usePressableState(props: PressableProps, stateKeys: (keyof PressableState)[] = DEFAULT_STATE_KEYS): UsePressableResult { + // create a state entry to track the pressable state for the requested keys. + const [state, setState] = React.useState(() => initialStateFromKeys(stateKeys)); + + // determine which states are being tracked so that we can wrap the appropriate handlers + const press = stateKeys.includes('pressed'); + const hover = stateKeys.includes('hovered'); + const focus = stateKeys.includes('focused'); + + // pull out the interactive props so that we can wrap them with our own handlers, and then forward to the original handlers if they exist + const { onPressIn, onPressOut, onHoverIn, onHoverOut, onFocus, onBlur } = props; + + // create a memoized object of the overrides to avoid creating new functions on every render. This is set up this way as this allows + // this hook to only create the override functions for the keys that are requested to be tracked. It also only uses one hook entry for + // the callbacks. The downside is changing one function will recreate all of the functions but realistically it is not common to change + // the handlers frequently at runtime. + const overrides = React.useMemo(() => { + const callbacks: PressableProps = {}; + if (press) { + callbacks.onPressIn = (e) => { + setState((prev) => ({ ...prev, pressed: true })); + onPressIn?.(e); + }; + callbacks.onPressOut = (e) => { + setState((prev) => ({ ...prev, pressed: false })); + onPressOut?.(e); + }; + } + if (hover) { + callbacks.onHoverIn = (e) => { + setState((prev) => ({ ...prev, hovered: true })); + onHoverIn?.(e); + }; + callbacks.onHoverOut = (e) => { + setState((prev) => ({ ...prev, hovered: false })); + onHoverOut?.(e); + }; + } + if (focus) { + callbacks.onFocus = (e) => { + setState((prev) => ({ ...prev, focused: true })); + onFocus?.(e); + }; + callbacks.onBlur = (e) => { + setState((prev) => ({ ...prev, focused: false })); + onBlur?.(e); + }; + } + return callbacks; + }, [press, hover, focus, onPressIn, onPressOut, onHoverIn, onHoverOut, onFocus, onBlur]); + + return [{ ...props, ...overrides }, state]; +} + +/** + * Creates an initial state object for the requested keys, with all values set to false. This is used to initialize the state for the usePressableState hook. + * @param keys The keys of the PressableState to initialize. + * @returns An initial state object with all values set to false. + */ +function initialStateFromKeys(keys: (keyof PressableState)[]): PressableState { + const initialState: PressableState = {}; + for (const key of keys) { + initialState[key] = false; + } + return initialState; +} diff --git a/packages/framework-base/src/immutable-merge/Merge.test.ts b/packages/framework-base/src/immutable-merge/Merge.test.ts index 0f579e34916..14a5732144f 100644 --- a/packages/framework-base/src/immutable-merge/Merge.test.ts +++ b/packages/framework-base/src/immutable-merge/Merge.test.ts @@ -1,5 +1,5 @@ -import type { MergeOptions } from './Merge.ts'; -import { immutableMerge, immutableMergeCore, processImmutable } from './Merge.ts'; +import type { MergeOptions } from './Merge'; +import { immutableMerge, immutableMergeCore, processImmutable } from './Merge'; interface IFakeStyle { s1?: string; diff --git a/packages/framework-base/src/immutable-merge/Merge.ts b/packages/framework-base/src/immutable-merge/Merge.ts index 54f90e4f9e4..ebde7e91859 100644 --- a/packages/framework-base/src/immutable-merge/Merge.ts +++ b/packages/framework-base/src/immutable-merge/Merge.ts @@ -1,5 +1,5 @@ -import { getEntityType, isObject } from '../utilities/typeUtils.ts'; -import type { ObjectMerger, ObjectMergerWithOptions } from '../utilities/mergeTypes.ts'; +import { getEntityType, isObject } from '../utilities/typeUtils'; +import type { ObjectMerger, ObjectMergerWithOptions } from '../types/props.types'; /** * The basic options for recursion at a given level. Two types for two behaviors: diff --git a/packages/framework-base/src/index.ts b/packages/framework-base/src/index.ts index ee17d8e0fd9..1a1e7255eda 100644 --- a/packages/framework-base/src/index.ts +++ b/packages/framework-base/src/index.ts @@ -1,63 +1,98 @@ -// immutable-merge exports -export { immutableMerge, immutableMergeCore, processImmutable, filterToObjects } from './immutable-merge/Merge.ts'; +/** + * ----- IMMUTABLE MERGE ----- + */ + +export { immutableMerge, immutableMergeCore, processImmutable, filterToObjects } from './immutable-merge/Merge'; export type { BuiltinRecursionHandlers, CustomRecursionHandler, MergeOptions, RecursionHandler, RecursionOption, -} from './immutable-merge/Merge.ts'; +} from './immutable-merge/Merge'; + +/** + * ----- MEMO CACHE ----- + */ +export type { GetMemoValue, GetTypedMemoValue } from './memo-cache/getMemoCache'; +export { getMemoCache, getTypedMemoCache } from './memo-cache/getMemoCache'; +export { memoize } from './memo-cache/memoize'; + +/** + * ----- MERGE PROPS / MERGE STYLES ----- + */ +export { mergeStyles } from './merge-props/mergeStyles'; +export { mergeProps } from './merge-props/mergeProps'; + +/** + * ----- COMPONENT PATTERNS ----- + */ + +export { renderSlot, createSlotComponent, renderJsx } from './component-patterns/render'; +export { directComponent, legacyDirectComponent } from './component-patterns/direct'; +export { phasedComponent, stagedComponent } from './component-patterns/phased'; +export { useSlot, useOptionalSlot } from './component-patterns/useSlot'; +export { + isDirectComponentType, + isSlotComponent, + isDirectComponent, + isLegacyDirectComponent, + isStagedComponent, + isPhasedComponent, +} from './component-patterns/identify'; + +// legacy JSX handler +export { withSlots } from './component-patterns/withSlots'; -// memo-cache exports -export type { GetMemoValue, GetTypedMemoValue } from './memo-cache/getMemoCache.ts'; -export { getMemoCache, getTypedMemoCache } from './memo-cache/getMemoCache.ts'; -export { memoize } from './memo-cache/memoize.ts'; +/** + * ----- HOOKS ----- + */ +export { usePressableState } from './hooks/usePressableState'; +export type { UsePressableResult } from './hooks/usePressableState'; +export type { PressableState, PressableStateKeys } from './types/interactive.types'; -// merge-props exports -export { mergeStyles } from './merge-props/mergeStyles.ts'; -export { mergeProps } from './merge-props/mergeProps.ts'; +/** + * ----- JSX RUNTIME ----- + */ +export { jsx, jsxs } from './jsx-runtime'; +export type { FurnJSX } from './types/react.types'; -// component pattern exports - extracting from elements -export { extractChildren, extractProps, extractStyle } from './component-patterns/extract.ts'; +/** + * ----- UTILITIES ----- + */ +export { filterProps, propTransformFromFilter } from './utilities/filterProps'; +export { extractChildren, extractProps, extractStyle } from './utilities/extract'; +export { getPropsChildren, isObject, setPropsChildren, splitPropsAndChildren, getEntityType } from './utilities/typeUtils'; +export type { ExpandedTypeof, TypeofResult } from './utilities/typeUtils'; -// component pattern exports - rendering utilities -export { renderForJsxRuntime, renderSlot, asDirectComponent } from './component-patterns/render.ts'; +/** + * ------- TYPES -------- + */ -// component pattern exports - core types +export type { + StyleProp, + ObjectBase, + ObjectFallback, + ObjectMerger, + ObjectMergerWithOptions, + StyleMerger, + PropsFilter, + PropsOf, + PropsWithoutChildren, + PropsWithoutRef, +} from './types/props.types'; export type { DirectComponent, FunctionComponent, - FunctionComponentCore, LegacyDirectComponent, + LegacyFunctionComponent, PhasedComponent, PhasedRender, - PropsOf, + StagedComponent, + StagedRender, + PropsTransform, RenderType, RenderResult, - StagedRender, - ComposableFunction, - FinalRender, - SlotFn, - NativeReactType, -} from './component-patterns/render.types.ts'; - -// component pattern exports - component builders -export { directComponent } from './component-patterns/directComponent.ts'; -export { getPhasedRender, phasedComponent } from './component-patterns/phasedComponent.ts'; -export { stagedComponent } from './component-patterns/stagedComponent.ts'; - -// component pattern exports - legacy JSX handlers -export { withSlots } from './component-patterns/withSlots.tsx'; - -// jsx runtime exports -export { jsx, jsxs } from './jsx-runtime.ts'; -export type { FurnJSX } from './jsx-namespace.ts'; - -// general utilities -export { filterProps } from './utilities/filterProps.ts'; -export type { PropsFilter } from './utilities/filterProps.ts'; - -// core type utilities exports -export type { StyleProp, ObjectBase, ObjectFallback } from './utilities/baseTypes.ts'; -export type { ObjectMerger, ObjectMergerWithOptions, StyleMerger } from './utilities/mergeTypes.ts'; -export type { ExpandedTypeof, TypeofResult } from './utilities/typeUtils.ts'; + SlotComponent, +} from './types/render.types'; +export type { DistributiveOmit, DistributivePick, UnionToIntersection, ReplaceNullWithUndefined, IsSingleton } from './types/utility.types'; diff --git a/packages/framework-base/src/jsx-runtime.ts b/packages/framework-base/src/jsx-runtime.ts index 956e2ab8589..9ee52d066b9 100644 --- a/packages/framework-base/src/jsx-runtime.ts +++ b/packages/framework-base/src/jsx-runtime.ts @@ -1,7 +1,7 @@ import type React from 'react'; import * as ReactJSX from 'react/jsx-runtime'; -import { renderForJsxRuntime } from './component-patterns/render.ts'; -export type { FurnJSX as JSX } from './jsx-namespace.ts'; +import { renderForJsxRuntime } from './component-patterns/render'; +export type { JSX } from 'react/jsx-runtime'; export const jsx: typeof ReactJSX.jsx = (type, props, key?: React.Key) => { return renderForJsxRuntime(type, props, key, ReactJSX.jsx); diff --git a/packages/framework-base/src/memo-cache/getCacheEntry.test.ts b/packages/framework-base/src/memo-cache/getCacheEntry.test.ts index ca74351f4c7..bb92e0885c8 100644 --- a/packages/framework-base/src/memo-cache/getCacheEntry.test.ts +++ b/packages/framework-base/src/memo-cache/getCacheEntry.test.ts @@ -1,5 +1,5 @@ -import type { CacheEntry } from './getCacheEntry.ts'; -import { getCacheEntry } from './getCacheEntry.ts'; +import type { CacheEntry } from './getCacheEntry'; +import { getCacheEntry } from './getCacheEntry'; interface TestObj { id: number; diff --git a/packages/framework-base/src/memo-cache/getMemoCache.test.ts b/packages/framework-base/src/memo-cache/getMemoCache.test.ts index 934e432b298..1f2c8ff1862 100644 --- a/packages/framework-base/src/memo-cache/getMemoCache.test.ts +++ b/packages/framework-base/src/memo-cache/getMemoCache.test.ts @@ -1,4 +1,4 @@ -import { getMemoCache, getTypedMemoCache } from './getMemoCache.ts'; +import { getMemoCache, getTypedMemoCache } from './getMemoCache'; interface TestObj { id: number; diff --git a/packages/framework-base/src/memo-cache/getMemoCache.ts b/packages/framework-base/src/memo-cache/getMemoCache.ts index b02cfd52675..fd3b558479e 100644 --- a/packages/framework-base/src/memo-cache/getMemoCache.ts +++ b/packages/framework-base/src/memo-cache/getMemoCache.ts @@ -1,5 +1,5 @@ -import type { CacheEntry } from './getCacheEntry.ts'; -import { getCacheEntry } from './getCacheEntry.ts'; +import type { CacheEntry } from './getCacheEntry'; +import { getCacheEntry } from './getCacheEntry'; export type ValueFactory = () => T; diff --git a/packages/framework-base/src/memo-cache/memoize.test.ts b/packages/framework-base/src/memo-cache/memoize.test.ts index aa0d595a823..923df5c2057 100644 --- a/packages/framework-base/src/memo-cache/memoize.test.ts +++ b/packages/framework-base/src/memo-cache/memoize.test.ts @@ -1,4 +1,4 @@ -import { memoize } from './memoize.ts'; +import { memoize } from './memoize'; let _globalCalls = 0; diff --git a/packages/framework-base/src/memo-cache/memoize.ts b/packages/framework-base/src/memo-cache/memoize.ts index 75e082ca28a..3d312a85c3a 100644 --- a/packages/framework-base/src/memo-cache/memoize.ts +++ b/packages/framework-base/src/memo-cache/memoize.ts @@ -1,4 +1,4 @@ -import { getMemoCache } from './getMemoCache.ts'; +import { getMemoCache } from './getMemoCache'; /** * This wraps a function to memoize the results using the standard javascript memoization pattern diff --git a/packages/framework-base/src/merge-props/index.ts b/packages/framework-base/src/merge-props/index.ts deleted file mode 100644 index ddc67901d81..00000000000 --- a/packages/framework-base/src/merge-props/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { mergeStyles } from './mergeStyles.ts'; -export { mergeProps } from './mergeProps.ts'; diff --git a/packages/framework-base/src/merge-props/mergeProps.test.ts b/packages/framework-base/src/merge-props/mergeProps.test.ts new file mode 100644 index 00000000000..6f943534e97 --- /dev/null +++ b/packages/framework-base/src/merge-props/mergeProps.test.ts @@ -0,0 +1,28 @@ +import { mergeProps } from './mergeProps'; + +describe('mergeProps children handling', () => { + it('does not let a null child overwrite a real child', () => { + const result = mergeProps({ children: 'Real' }, { children: null }); + expect(result).toEqual({ children: 'Real' }); + }); + + it('does not let an undefined child overwrite a real child', () => { + const result = mergeProps({ children: 'Real' }, { children: undefined }); + expect(result).toEqual({ children: 'Real' }); + }); + + it('lets a later real child override an earlier one', () => { + const result = mergeProps({ children: 'First' }, { children: 'Second' }); + expect(result).toEqual({ children: 'Second' }); + }); + + it('keeps a real child when the null comes first', () => { + const result = mergeProps({ children: null }, { children: 'Real' }); + expect(result).toEqual({ children: 'Real' }); + }); + + it('preserves other props while fixing children', () => { + const result = mergeProps({ children: 'Real', a: 1 }, { children: null, b: 2 }); + expect(result).toEqual({ children: 'Real', a: 1, b: 2 }); + }); +}); diff --git a/packages/framework-base/src/merge-props/mergeProps.ts b/packages/framework-base/src/merge-props/mergeProps.ts index 0bd62a3f20e..a969c7ce7a1 100644 --- a/packages/framework-base/src/merge-props/mergeProps.ts +++ b/packages/framework-base/src/merge-props/mergeProps.ts @@ -1,8 +1,8 @@ -import type { MergeOptions } from '../immutable-merge/Merge.ts'; -import { immutableMergeCore, filterToObjects } from '../immutable-merge/Merge.ts'; -import type { ObjectMerger } from '../utilities/mergeTypes.ts'; +import type { MergeOptions } from '../immutable-merge/Merge'; +import { immutableMergeCore, filterToObjects } from '../immutable-merge/Merge'; +import type { ObjectMerger } from '../types/props.types'; -import { mergeStyles } from './mergeStyles.ts'; +import { mergeStyles } from './mergeStyles'; /** * Props will not deeply merge with the exception of a style property. Also className needs to be handled specially. @@ -10,6 +10,17 @@ import { mergeStyles } from './mergeStyles.ts'; const mergePropsOptions: MergeOptions = { className: (...names: any[]) => names.filter((v) => v && typeof v === 'string').join(' '), style: mergeStyles, + // children follow last-wins semantics, but a null (or undefined) child must not clobber a real child value + // supplied elsewhere in the merge. Pick the last defined (non-null, non-undefined) value instead. + children: (...values: any[]) => { + let merged: unknown = undefined; + for (const value of values) { + if (value !== null && value !== undefined) { + merged = value; + } + } + return merged; + }, }; /** diff --git a/packages/framework-base/src/merge-props/mergeStyles.test.ts b/packages/framework-base/src/merge-props/mergeStyles.test.ts index 52201117741..93e1be336b5 100644 --- a/packages/framework-base/src/merge-props/mergeStyles.test.ts +++ b/packages/framework-base/src/merge-props/mergeStyles.test.ts @@ -1,5 +1,5 @@ -import { flattenStyle, mergeAndFlattenStyles, mergeStyles } from './mergeStyles.ts'; -import type { StyleProp } from '../utilities/baseTypes.ts'; +import { flattenStyle, mergeAndFlattenStyles, mergeStyles } from './mergeStyles'; +import type { StyleProp } from '../types/props.types'; type OpaqueColorValue = symbol & { __TYPE__: 'Color' }; type ColorValue = string | OpaqueColorValue; diff --git a/packages/framework-base/src/merge-props/mergeStyles.ts b/packages/framework-base/src/merge-props/mergeStyles.ts index 46b4c611a63..bf7bb201995 100644 --- a/packages/framework-base/src/merge-props/mergeStyles.ts +++ b/packages/framework-base/src/merge-props/mergeStyles.ts @@ -1,8 +1,6 @@ -import { immutableMerge } from '../immutable-merge/Merge.ts'; -import { getMemoCache } from '../memo-cache/getMemoCache.ts'; -import type { StyleMerger } from '../utilities/mergeTypes.ts'; - -import type { StyleProp } from '../utilities/baseTypes.ts'; +import { immutableMerge } from '../immutable-merge/Merge'; +import { getMemoCache } from '../memo-cache/getMemoCache'; +import type { StyleMerger, StyleProp } from '../types/props.types'; /** * Take a react-native style, which may be a recursive array, and return as a flattened diff --git a/packages/framework-base/src/types/interactive.types.ts b/packages/framework-base/src/types/interactive.types.ts new file mode 100644 index 00000000000..4d4fae5b386 --- /dev/null +++ b/packages/framework-base/src/types/interactive.types.ts @@ -0,0 +1,19 @@ +import type { PressableProps } from 'react-native'; + +/** + * Interactive state type for components that can be pressed, hovered, or focused. This is the type used + * with pressable components to track their interactive state. It is a subset of the PressableProps + */ +export type PressableState = { + pressed?: boolean; + hovered?: boolean; + focused?: boolean; +}; + +/** + * Keys of PressableProps that are used to track interactive state. This is used to filter out the props that are not relevant to the interactive state. + */ +export type PressableStateKeys = Extract< + keyof PressableProps, + 'onPressIn' | 'onPressOut' | 'onHoverIn' | 'onHoverOut' | 'onFocus' | 'onBlur' +>; diff --git a/packages/framework-base/src/types/props.types.test.ts b/packages/framework-base/src/types/props.types.test.ts new file mode 100644 index 00000000000..0809ab3013c --- /dev/null +++ b/packages/framework-base/src/types/props.types.test.ts @@ -0,0 +1,86 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/** + * Type-consistency tests for the base prop types. + * + * The `type`-level assertions below are validated at build time by the type-check pass + * (`targets/tsconfig.check.json` includes `*.test.*` files), so any regression in these foundational + * types will break the build. Each assertion is also materialized as a runtime boolean so the `it` + * blocks form a real Jest suite that fails loudly if an assertion ever resolves to something other + * than `true`. + */ + +import type { StyleProp, ObjectBase, ObjectFallback } from './props.types'; +import type { StyleProp as RNStyleProp } from 'react-native'; + +// --- type assertion helpers --------------------------------------------------------------------- + +/** Resolves to `true` when `A` is assignable to `B`, otherwise `false`. Wrapped in tuples to avoid distributing over unions. */ +type Extends = [A] extends [B] ? true : false; + +/** Resolves to `true` only when `X` and `Y` are exactly the same type. */ +type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? true : false; + +/** Compile-time guard: only accepts a `true` type argument, producing a build error otherwise. */ +type Expect = T; + +// --- fixtures ----------------------------------------------------------------------------------- + +type StyleBase = { + color?: string; + fontSize?: number; +}; + +interface IStyleBase { + color?: string; + fontSize?: number; +} + +// --- compile-time assertions -------------------------------------------------------------------- +// Each constant is typed via `Expect<...>`; if a type relationship regresses, the build fails here. +// The runtime value is always `true`, so the Jest assertions below pass while still exercising the +// exact relationships we care about. + +// StyleProp must remain assignable to React Native's StyleProp, for both type aliases and interfaces. +const stylePropCompatibleWithType: Expect, RNStyleProp>> = true; +const stylePropCompatibleWithInterface: Expect, RNStyleProp>> = true; + +// ObjectBase should accept plain records, interfaces, and class instances. +const objectBaseAcceptsRecord: Expect, ObjectBase>> = true; +const objectBaseAcceptsInterface: Expect> = true; +const objectBaseAcceptsClass: Expect> = true; + +// ObjectFallback (Record) is stricter: it accepts records but rejects class +// instances and interfaces that lack an implicit index signature. +const objectFallbackAcceptsRecord: Expect, ObjectFallback>> = true; +const objectFallbackRejectsClass: Expect, false>> = true; +const objectFallbackRejectsInterface: Expect, false>> = true; + +// Cross assignment: ObjectFallback widens to ObjectBase, but ObjectBase does not narrow to ObjectFallback. +const fallbackAssignableToBase: Expect> = true; +const baseNotAssignableToFallback: Expect, false>> = true; + +// --- runtime suite ------------------------------------------------------------------------------ + +describe('base prop type consistency', () => { + it('keeps StyleProp compatible with React Native StyleProp', () => { + expect(stylePropCompatibleWithType).toBe(true); + expect(stylePropCompatibleWithInterface).toBe(true); + }); + + it('keeps ObjectBase permissive for records, interfaces, and class instances', () => { + expect(objectBaseAcceptsRecord).toBe(true); + expect(objectBaseAcceptsInterface).toBe(true); + expect(objectBaseAcceptsClass).toBe(true); + }); + + it('keeps ObjectFallback strict against classes and index-less interfaces', () => { + expect(objectFallbackAcceptsRecord).toBe(true); + expect(objectFallbackRejectsClass).toBe(true); + expect(objectFallbackRejectsInterface).toBe(true); + }); + + it('allows ObjectFallback to widen to ObjectBase but not the reverse', () => { + expect(fallbackAssignableToBase).toBe(true); + expect(baseNotAssignableToFallback).toBe(true); + }); +}); diff --git a/packages/framework-base/src/types/props.types.ts b/packages/framework-base/src/types/props.types.ts new file mode 100644 index 00000000000..b59e357bc29 --- /dev/null +++ b/packages/framework-base/src/types/props.types.ts @@ -0,0 +1,120 @@ +import type React from 'react'; +import type { DistributiveOmit } from './utility.types'; + +/** + * This is a copy of the react-native style prop type, copied here to avoid RN dependencies this early in the dependency tree. + */ +type Falsy = undefined | null | false | ''; +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface RecursiveArray extends Array> {} +/** Keep a brand of 'T' so that calls to `StyleSheet.flatten` can take `RegisteredStyle` and return `T`. */ +type RegisteredStyle = number & { __registeredStyleBrand: T }; +export type StyleProp = T | RegisteredStyle | RecursiveArray | Falsy> | Falsy; + +/** + * This is the baseline for acceptance object types, meaning for T extends ObjectBase. The options here + * are: + * - {} an empty object, which works but is a bit too loose for general use + * - Record which is fine with types but doesn't work with + * interfaces as they have no implicit index signature + * - object which is the built in object type, slightly stricter than {} but still allows for interfaces + * + * There's no perfect option here but object is the best overall choice. + */ +export type ObjectBase = object; + +/** + * For fallback object types it is better to use the stricter Record type, as it + * is more likely to catch issues with unexpected properties and is still compatible with the + * ObjectBase type. + */ +export type ObjectFallback = Record; + +/** + * Get the props from a react component type + */ +export type PropsOf = TComponent extends React.JSXElementConstructor ? P : TFallback; + +/** + * Removes the 'ref' prop from the given Props type, leaving unions intact (such as the discriminated union created by + * IntrinsicSlotProps). This allows IntrinsicSlotProps to be used with React.forwardRef. + */ +export type PropsWithoutRef

= 'ref' extends keyof P ? DistributiveOmit : P; + +/** + * Returns just the children entry from a props type or {} if not present + */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- {} is intentional here as a no-op when intersected/spread +export type PropsChildren

= 'children' extends keyof P ? Pick : {}; + +/** + * Removes the 'children' prop from the given Props type, leaving unions intact (such as the discriminated union created by + * IntrinsicSlotProps). This allows IntrinsicSlotProps to be used with React.forwardRef. + */ +export type PropsWithoutChildren

= 'children' extends keyof P ? DistributiveOmit : P; + +/** + * A partial props type that omits children + */ +export type PartialWithoutChildren

= PropsWithoutChildren>; + +/** + * A signature for a property filter function. + * @param propName - The name of a property to test. + * @returns A boolean indicating whether the property is valid. true if valid, false if should be filtered out. + */ +export type PropsFilter = (propName: string) => boolean; + +/** + * Overloaded function types for an object merger, similar to Object.assign but with better type inference and support for + * undefined values. + */ +export type ObjectMerger = { + // T1 defined overloads + (o1: T1, ...objs: undefined[]): T1; + (o1: T1, o2: T2, ...objs: undefined[]): T1 & T2; + (o1: T1, o2: T2, o3: T3, ...objs: undefined[]): T1 & T2 & T3; + // T1 undefined overloads + (o1: undefined, o2: T2, ...objs: undefined[]): T2; + (o1: undefined, o2: T2, o3: T3, ...objs: undefined[]): T2 & T3; + // T2 undefined overload + (o1: T1, o2: undefined, o3: T3, ...objs: undefined[]): T1 & T3; + // rest overloads + (...objs: unknown[]): T | undefined; +}; + +/** + * Overloaded function types for an object merger that takes options, similar to Object.assign but with better type inference and support for + * undefined values, and with an options parameter to control merge behavior. + */ +export type ObjectMergerWithOptions = { + // T1 defined overloads + (opt: TOptions, o1: T1, ...objs: undefined[]): T1; + (opt: TOptions, o1: T1, o2: T2, ...objs: undefined[]): T1 & T2; + (opt: TOptions, o1: T1, o2: T2, o3: T3, ...objs: undefined[]): T1 & T2 & T3; + // T1 undefined overloads + (opt: TOptions, o1: undefined, o2: T2, ...objs: undefined[]): T2; + (opt: TOptions, o1: undefined, o2: T2, o3: T3, ...objs: undefined[]): T2 & T3; + // T2 undefined overload + (opt: TOptions, o1: T1, o2: undefined, o3: T3, ...objs: undefined[]): T1 & T3; + // rest overloads + (opt: TOptions, ...objs: unknown[]): T | undefined; +}; + +/** + * Overloaded function types for a style merger, which is similar to an object merger but specifically for merging styles that may be in the form of StyleProp types. + * This includes support for merging styles of different types, which is useful when merging token-based styles with React Native StyleProp types. + */ +export type StyleMerger = { + // T1 defined overloads + (o1: StyleProp, ...objs: undefined[]): T1; + (o1: StyleProp, o2: StyleProp, ...objs: undefined[]): T1 & T2; + (o1: StyleProp, o2: StyleProp, o3: StyleProp, ...objs: undefined[]): T1 & T2 & T3; + // T1 undefined overloads + (o1: StyleProp, o2: StyleProp, ...objs: undefined[]): T2; + (o1: StyleProp, o2: StyleProp, o3: StyleProp, ...objs: undefined[]): T2 & T3; + // T2 undefined overload + (o1: StyleProp, o2: StyleProp, o3: StyleProp, ...objs: undefined[]): T1 & T3; + // rest overloads + (...objs: unknown[]): T | undefined; +}; diff --git a/packages/framework-base/src/jsx-namespace.ts b/packages/framework-base/src/types/react.types.ts similarity index 81% rename from packages/framework-base/src/jsx-namespace.ts rename to packages/framework-base/src/types/react.types.ts index 534abc94ea4..fa059e8fd0a 100644 --- a/packages/framework-base/src/jsx-namespace.ts +++ b/packages/framework-base/src/types/react.types.ts @@ -1,17 +1,23 @@ import type React from 'react'; /** - * This file defines a custom JSX namespace that re-exports React's JSX types, but also allows us to add our own custom behavior to the JSX runtime. - * The main reason we need this is to support our "direct component" pattern, which allows certain components to bypass React's createElement and - * return their own React elements directly from the JSX runtime. + * React 18/19 type-compatibility helpers. * - * Exporting this custom namespace is required to make intrinsic attributes like key and ref work correctly with our custom JSX functions. The normal - * fallback behavior if not defined is to not allow any attributes on intrinsic elements, which breaks a lot of React functionality. + * `@types/react` moved the JSX types between major versions: + * - React 18: they live in the global `JSX` namespace (no `React.JSX`). + * - React 19: they live in `React.JSX` (no global `JSX`). * - * The custom behavior is implemented in the jsx and jsxs functions, which first check if the type being rendered is a direct component and if so, call it directly. + * The `ReactJSX*` aliases below resolve to whichever set exists in the consuming project, letting us + * reference the JSX intrinsic types without breaking under either React version. They are gathered into + * the `FurnJSX` namespace, which mirrors the shape of a JSX namespace so it can be used as a custom + * `jsxImportSource` namespace if needed. * - * Thanks to the emotion library's jsx-namespace for providing a reference implementation of how to do this while handling both React 18 and React 19's - * changes to the JSX types. + * NOTE: the framework's jsx-runtime currently re-exports React's own JSX namespace directly, so `FurnJSX` + * is not wired up as the active JSX namespace. It is kept here because the React 18/19 indirection is a + * generally useful building block. + * + * Thanks to the emotion library's jsx-namespace for providing a reference implementation of how to do this + * while handling both React 18 and React 19's changes to the JSX types. */ type IsPreReact19 = 2 extends Parameters>['length'] ? true : false; diff --git a/packages/framework-base/src/types/render.types.ts b/packages/framework-base/src/types/render.types.ts new file mode 100644 index 00000000000..fc7f014cf92 --- /dev/null +++ b/packages/framework-base/src/types/render.types.ts @@ -0,0 +1,182 @@ +import type React from 'react'; +import type { SLOT_COMPONENT_KEY, SLOT_RENDER_TYPE_KEY, SLOT_PROPS_KEY, SLOT_PROP_TRANSFORM_KEY } from '../const'; +import type { PropsOf } from './props.types'; + +/** + * Base types for rendering components in a react application, extracted from react types. + * + * EXPERIMENT: use React's standard JSX namespace directly rather than a custom FurnJSX namespace. + */ +export type RenderResult = React.JSX.Element; +export type RenderType = React.JSX.ElementType; + +/** + * type of the render function, not a FunctionComponent to help prevent hook usage + */ +export type FunctionComponent = { + (props: TProps): RenderResult; + displayName?: string; +}; + +export type CustomRenderType = + | 'callable' // A function that can be called directly to bypass a level of indirection + | 'legacy' // A function that can be called directly, but with children as a rest parameter + | 'phased' // A function that is called in phases, first during the hook phase and then during the render phase + | 'phased-legacy'; // A function that is called in phases, but with the legacy signature + +/** + * DIRECT RENDERING + * + * This is a pattern where a function can by called directly to render a component, bypassing creating additional layers of the + * rendering tree. This is useful for higher order components that mainly need to do simple prop manipulation but want to + * compartmentalize the logic. + * + * Note that for this to be safe, hooks cannot be used in the function. This is the reason why function component is redefined, + * to help linting tools catch bad usage. + * + * The newer DirectComponent type should be used, as it will handle children consistently. + */ + +/** + * The newer direct component type. Effectively a function component with an extra static marking it as callable + */ +export type DirectComponent = FunctionComponent & { + [SLOT_RENDER_TYPE_KEY]: Extract; +}; + +/** + * A legacy function component is a function that can be called with props and children, but the children are passed as a rest parameter. + * This is a legacy pattern and should be avoided in new code. + * @deprecated + */ +export type LegacyFunctionComponent = { + (props: TProps, ...children: React.ReactNode[]): RenderResult; + displayName?: string; +}; + +/** + * The legacy direct callable component type, which is used to mark function components as being directly callable, as well + * as being used to render across a hook phase and a render phase safely. This was effectively a perpetuated error as the + * correct signature should have children as a part of props at the component level. + * @deprecated + */ +export type LegacyDirectComponent = LegacyFunctionComponent & { + [SLOT_RENDER_TYPE_KEY]: Extract; +}; + +/** + * 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 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 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 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. + */ + +/** + * Phased component signature, it can be rendered as a standard component, but if combined with the useSlots + * hook, the hook routines for the component can be called returning an inner component that may be directly + * rendered. + * + * For this type of component, the returned inner component can be of any type. + */ +export type PhasedComponent = FunctionComponent & { + [SLOT_COMPONENT_KEY]: PhasedRender; + [SLOT_RENDER_TYPE_KEY]: Extract; +}; + +export type PhasedRender = (props: Partial) => React.ComponentType; + +/** + * Legacy pattern which returns a legacy direct component. + * @deprecated use the newer phasedComponent pattern for new code + */ +export type StagedComponent = FunctionComponent & { + [SLOT_COMPONENT_KEY]: StagedRender; + [SLOT_RENDER_TYPE_KEY]: Extract; +}; + +export type StagedRender = (props: Partial) => LegacyFunctionComponent; + +/** + * SLOT COMPONENT + * + * These are designed to be closely aligned with the newer Fluent slot patterns. In essence a Slot is a valid component + * which contains: + * - An React component or element type that will be used to render the slot. + * - Optional attached props for the slot, these will be merged with the props passed in during rendering. + * - Optional props transformation function, this can be used to do things like filtering or making final modifications to the props before they are passed to the inner component. + */ +export type SlotComponent = FunctionComponent & { + /** + * This is the inner component type used to render the slot. + */ + [SLOT_COMPONENT_KEY]: React.ComponentType; + + /** + * Optional attached props for the slot, these will be merged with the props passed in during rendering. + */ + [SLOT_PROPS_KEY]: Partial; + + /** + * Optional props transformation function, this can be used to do things like filtering or making final + * modifications to the props before they are passed to the inner component. + */ + [SLOT_PROP_TRANSFORM_KEY]?: (props: TProps) => TProps; +}; + +export type PropsTransform = (props: TPropsIn) => TPropsOut; + +/** + * useSlot hook signature, here to make overloads easier to understand + * - overload 1 handles the case where all required props are provided, in this case the component accepts partial props + * - overload 2 handles the case where required props are missing, in this case they must be passed to the jsx + */ +export type UseSlot = { + /** + * First overload: fulfilled props + * - either no required props or all required props (except children) satisfied + * - result is a component that has partial props so there is no need to provide them again in jsx + */ + ( + component: React.ComponentType, + props: PropsOf, + transform?: PropsTransform, + ): SlotComponent>>; + /** + * Second overload: non-children props not fulfilled + * - result is a component that requires all required props to be provided in the jsx + */ + ( + component: React.ComponentType, + props: Partial>, + transform?: PropsTransform, + ): SlotComponent>; +}; + +/** + * useOptionalSlot signature, adds the ability for the component to be null or undefined + * - if so will return null for the slot + */ +export type UseOptionalSlot = { + /** + * Third overload: component type is undefined or null + * - result is a null return result + */ + ( + component: React.ComponentType | undefined | null, + props: PropsOf, + transform?: PropsTransform, + ): SlotComponent>> | null; + ( + component: React.ComponentType | undefined | null, + props: Partial>, + transform?: PropsTransform, + ): SlotComponent> | null; +}; diff --git a/packages/framework-base/src/types/utility.types.ts b/packages/framework-base/src/types/utility.types.ts new file mode 100644 index 00000000000..f5cd653535d --- /dev/null +++ b/packages/framework-base/src/types/utility.types.ts @@ -0,0 +1,70 @@ +/** + * Helper type that works similar to Omit, + * but when modifying an union type it will distribute the omission to all the union members. + * + * See [distributive conditional types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types) for more information + */ +// Traditional Omit is basically equivalent to => Pick> +// +// let's say we have Omit<{ a: string } | { b: string }, 'a'> +// equivalent to: Pick<{ a: string } | { b: string }, Exclude> +// The expected result would be {} | { b: string }, the omission of 'a' from all the union members, +// but keyof ({ a: string } | { b: string }) is never as they don't share common keys +// so Exclude is never, +// and Pick<{ a: string } | { b: string }, never> is {}. +// +// With DistributiveOmit on the other hand it becomes like this: +// DistributiveOmit<{ a: string } | { b: string }, 'a'> +// equivalent to: Omit<{ a: string }, 'a'> | Omit<{ b: string }, 'a'> +// Since every single Omit clause in this case is being applied to a single union member there's no conflicts on keyof evaluation and in the second clause Omit<{ b: string }, 'a'> becomes { b: string }, +// so the result is {} | { b: string }, as expected. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type DistributiveOmit = T extends unknown ? Omit : T; + +/** + * Helper type that works similar to Pick, + * but when modifying an union type it will distribute the picking to all the union members. + * + * See {@link https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types} for more information + * + * @public + */ +export type DistributivePick = T extends unknown ? Pick : never; + +/** + * Converts a union type (`A | B | C`) to an intersection type (`A & B & C`) + */ +export type UnionToIntersection = (U extends unknown ? (x: U) => U : never) extends (x: infer I) => U ? I : never; + +/** + * If type T includes `null`, remove it and add `undefined` instead. + * + * @internal + */ +export type ReplaceNullWithUndefined = T extends null ? Exclude | undefined : T; + +/** + * Simplify a type by removing any unnecessary nesting and making it easier to read. + * This is useful for types that are the result of complex type operations, such as unions and intersections. + */ +export type Simplify = { [K in keyof T]: T[K] } & {}; + +/** + * Makes all properties of a type optional, but ensure the Partial<> designation is cleanly on the outside for consistency + */ +export type OuterPartial = Simplify>>; + +/** + * Evaluates to true if the given type contains exactly one string, or false if it is a union of strings. + * + * ``` + * IsSingleton<'a'> // true + * IsSingleton<'a' | 'b' | 'c'> // false + * ``` + */ +export type IsSingleton = { [K in T]: Exclude extends never ? true : false }[T]; + +/** + * Make all the fields of a given type partial except the ones specified in K + */ +export type PartialExcept = DistributivePick & Partial>; diff --git a/packages/framework-base/src/utilities/baseTypes.ts b/packages/framework-base/src/utilities/baseTypes.ts deleted file mode 100644 index b93dd70140f..00000000000 --- a/packages/framework-base/src/utilities/baseTypes.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * This is a copy of the react-native style prop type, copied here to avoid RN dependencies this early in the dependency tree. - */ -type Falsy = undefined | null | false | ''; -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -interface RecursiveArray extends Array> {} -/** Keep a brand of 'T' so that calls to `StyleSheet.flatten` can take `RegisteredStyle` and return `T`. */ -type RegisteredStyle = number & { __registeredStyleBrand: T }; -export type StyleProp = T | RegisteredStyle | RecursiveArray | Falsy> | Falsy; - -/** - * This is the baseline for acceptance object types, meaning for T extends ObjectBase. The options here - * are: - * - {} an empty object, which works but is a bit too loose for general use - * - Record which is fine with types but doesn't work with - * interfaces as they have no implicit index signature - * - object which is the built in object type, slightly stricter than {} but still allows for interfaces - * - * There's no perfect option here but object is the best overall choice. - */ -export type ObjectBase = object; - -/** - * For fallback object types it is better to use the stricter Record type, as it - * is more likely to catch issues with unexpected properties and is still compatible with the - * ObjectBase type. - */ -export type ObjectFallback = Record; diff --git a/packages/framework-base/src/utilities/baseTypes.validate.ts b/packages/framework-base/src/utilities/baseTypes.validate.ts deleted file mode 100644 index 3c5d6ae9d62..00000000000 --- a/packages/framework-base/src/utilities/baseTypes.validate.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/** - * Type validation that the base types behave as expected. This code is never run and is not included - * in other files, but will cause build breaks if the types no longer behave as expected. - */ - -import type { StyleProp, ObjectBase, ObjectFallback } from './baseTypes.ts'; -import type { StyleProp as RNStyleProp } from 'react-native'; - -/** - * Validate that StyleProp is compatible with React Native's StyleProp type, as this is a critical part of our type system for styles and we want to ensure it remains compatible with RN's types. - */ -export type ValidateStyleProp = StyleProp extends RNStyleProp ? true : never; - -type StyleBase = { - color?: string; - fontSize?: number; -}; - -type TestProps = { - p1?: string; - p2?: number; - p3?: boolean; - style?: StyleProp; -}; - -const typeProps: TestProps = { - p1: 'string', - p2: 123, - p3: true, - style: { - color: 'red', - fontSize: 16, - }, -}; - -interface IStyleBase { - color?: string; - fontSize?: number; -} - -interface ITestProps { - p1?: string; - p2?: number; - p3?: boolean; - style?: StyleProp; -} - -const interfaceProps: ITestProps = { - p1: 'string', - p2: 123, - p3: true, - style: { - color: 'red', - fontSize: 16, - }, -}; - -export function validateBaseTypes() { - // This function is never called, but if the types of the base types change in a way that breaks compatibility with expected types, this will cause a build error and alert us to the issue. - - // Test that StyleProp is compatible with React Native's StyleProp type - const stylePropTest: ValidateStyleProp = true; - const stylePropTest2: ValidateStyleProp = true; - - // just using the values to stop typescript complaints - if (!stylePropTest || !stylePropTest2) { - throw new Error("StyleProp is not compatible with React Native's StyleProp type"); - } - - // Test that ObjectBase is compatible with object and Record - - const objectBaseTest1: ObjectBase = {}; - const objectBaseTest2: ObjectBase = { key: 'value' }; - const objectBaseTest3: ObjectBase = new Date(); - const objectBaseTest4: ObjectBase = typeProps; - const objectBaseTest5: ObjectBase = interfaceProps; - const objectBaseTest6: ObjectFallback = {}; - const objectBaseTest7: ObjectFallback = { key: 'value' }; - // @ts-expect-error - this should error because Date is not compatible with Record due to its properties not being string keys and unknown values - const objectBaseTest8: ObjectFallback = new Date(); - const objectBaseTest9: ObjectFallback = typeProps; - // @ts-expect-error - this should error because interfaceProps is not compatible with Record due to the style property being a StyleProp type which is not compatible with Record - const objectBaseTest10: ObjectFallback = interfaceProps; - - // cross assignment - const baseFromFallback: ObjectBase = objectBaseTest7; - // @ts-expect-error - this should error because ObjectFallback is not compatible with ObjectBase due to ObjectBase allowing for more types of objects than ObjectFallback - const fallbackFromBase: ObjectFallback = objectBaseTest2; - - return { - ...objectBaseTest1, - ...objectBaseTest2, - ...objectBaseTest3, - ...objectBaseTest4, - ...objectBaseTest5, - ...objectBaseTest6, - ...objectBaseTest7, - ...objectBaseTest8, - ...objectBaseTest9, - ...objectBaseTest10, - ...baseFromFallback, - }; -} diff --git a/packages/framework-base/src/component-patterns/extract.ts b/packages/framework-base/src/utilities/extract.ts similarity index 97% rename from packages/framework-base/src/component-patterns/extract.ts rename to packages/framework-base/src/utilities/extract.ts index d3c2c38d392..6b2bbadfb1b 100644 --- a/packages/framework-base/src/component-patterns/extract.ts +++ b/packages/framework-base/src/utilities/extract.ts @@ -1,3 +1,4 @@ +import type React from 'react'; import type { ViewStyle, TextStyle, ImageStyle } from 'react-native'; type StyleTypes = ViewStyle | TextStyle | ImageStyle; diff --git a/packages/framework-base/src/utilities/filterProps.ts b/packages/framework-base/src/utilities/filterProps.ts index d78c5c003f4..a61293e277a 100644 --- a/packages/framework-base/src/utilities/filterProps.ts +++ b/packages/framework-base/src/utilities/filterProps.ts @@ -1,7 +1,6 @@ -import { mergeProps } from '../merge-props/mergeProps.ts'; -import { isObject } from './typeUtils.ts'; - -export type PropsFilter = (propName: string) => boolean; +import { mergeProps } from '../merge-props/mergeProps'; +import { isObject } from './typeUtils'; +import type { PropsFilter } from '../types/props.types'; export function filterProps(props: TProps, filter?: PropsFilter): TProps { if (filter && isObject(props)) { @@ -13,3 +12,23 @@ export function filterProps(props: TProps, filter?: PropsFilter): TProps } return props; } + +/** + * Caching wrapper that turns filters into transform functions. Realistically we only have three main filters in the library + * that are rarely used. This will create wrappers on demand that will be cached and re-used. + */ +export const propTransformFromFilter = (() => { + // weak map for caching transform functions + type TransformCache = WeakMap(props: T) => T>; + let cache: TransformCache; + return (filter?: PropsFilter) => { + if (filter) { + cache ??= new WeakMap(); + if (!cache.has(filter)) { + cache.set(filter, (props: T) => filterProps(props, filter)); + } + return cache.get(filter) as (props: TProps) => TProps; + } + return undefined; + }; +})(); diff --git a/packages/framework-base/src/utilities/mergeTypes.ts b/packages/framework-base/src/utilities/mergeTypes.ts deleted file mode 100644 index ae9d67e9b84..00000000000 --- a/packages/framework-base/src/utilities/mergeTypes.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { StyleProp, ObjectFallback } from './baseTypes.ts'; - -/** - * Overloaded function types for an object merger, similar to Object.assign but with better type inference and support for - * undefined values. - */ -export type ObjectMerger = { - // T1 defined overloads - (o1: T1, ...objs: undefined[]): T1; - (o1: T1, o2: T2, ...objs: undefined[]): T1 & T2; - (o1: T1, o2: T2, o3: T3, ...objs: undefined[]): T1 & T2 & T3; - // T1 undefined overloads - (o1: undefined, o2: T2, ...objs: undefined[]): T2; - (o1: undefined, o2: T2, o3: T3, ...objs: undefined[]): T2 & T3; - // T2 undefined overload - (o1: T1, o2: undefined, o3: T3, ...objs: undefined[]): T1 & T3; - // rest overloads - (...objs: unknown[]): T | undefined; -}; - -/** - * Overloaded function types for an object merger that takes options, similar to Object.assign but with better type inference and support for - * undefined values, and with an options parameter to control merge behavior. - */ -export type ObjectMergerWithOptions = { - // T1 defined overloads - (opt: TOptions, o1: T1, ...objs: undefined[]): T1; - (opt: TOptions, o1: T1, o2: T2, ...objs: undefined[]): T1 & T2; - (opt: TOptions, o1: T1, o2: T2, o3: T3, ...objs: undefined[]): T1 & T2 & T3; - // T1 undefined overloads - (opt: TOptions, o1: undefined, o2: T2, ...objs: undefined[]): T2; - (opt: TOptions, o1: undefined, o2: T2, o3: T3, ...objs: undefined[]): T2 & T3; - // T2 undefined overload - (opt: TOptions, o1: T1, o2: undefined, o3: T3, ...objs: undefined[]): T1 & T3; - // rest overloads - (opt: TOptions, ...objs: unknown[]): T | undefined; -}; - -/** - * Overloaded function types for a style merger, which is similar to an object merger but specifically for merging styles that may be in the form of StyleProp types. - * This includes support for merging styles of different types, which is useful when merging token-based styles with React Native StyleProp types. - */ -export type StyleMerger = { - // T1 defined overloads - (o1: StyleProp, ...objs: undefined[]): T1; - (o1: StyleProp, o2: StyleProp, ...objs: undefined[]): T1 & T2; - (o1: StyleProp, o2: StyleProp, o3: StyleProp, ...objs: undefined[]): T1 & T2 & T3; - // T1 undefined overloads - (o1: StyleProp, o2: StyleProp, ...objs: undefined[]): T2; - (o1: StyleProp, o2: StyleProp, o3: StyleProp, ...objs: undefined[]): T2 & T3; - // T2 undefined overload - (o1: StyleProp, o2: StyleProp, o3: StyleProp, ...objs: undefined[]): T1 & T3; - // rest overloads - (...objs: unknown[]): T | undefined; -}; diff --git a/packages/framework-base/src/utilities/typeUtils.ts b/packages/framework-base/src/utilities/typeUtils.ts index 02850270f2b..3d3f5980739 100644 --- a/packages/framework-base/src/utilities/typeUtils.ts +++ b/packages/framework-base/src/utilities/typeUtils.ts @@ -1,3 +1,8 @@ +import type React from 'react'; +import type { PropsWithChildren } from 'react'; +import type { DistributiveOmit, DistributivePick } from '../types/utility.types'; +import type { PropsWithoutChildren } from '../types/props.types'; + /** * */ @@ -30,24 +35,92 @@ export function getEntityType(val: unknown): ExpandedTypeof { * @param value some value of unknown type * @returns an assertion that the value is an object with string keys and unknown values (not an array or null) */ -export function isObject>(value: unknown): value is T { +export function isObject>(value: unknown): value is T { return typeof value === 'object' && value !== null && !Array.isArray(value); } +export function getPropSubset(props: TProps, keys: Extract[]): Partial | undefined { + let result: Partial | undefined = undefined; + if (isObject(props)) { + for (const key of keys) { + if (key in props) { + result ??= {}; + result[key] = props[key]; + } + } + } + return result; +} + +export function getPropsWithoutSubset(props: TProps, keys: Extract[]): Partial | undefined { + const result = {} as TProps; + if (isObject(props)) { + for (const key in props) { + if (!keys.includes(key as Extract)) { + result[key as keyof TProps] = props[key] as TProps[keyof TProps]; + } + } + } + return result; +} + +export function splitProps(props: TProps, keys: Extract[]): [TProps, Partial | undefined] { + const split = getPropSubset(props, keys); + if (split) { + return [getPropsWithoutSubset(props, keys) as TProps, split]; + } + return [props, undefined]; +} + +export function splitAndOmitProp( + props: TProps, + key: K, +): [DistributiveOmit, DistributivePick | undefined] { + type ResultType = ReturnType>; + + const extractedKey = key as unknown as Extract; + const split = getPropSubset(props, [extractedKey]) as ResultType[1]; + if (split) { + return [getPropsWithoutSubset(props, [extractedKey]) as ResultType[0], split]; + } + return [props as ResultType[0], undefined]; +} + /** * Helper to split props into children and non-children props. * @param props unknown props type object to split * @returns a tuple of the non-children props and the children */ -export function splitPropsAndChildren(props: TProps): [Omit, React.ReactNode] { - const { children, ...rest } = props as React.PropsWithChildren; - return [rest as Omit, children]; +export function splitPropsAndChildren(props: TProps): [PropsWithoutChildren, { children?: React.ReactNode } | undefined] { + const [nonChildrenProps, childrenProps] = splitAndOmitProp(props as PropsWithChildren, 'children'); + return [nonChildrenProps as PropsWithoutChildren, childrenProps]; } /** * Helper to get the children from an unknown props type object. */ -export function extractChildren(props: TProps): React.ReactNode { - const { children } = props as React.PropsWithChildren; +export function getPropsChildren(props: TProps): React.ReactNode | undefined { + return (props as PropsWithChildren).children; +} + +export function setPropsChildren(props: TProps, children: React.ReactNode | undefined): TProps { + if (children !== undefined) { + const existing = getPropsChildren(props); + if (existing !== children) { + const newChildren = normalizeChildren(children); + if (existing !== newChildren && (newChildren != null || existing == null)) { + (props as PropsWithChildren).children = newChildren; + } + } + } + return props; +} + +function normalizeChildren(children: React.ReactNode): React.ReactNode { + if (Array.isArray(children)) { + if (children.length < 2) { + return children.length === 1 ? children[0] : undefined; + } + } return children; } diff --git a/packages/framework-base/targets/tsconfig.build.json b/packages/framework-base/targets/tsconfig.build.json new file mode 100644 index 00000000000..09d846cc7d7 --- /dev/null +++ b/packages/framework-base/targets/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "@fluentui-react-native/scripts/tsconfig-strict", + "compilerOptions": { + "outDir": "../lib", + "allowJs": true, + "checkJs": true, + "rootDir": "../src", + "composite": true, + "tsBuildInfoFile": "../.cache/tsconfig.build.tsbuildinfo" + }, + "include": ["../src"], + "exclude": ["../**/*.test.*"] +} diff --git a/packages/framework-base/targets/tsconfig.check.json b/packages/framework-base/targets/tsconfig.check.json new file mode 100644 index 00000000000..3f1ff13cff4 --- /dev/null +++ b/packages/framework-base/targets/tsconfig.check.json @@ -0,0 +1,12 @@ +{ + "extends": "@fluentui-react-native/scripts/tsconfig-strict", + "compilerOptions": { + "rootDir": "../src", + "noEmit": true, + "allowJs": true, + "checkJs": true, + "composite": true, + "tsBuildInfoFile": "../.cache/tsconfig.check.tsbuildinfo" + }, + "include": ["../src"] +} diff --git a/packages/framework-base/tsconfig.json b/packages/framework-base/tsconfig.json index dd65e7b5f42..ad7a2cb1fd6 100644 --- a/packages/framework-base/tsconfig.json +++ b/packages/framework-base/tsconfig.json @@ -1,17 +1,17 @@ { - "extends": "@fluentui-react-native/scripts/tsconfig-strict", "compilerOptions": { - "outDir": "lib", - "allowJs": true, - "checkJs": true, - "rootDir": "src", - "composite": true, - "tsBuildInfoFile": ".cache/tsconfig.tsbuildinfo" + "composite": true }, - "include": ["src"], + "files": [], "references": [ { "path": "../../scripts/tsconfig.json" + }, + { + "path": "./targets/tsconfig.build.json" + }, + { + "path": "./targets/tsconfig.check.json" } ] } diff --git a/packages/framework/composition/src/composeFactory.test.tsx b/packages/framework/composition/src/composeFactory.test.tsx index 83b9d36da4f..65b0aac5cee 100644 --- a/packages/framework/composition/src/composeFactory.test.tsx +++ b/packages/framework/composition/src/composeFactory.test.tsx @@ -6,8 +6,8 @@ import { View, Text } from 'react-native'; import type { ThemeHelper } from '@fluentui-react-native/use-styling'; import * as renderer from 'react-test-renderer'; -import type { UseStyledSlots } from './composeFactory.ts'; -import { composeFactory } from './composeFactory.ts'; +import type { UseStyledSlots } from './composeFactory'; +import { composeFactory } from './composeFactory'; type Theme = { values: { diff --git a/packages/framework/composition/src/composeFactory.ts b/packages/framework/composition/src/composeFactory.ts index e9b0639e0f1..39c37545728 100644 --- a/packages/framework/composition/src/composeFactory.ts +++ b/packages/framework/composition/src/composeFactory.ts @@ -1,6 +1,6 @@ import type { MergeOptions } from '@fluentui-react-native/framework-base'; import { immutableMergeCore } from '@fluentui-react-native/framework-base'; -import type { ComposableFunction, FinalRender } from '@fluentui-react-native/framework-base'; +import type { LegacyFunctionComponent, StagedComponent } from '@fluentui-react-native/framework-base'; import { stagedComponent } from '@fluentui-react-native/framework-base'; import type { UseSlotOptions, Slots } from '@fluentui-react-native/use-slots'; import { buildUseSlots } from '@fluentui-react-native/use-slots'; @@ -25,7 +25,7 @@ export type ComposeFactoryOptions) => FinalRender; + useRender: (props: TProps, useSlots: UseStyledSlots) => LegacyFunctionComponent; /** * optional statics to attach to the component @@ -33,7 +33,7 @@ export type ComposeFactoryOptions = ComposableFunction & { +export type ComposeFactoryComponent = StagedComponent & { __options: ComposeFactoryOptions; customize: (...tokens: TokenSettings[]) => ComposeFactoryComponent; compose( @@ -68,7 +68,7 @@ export function composeFactory; // build the staged component - const component = stagedComponent((props) => options.useRender(props, useSlots)) as LocalComponent; + const component = stagedComponent((props) => options.useRender(props as TProps, useSlots)) as LocalComponent; // attach additional props to the returned component component.displayName = options.displayName; diff --git a/packages/framework/composition/src/index.ts b/packages/framework/composition/src/index.ts index bd4d5898ffb..a118b3d1da6 100644 --- a/packages/framework/composition/src/index.ts +++ b/packages/framework/composition/src/index.ts @@ -1,2 +1,2 @@ -export { composeFactory } from './composeFactory.ts'; -export type { ComposeFactoryComponent, ComposeFactoryOptions, UseStyledSlots } from './composeFactory.ts'; +export { composeFactory } from './composeFactory'; +export type { ComposeFactoryComponent, ComposeFactoryOptions, UseStyledSlots } from './composeFactory'; diff --git a/packages/framework/framework/package.json b/packages/framework/framework/package.json index d0862364cef..0e609f1e9a5 100644 --- a/packages/framework/framework/package.json +++ b/packages/framework/framework/package.json @@ -38,7 +38,6 @@ "@fluentui-react-native/framework-base": "workspace:*", "@fluentui-react-native/theme-types": "workspace:*", "@fluentui-react-native/tokens": "workspace:*", - "@fluentui-react-native/use-slot": "workspace:*", "@fluentui-react-native/use-slots": "workspace:*", "@fluentui-react-native/use-styling": "workspace:*", "@fluentui-react-native/use-tokens": "workspace:*" diff --git a/packages/framework/framework/src/compose.ts b/packages/framework/framework/src/compose.ts index 0dfd92dd19f..5285edac215 100644 --- a/packages/framework/framework/src/compose.ts +++ b/packages/framework/framework/src/compose.ts @@ -3,7 +3,7 @@ import { composeFactory } from '@fluentui-react-native/composition'; import type { Theme } from '@fluentui-react-native/theme-types'; import type { ObjectBase } from '@fluentui-react-native/framework-base'; -import { themeHelper } from './themeHelper.ts'; +import { themeHelper } from './themeHelper'; /** * This is an object used purely for configuring the typings on composable. It is not necessary to define diff --git a/packages/framework/framework/src/compressible.test.tsx b/packages/framework/framework/src/compressible.test.tsx index 86bdd7dc536..c0a2cd77dbe 100644 --- a/packages/framework/framework/src/compressible.test.tsx +++ b/packages/framework/framework/src/compressible.test.tsx @@ -4,15 +4,14 @@ import { act } from 'react'; import type { TextProps, TextStyle } from 'react-native'; import { Text, View } from 'react-native'; -import { mergeStyles } from '@fluentui-react-native/framework-base'; +import { mergeStyles, useSlot } from '@fluentui-react-native/framework-base'; import type { Theme } from '@fluentui-react-native/theme-types'; -import { useSlot } from '@fluentui-react-native/use-slot'; import { applyTokenLayers } from '@fluentui-react-native/use-tokens'; import * as renderer from 'react-test-renderer'; -import { compressible } from './compressible.ts'; -import { buildUseTokens } from './useTokens.ts'; -import type { UseTokens } from './useTokens.ts'; +import { compressible } from './compressible'; +import { buildUseTokens } from './useTokens'; +import type { UseTokens } from './useTokens'; type Variant = 'normal' | 'header' | 'caption'; interface VariantTextTokens { diff --git a/packages/framework/framework/src/compressible.ts b/packages/framework/framework/src/compressible.ts index 48c40b22ed3..b26a892959c 100644 --- a/packages/framework/framework/src/compressible.ts +++ b/packages/framework/framework/src/compressible.ts @@ -1,10 +1,10 @@ import type { Theme } from '@fluentui-react-native/theme-types'; -import type { StagedRender } from '@fluentui-react-native/framework-base'; import { stagedComponent } from '@fluentui-react-native/framework-base'; import type { CustomizableComponent } from '@fluentui-react-native/use-tokens'; -import type { TokenSettings } from './useStyling.ts'; -import type { UseTokens } from './useTokens.ts'; +import type { TokenSettings } from './useStyling'; +import type { UseTokens } from './useTokens'; +import type { LegacyFunctionComponent } from '@fluentui-react-native/framework-base'; /** * Utility function which can create function components that can be tree compressed (using the stagedRender pattern), @@ -14,13 +14,13 @@ import type { UseTokens } from './useTokens.ts'; * @returns A tree compressed function component with the `.customize` method exposed to it */ export function compressible( - fn: StagedRender, + fn: (props: TProps, useTokens: UseTokens) => LegacyFunctionComponent, useTokens: UseTokens, ): CustomizableComponent { type ThisComponent = CustomizableComponent; - const injectedWrapper = (props: TProps) => fn(props, useTokens); - const component = stagedComponent(injectedWrapper) as ThisComponent; + const injectedWrapper = (props: Partial) => fn(props as TProps, useTokens); + const component = stagedComponent(injectedWrapper) as unknown as ThisComponent; component.customize = (...tokens: TokenSettings[]) => { const useTokensNew = useTokens.customize(...tokens); diff --git a/packages/framework/framework/src/index.ts b/packages/framework/framework/src/index.ts index bb05e8af65a..b648c074c3d 100644 --- a/packages/framework/framework/src/index.ts +++ b/packages/framework/framework/src/index.ts @@ -32,9 +32,8 @@ export type { TokenBuilder, } from '@fluentui-react-native/tokens'; -export { useSlot } from '@fluentui-react-native/use-slot'; -export { renderSlot, stagedComponent, withSlots } from '@fluentui-react-native/framework-base'; -export type { ComposableFunction, FinalRender, NativeReactType, SlotFn, StagedRender } from '@fluentui-react-native/framework-base'; +export { renderSlot, stagedComponent, withSlots, useSlot } from '@fluentui-react-native/framework-base'; +export type { RenderType as NativeReactType } from '@fluentui-react-native/framework-base'; export { buildUseSlots } from '@fluentui-react-native/use-slots'; export type { GetSlotProps, Slots, UseSlotOptions, UseSlotsBase } from '@fluentui-react-native/use-slots'; @@ -81,7 +80,7 @@ export type { Variants, } from '@fluentui-react-native/theme-types'; -export { compose } from './compose.ts'; +export { compose } from './compose'; export type { ComposableComponent, ComposeOptions, @@ -91,11 +90,11 @@ export type { ExtractStatics, ExtractTokens, UseSlots, -} from './compose.ts'; -export { compressible } from './compressible.ts'; -export { useFluentTheme } from './useFluentTheme.ts'; -export type { HasLayer, TokensThatAreAlsoProps, UseStyling } from './useStyling.ts'; -export { buildProps, buildUseStyling } from './useStyling.ts'; -export type { BuildProps, TokenSettings, TokensFromTheme, UseStylingOptions } from './useStyling.ts'; -export { applyPropsToTokens, applyTokenLayers, buildUseTokens, customizable, patchTokens } from './useTokens.ts'; -export type { UseTokens, CustomizableComponent } from './useTokens.ts'; +} from './compose'; +export { compressible } from './compressible'; +export { useFluentTheme } from './useFluentTheme'; +export type { HasLayer, TokensThatAreAlsoProps, UseStyling } from './useStyling'; +export { buildProps, buildUseStyling } from './useStyling'; +export type { BuildProps, TokenSettings, TokensFromTheme, UseStylingOptions } from './useStyling'; +export { applyPropsToTokens, applyTokenLayers, buildUseTokens, customizable, patchTokens } from './useTokens'; +export type { UseTokens, CustomizableComponent } from './useTokens'; diff --git a/packages/framework/framework/src/themeHelper.ts b/packages/framework/framework/src/themeHelper.ts index 77d545b4f87..ffe58a4b884 100644 --- a/packages/framework/framework/src/themeHelper.ts +++ b/packages/framework/framework/src/themeHelper.ts @@ -1,7 +1,7 @@ import type { Theme } from '@fluentui-react-native/theme-types'; import type { ThemeHelper } from '@fluentui-react-native/use-styling'; -import { useFluentTheme } from './useFluentTheme.ts'; +import { useFluentTheme } from './useFluentTheme'; export const themeHelper: ThemeHelper = { useTheme: () => useFluentTheme(), diff --git a/packages/framework/framework/src/useStyling.ts b/packages/framework/framework/src/useStyling.ts index e5a7ea62416..f8fb0c97b2f 100644 --- a/packages/framework/framework/src/useStyling.ts +++ b/packages/framework/framework/src/useStyling.ts @@ -13,7 +13,7 @@ import type { } from '@fluentui-react-native/use-styling'; import { buildUseStyling as buildUseStylingBase, buildProps as buildPropsBase } from '@fluentui-react-native/use-styling'; -import { themeHelper } from './themeHelper.ts'; +import { themeHelper } from './themeHelper'; export type BuildProps = BuildPropsBase; diff --git a/packages/framework/framework/src/useTokens.ts b/packages/framework/framework/src/useTokens.ts index 79f848e2d47..14b0d0ca073 100644 --- a/packages/framework/framework/src/useTokens.ts +++ b/packages/framework/framework/src/useTokens.ts @@ -2,8 +2,8 @@ import type { Theme } from '@fluentui-react-native/theme-types'; import type { UseTokens as UseTokensCore } from '@fluentui-react-native/use-tokens'; import { buildUseTokens as buildUseTokensCore } from '@fluentui-react-native/use-tokens'; -import { themeHelper } from './themeHelper.ts'; -import type { TokenSettings } from './useStyling.ts'; +import { themeHelper } from './themeHelper'; +import type { TokenSettings } from './useStyling'; export { applyTokenLayers, applyPropsToTokens, customizable, patchTokens } from '@fluentui-react-native/use-tokens'; diff --git a/packages/framework/framework/tsconfig.json b/packages/framework/framework/tsconfig.json index 6e084a6344a..3390d4ba3a5 100644 --- a/packages/framework/framework/tsconfig.json +++ b/packages/framework/framework/tsconfig.json @@ -30,9 +30,6 @@ { "path": "../../utils/tokens/tsconfig.json" }, - { - "path": "../use-slot/tsconfig.json" - }, { "path": "../use-slots/tsconfig.json" }, diff --git a/packages/framework/theme/src/ThemeProvider.tsx b/packages/framework/theme/src/ThemeProvider.tsx index d3cc54b419b..6a951c49974 100644 --- a/packages/framework/theme/src/ThemeProvider.tsx +++ b/packages/framework/theme/src/ThemeProvider.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { ThemeContext } from '@fluentui-react-native/theme-types'; -import type { ThemeReference } from './themeReference.ts'; +import type { ThemeReference } from './themeReference'; export interface ThemeProviderProps extends React.PropsWithChildren> { /** diff --git a/packages/framework/theme/src/index.ts b/packages/framework/theme/src/index.ts index eb089aa60fc..02173d06b60 100644 --- a/packages/framework/theme/src/index.ts +++ b/packages/framework/theme/src/index.ts @@ -1,4 +1,4 @@ -export { ThemeProvider } from './ThemeProvider.tsx'; -export type { ThemeProviderProps } from './ThemeProvider.tsx'; -export { ThemeReference } from './themeReference.ts'; -export type { OnThemeChange, ThemeRecipe, ThemeTransform } from './themeReference.ts'; +export { ThemeProvider } from './ThemeProvider'; +export type { ThemeProviderProps } from './ThemeProvider'; +export { ThemeReference } from './themeReference'; +export type { OnThemeChange, ThemeRecipe, ThemeTransform } from './themeReference'; diff --git a/packages/framework/theme/src/themeReference.test.ts b/packages/framework/theme/src/themeReference.test.ts index 786b51010db..15f5f481e69 100644 --- a/packages/framework/theme/src/themeReference.test.ts +++ b/packages/framework/theme/src/themeReference.test.ts @@ -1,7 +1,7 @@ import { mockTheme } from '@fluentui-react-native/test-tools'; import type { Theme, Spacing, PartialTheme } from '@fluentui-react-native/theme-types'; -import { ThemeReference } from './themeReference.ts'; +import { ThemeReference } from './themeReference'; const themeBase = mockTheme; diff --git a/packages/framework/theme/src/themeReference.ts b/packages/framework/theme/src/themeReference.ts index cff6bce600c..d9d74abe196 100644 --- a/packages/framework/theme/src/themeReference.ts +++ b/packages/framework/theme/src/themeReference.ts @@ -1,6 +1,6 @@ import type { Theme, PartialTheme } from '@fluentui-react-native/theme-types'; -import { mergeTheme } from './mergeTheme.ts'; +import { mergeTheme } from './mergeTheme'; /** * Argument type for listening for theme changes diff --git a/packages/framework/themed-stylesheet/src/index.ts b/packages/framework/themed-stylesheet/src/index.ts index 034d65bc319..d2c85bd61a5 100644 --- a/packages/framework/themed-stylesheet/src/index.ts +++ b/packages/framework/themed-stylesheet/src/index.ts @@ -1,2 +1,2 @@ -export { themedStyleSheet } from './themedStyleSheet.ts'; -export type { NamedStyles } from './themedStyleSheet.ts'; +export { themedStyleSheet } from './themedStyleSheet'; +export type { NamedStyles } from './themedStyleSheet'; diff --git a/packages/framework/themed-stylesheet/src/themedStyleSheet.test.ts b/packages/framework/themed-stylesheet/src/themedStyleSheet.test.ts index 841afa5b17f..e0b8027dd7c 100644 --- a/packages/framework/themed-stylesheet/src/themedStyleSheet.test.ts +++ b/packages/framework/themed-stylesheet/src/themedStyleSheet.test.ts @@ -1,4 +1,4 @@ -import { themedStyleSheet } from './themedStyleSheet.ts'; +import { themedStyleSheet } from './themedStyleSheet'; interface IFakeTheme { color1?: string; diff --git a/packages/framework/use-slot/.npmignore b/packages/framework/use-slot/.npmignore deleted file mode 100644 index b85bc2eca6e..00000000000 --- a/packages/framework/use-slot/.npmignore +++ /dev/null @@ -1,10 +0,0 @@ -node_modules -.gitignore -.gitattributes -.editorconfig -config.js -jest.config.js -tslint.json -tsconfig.json -jsconfig.json -*.build.log diff --git a/packages/framework/use-slot/CHANGELOG.json b/packages/framework/use-slot/CHANGELOG.json deleted file mode 100644 index b6f9b526d40..00000000000 --- a/packages/framework/use-slot/CHANGELOG.json +++ /dev/null @@ -1,758 +0,0 @@ -{ - "name": "@fluentui-react-native/use-slot", - "entries": [ - { - "date": "Tue, 05 Aug 2025 18:50:39 GMT", - "version": "0.6.12", - "tag": "@fluentui-react-native/use-slot_v0.6.12", - "comments": { - "patch": [ - { - "author": "jasonmo@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "1ed213f1b48036479e39ace7f25f9a31f46042bf", - "comment": "move repo to pnpm, create babel and jest config packages, fix dependencies" - } - ] - } - }, - { - "date": "Tue, 22 Jul 2025 19:06:24 GMT", - "version": "0.6.8", - "tag": "@fluentui-react-native/use-slot_v0.6.8", - "comments": { - "patch": [ - { - "author": "jasonmo@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "21e536f6875cb6fb9c33b439f0eafe1d4debc7a4", - "comment": "centralize jsx rendering functionality in framework-base" - } - ] - } - }, - { - "date": "Wed, 16 Jul 2025 20:06:46 GMT", - "version": "0.6.7", - "tag": "@fluentui-react-native/use-slot_v0.6.7", - "comments": { - "patch": [ - { - "author": "jasonmo@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "6ed0a256caf94e287c6a9120ec5d0458bd5c7bfc", - "comment": "create common framework-base package to share common utiltities more broadly" - } - ] - } - }, - { - "date": "Tue, 15 Jul 2025 23:26:52 GMT", - "version": "0.6.6", - "tag": "@fluentui-react-native/use-slot_v0.6.6", - "comments": { - "none": [ - { - "author": "sanajmi@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "405ca368dc3f2963bfc252a1952b1e14ff503499", - "comment": "fix: run `beachball sync` to sync package versions with NPM" - }, - { - "author": "jasonmo@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "54087dca787180bbf34630470cd0d06e88366b30", - "comment": "update eslint to 9.x and use new flat config format with security rules" - }, - { - "author": "ruaraki@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "a245202877ef7f2be9ab5f0e1356b2270957e693", - "comment": "Run bump-versions" - }, - { - "author": "4123478+tido64@users.noreply.github.com", - "package": "@fluentui-react-native/use-slot", - "commit": "79bb06f6bbfd0a36f9d2ef371f4857be9660f027", - "comment": "Fixed `align-deps` warnings" - }, - { - "author": "jasonmo@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "3e168e4363d1821818f219819d29532eace8d979", - "comment": "update scripts package to be fully js, use esm, and validate fully" - } - ], - "patch": [ - { - "author": "sanajmi@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "9cbe229ec7f6363d229dcaef506921b8d9f47d7d", - "comment": "fix: run beachball sync" - }, - { - "author": "jasonmo@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "6b4d59227b1e9bbbf25cc93a3502015f663b363e", - "comment": "add react-native entrypoints that ensure metro targets TS files rather than JS files for bundling" - }, - { - "author": "jasonmo@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "3d36cc95620fdb938221cbe2c5ce002264108a50", - "comment": "fix repo linting with new configs and version, fixing linting errors as well" - }, - { - "author": "jasonmo@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "479b93cea460a26df70c55b5d3335927ed374713", - "comment": "update builds to use node16 settings and modern export maps" - } - ] - } - }, - { - "date": "Thu, 10 Jul 2025 19:20:14 GMT", - "version": "0.6.3", - "tag": "@fluentui-react-native/use-slot_v0.6.3", - "comments": { - "patch": [ - { - "author": "4123478+tido64@users.noreply.github.com", - "package": "@fluentui-react-native/use-slot", - "commit": "24b9abab3370f008afd2dbb304751fc9af6bec4a", - "comment": "Add support for React Native 0.74" - } - ] - } - }, - { - "date": "Wed, 11 Jun 2025 19:34:46 GMT", - "version": "0.6.2", - "tag": "@fluentui-react-native/use-slot_v0.6.2", - "comments": { - "none": [ - { - "author": "4123478+tido64@users.noreply.github.com", - "package": "@fluentui-react-native/use-slot", - "commit": "2342b87a20e5a584bd2073c8c5a266f0aa48c8bb", - "comment": "Added missing dev dependencies" - } - ] - } - }, - { - "date": "Wed, 29 Jan 2025 23:43:52 GMT", - "version": "0.6.2", - "tag": "@fluentui-react-native/use-slot_v0.6.2", - "comments": { - "patch": [ - { - "author": "sanajmi@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "5ca24ac45054439768404d117213a2ae4767235d", - "comment": "remove enzyme tests" - } - ] - } - }, - { - "date": "Thu, 11 Apr 2024 18:08:40 GMT", - "version": "0.6.0", - "tag": "@fluentui-react-native/use-slot_v0.6.0", - "comments": { - "minor": [ - { - "author": "30809111+acoates-ms@users.noreply.github.com", - "package": "@fluentui-react-native/use-slot", - "commit": "6164473627c321b03d301376c48227f4abd8e947", - "comment": "Update to RN 0.73" - } - ] - } - }, - { - "date": "Fri, 01 Mar 2024 20:39:08 GMT", - "version": "0.5.2", - "tag": "@fluentui-react-native/use-slot_v0.5.2", - "comments": { - "patch": [ - { - "author": "ruaraki@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "17c82b22513a177eaaee6941e5d4bbc2d2654030", - "comment": "Use workspace for version of local package" - } - ] - } - }, - { - "date": "Thu, 22 Feb 2024 23:27:44 GMT", - "version": "0.5.1", - "tag": "@fluentui-react-native/use-slot_v0.5.1", - "comments": { - "patch": [ - { - "author": "rofang@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "dbd7aadff1ba8c61fd5d8aedde8b079c650e37fa", - "comment": "Rework package json version notation" - } - ] - } - }, - { - "date": "Thu, 09 Nov 2023 02:39:11 GMT", - "version": "0.5.0", - "tag": "@fluentui-react-native/use-slot_v0.5.0", - "comments": { - "minor": [ - { - "author": "30809111+acoates-ms@users.noreply.github.com", - "package": "@fluentui-react-native/use-slot", - "commit": "4a6a9cd63396c6e8fc6ffbfec7137652928e40ef", - "comment": "Update to react-native 0.72" - }, - { - "author": "beachball", - "package": "@fluentui-react-native/use-slot", - "comment": "Bump @fluentui-react-native/merge-props to v0.8.0", - "commit": "not available" - } - ] - } - }, - { - "date": "Wed, 09 Aug 2023 22:02:15 GMT", - "version": "0.4.2", - "tag": "@fluentui-react-native/use-slot_v0.4.2", - "comments": { - "patch": [ - { - "author": "beachball", - "package": "@fluentui-react-native/use-slot", - "comment": "Bump @fluentui-react-native/merge-props to v0.7.1", - "commit": "not available" - } - ] - } - }, - { - "date": "Mon, 12 Jun 2023 17:46:44 GMT", - "tag": "@fluentui-react-native/use-slot_v0.4.1", - "version": "0.4.1", - "comments": { - "patch": [ - { - "author": "beachball", - "package": "@fluentui-react-native/use-slot", - "comment": "Bump @fluentui-react-native/merge-props to v0.7.0", - "commit": "6530042f4f24cbb296337a8bcf5fb32a09cc6378" - } - ] - } - }, - { - "date": "Mon, 05 Jun 2023 19:26:25 GMT", - "tag": "@fluentui-react-native/use-slot_v0.4.0", - "version": "0.4.0", - "comments": { - "minor": [ - { - "author": "sanajmi@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "8c0f9d27e34913da82c00299c12c399877257325", - "comment": "Upgrade to React Native 0.71" - }, - { - "author": "beachball", - "package": "@fluentui-react-native/use-slot", - "comment": "Bump @fluentui-react-native/merge-props to v0.6.0", - "commit": "8c0f9d27e34913da82c00299c12c399877257325" - } - ] - } - }, - { - "date": "Fri, 24 Feb 2023 20:40:00 GMT", - "tag": "@fluentui-react-native/use-slot_v0.3.4", - "version": "0.3.4", - "comments": { - "patch": [ - { - "author": "78454019+lyzhan7@users.noreply.github.com", - "package": "@fluentui-react-native/use-slot", - "commit": "743bd1507af336a62946361f3fe3c800ae5bd4c2", - "comment": "Order imports" - }, - { - "author": "beachball", - "package": "@fluentui-react-native/use-slot", - "comment": "Bump @fluentui-react-native/merge-props to v0.5.3", - "commit": "743bd1507af336a62946361f3fe3c800ae5bd4c2" - } - ] - } - }, - { - "date": "Thu, 02 Feb 2023 01:29:39 GMT", - "tag": "@fluentui-react-native/use-slot_v0.3.3", - "version": "0.3.3", - "comments": { - "patch": [ - { - "author": "30809111+acoates-ms@users.noreply.github.com", - "package": "@fluentui-react-native/use-slot", - "commit": "ac679ca51704ba5c2d2cc2385a71694b69735a1d", - "comment": "Add eslint to packages missing a config" - } - ] - } - }, - { - "date": "Wed, 01 Feb 2023 22:33:17 GMT", - "tag": "@fluentui-react-native/use-slot_v0.3.2", - "version": "0.3.2", - "comments": { - "patch": [ - { - "author": "beachball", - "package": "@fluentui-react-native/use-slot", - "comment": "Bump @fluentui-react-native/merge-props to v0.5.2", - "commit": "2d0138c80d7512b905fcf32583760bec2b911910" - } - ] - } - }, - { - "date": "Mon, 30 Jan 2023 15:17:32 GMT", - "tag": "@fluentui-react-native/use-slot_v0.3.1", - "version": "0.3.1", - "comments": { - "none": [ - { - "author": "4123478+tido64@users.noreply.github.com", - "package": "@fluentui-react-native/use-slot", - "commit": "4de4c2b37c835ebd9af015ff767f9dd68452f786", - "comment": "Migrate to align-deps" - } - ] - } - }, - { - "date": "Fri, 30 Sep 2022 00:54:37 GMT", - "tag": "@fluentui-react-native/use-slot_v0.3.1", - "version": "0.3.1", - "comments": { - "patch": [ - { - "author": "krsiler@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "de9ddf75304295843613e144473e4fef3190f14b", - "comment": "Update react-native to 0.68" - }, - { - "author": "beachball", - "package": "@fluentui-react-native/use-slot", - "comment": "Bump @fluentui-react-native/merge-props to v0.5.1", - "commit": "de9ddf75304295843613e144473e4fef3190f14b" - } - ] - } - }, - { - "date": "Thu, 14 Jul 2022 18:09:50 GMT", - "tag": "@fluentui-react-native/use-slot_v0.3.0", - "version": "0.3.0", - "comments": { - "minor": [ - { - "author": "sanajmi@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "155145b496a51b06bba73f8f9c6b89dc7b348c4b", - "comment": "Update to React Native 0.66" - }, - { - "author": "beachball", - "package": "@fluentui-react-native/use-slot", - "comment": "Bump @fluentui-react-native/merge-props to v0.5.0", - "commit": "155145b496a51b06bba73f8f9c6b89dc7b348c4b" - } - ] - } - }, - { - "date": "Thu, 31 Mar 2022 07:27:48 GMT", - "tag": "@fluentui-react-native/use-slot_v0.2.6", - "version": "0.2.6", - "comments": { - "patch": [ - { - "author": "beachball", - "package": "@fluentui-react-native/use-slot", - "comment": "Bump @fluentui-react-native/merge-props to v0.4.3", - "commit": "c39b4436f2ca160a31ca1d7ca9fa8bf7d59512ed" - } - ] - } - }, - { - "date": "Fri, 18 Feb 2022 23:27:11 GMT", - "tag": "@fluentui-react-native/use-slot_v0.2.5", - "version": "0.2.5", - "comments": { - "none": [ - { - "author": "ruaraki@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "597b1e6bd7b3791df8fad2e0ec83810872261c54", - "comment": "Remove web endpoint" - } - ] - } - }, - { - "date": "Wed, 16 Feb 2022 21:58:21 GMT", - "tag": "@fluentui-react-native/use-slot_v0.2.5", - "version": "0.2.5", - "comments": { - "none": [ - { - "author": "ruaraki@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "fc506e0ea21def220cd2e7b4fe36361446c5f8c6", - "comment": "Update just" - } - ] - } - }, - { - "date": "Fri, 14 Jan 2022 01:00:04 GMT", - "tag": "@fluentui-react-native/use-slot_v0.2.5", - "version": "0.2.5", - "comments": { - "none": [ - { - "author": "sanajmi@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "163defba33412f6f44117750a540b8fdecd3bdca", - "comment": "Remove references to uifabricshared" - } - ] - } - }, - { - "date": "Tue, 21 Dec 2021 20:56:31 GMT", - "tag": "@fluentui-react-native/use-slot_v0.2.5", - "version": "0.2.5", - "comments": { - "patch": [ - { - "author": "ruaraki@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "3012e075ca89e36931357442679b73d20540af8b", - "comment": "Fix chevron on menu button" - } - ] - } - }, - { - "date": "Mon, 20 Dec 2021 22:56:01 GMT", - "tag": "@fluentui-react-native/use-slot_v0.2.4", - "version": "0.2.4", - "comments": { - "patch": [ - { - "author": "ruaraki@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "f4a5b4f5c9c190400e3e34c4a33b3ed30696d41e", - "comment": "Add repository property to all package.json files" - }, - { - "author": "beachball", - "package": "@fluentui-react-native/use-slot", - "comment": "Bump @fluentui-react-native/merge-props to v0.4.2", - "commit": "bc86b4fbf5f07c0bfbc950a1a99a3cf4b35807d1" - } - ] - } - }, - { - "date": "Sat, 18 Dec 2021 04:15:05 GMT", - "tag": "@fluentui-react-native/use-slot_v0.2.3", - "version": "0.2.3", - "comments": { - "patch": [ - { - "author": "afoxman@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "38fe241a385ecd1ce4e51b5bcba57a9dd0426789", - "comment": "Update to TypeScript 4.5.4." - }, - { - "author": "beachball", - "package": "@fluentui-react-native/use-slot", - "comment": "Bump @fluentui-react-native/merge-props to v0.4.1", - "commit": "38fe241a385ecd1ce4e51b5bcba57a9dd0426789" - } - ] - } - }, - { - "date": "Fri, 17 Dec 2021 22:06:58 GMT", - "tag": "@fluentui-react-native/use-slot_v0.2.2", - "version": "0.2.2", - "comments": { - "patch": [ - { - "author": "ruaraki@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "b9ea5eb1409f37305169f75398fa7f41229cefbd", - "comment": "Revert \"Remove content prop (#1257)\"" - } - ] - } - }, - { - "date": "Fri, 17 Dec 2021 19:53:21 GMT", - "tag": "@fluentui-react-native/use-slot_v0.2.1", - "version": "0.2.1", - "comments": { - "patch": [ - { - "author": "ruaraki@microsoft.com", - "package": "@fluentui-react-native/use-slot", - "commit": "3012e075ca89e36931357442679b73d20540af8b", - "comment": "Fix chevron on menu button" - } - ] - } - }, - { - "date": "Wed, 17 Nov 2021 19:28:07 GMT", - "tag": "@fluentui-react-native/use-slot_v0.2.0", - "version": "0.2.0", - "comments": { - "minor": [ - { - "comment": "Update to react-native 0.64", - "author": "afoxman@microsoft.com", - "commit": "f16d742b5c2d5b97e49ac1ef7b740cbf89fc42e6", - "package": "@fluentui-react-native/use-slot" - } - ], - "patch": [ - { - "comment": "Bump @fluentui-react-native/merge-props to v0.4.0", - "author": "afoxman@microsoft.com", - "commit": "f16d742b5c2d5b97e49ac1ef7b740cbf89fc42e6", - "package": "@fluentui-react-native/use-slot" - } - ] - } - }, - { - "date": "Thu, 11 Nov 2021 23:17:41 GMT", - "tag": "@fluentui-react-native/use-slot_v0.1.7", - "version": "0.1.7", - "comments": { - "none": [ - { - "comment": "Bump @fluentui-react-native/merge-props to v0.3.5", - "author": "ruaraki@microsoft.com", - "commit": "110fba2a1721a914501404f6040f07656307549e", - "package": "@fluentui-react-native/use-slot" - } - ] - } - }, - { - "date": "Mon, 25 Oct 2021 19:24:43 GMT", - "tag": "@fluentui-react-native/use-slot_v0.1.7", - "version": "0.1.7", - "comments": { - "patch": [ - { - "comment": "Use tslib where spreadArray is used", - "author": "ruaraki@microsoft.com", - "commit": "f6578742d6c957cf7e346db82f89171f80bedc3b", - "package": "@fluentui-react-native/use-slot" - } - ] - } - }, - { - "date": "Tue, 28 Sep 2021 21:08:25 GMT", - "tag": "@fluentui-react-native/use-slot_v0.1.6", - "version": "0.1.6", - "comments": { - "patch": [ - { - "comment": "Delete NativeButton", - "author": "67026167+chiuam@users.noreply.github.com", - "commit": "335503904094894a91e53c6de328bf13e13bcc1c", - "package": "@fluentui-react-native/use-slot" - } - ] - } - }, - { - "date": "Tue, 21 Sep 2021 19:08:16 GMT", - "tag": "@fluentui-react-native/use-slot_v0.1.5", - "version": "0.1.5", - "comments": { - "none": [ - { - "comment": "Run prettier", - "author": "ruaraki@microsoft.com", - "commit": "e52c9b3d031599af4f43c261cb0a0327bc68a5ab", - "package": "@fluentui-react-native/use-slot" - } - ] - } - }, - { - "date": "Tue, 14 Sep 2021 19:31:04 GMT", - "tag": "@fluentui-react-native/use-slot_v0.1.5", - "version": "0.1.5", - "comments": { - "none": [ - { - "comment": "Revert \"Bump Prettier and run it (#997)\"", - "author": "ruaraki@microsoft.com", - "commit": "c3eefc2cd6e8e23d64ca72460586f33120edc336", - "package": "@fluentui-react-native/use-slot" - } - ] - } - }, - { - "date": "Fri, 10 Sep 2021 21:30:12 GMT", - "tag": "@fluentui-react-native/use-slot_v0.1.5", - "version": "0.1.5", - "comments": { - "none": [ - { - "comment": "Run the prettier", - "author": "ruaraki@microsoft.com", - "commit": "7ac71c6add6f05230c649dad60b5f7442f08cbb3", - "package": "@fluentui-react-native/use-slot" - } - ] - } - }, - { - "date": "Sat, 07 Aug 2021 00:40:04 GMT", - "tag": "@fluentui-react-native/use-slot_v0.1.5", - "version": "0.1.5", - "comments": { - "patch": [ - { - "comment": "enable usePressableState with stock Pressable component", - "author": "jasonmo@microsoft.com", - "commit": "e506337e109ef24e675c2ac794179db86fcb7e86", - "package": "@fluentui-react-native/use-slot" - } - ], - "none": [ - { - "comment": "Bump @fluentui-react-native/merge-props to v0.3.3", - "author": "jasonmo@microsoft.com", - "commit": "e506337e109ef24e675c2ac794179db86fcb7e86", - "package": "@fluentui-react-native/use-slot" - } - ] - } - }, - { - "date": "Fri, 06 Aug 2021 00:36:02 GMT", - "tag": "@fluentui-react-native/use-slot_v0.1.4", - "version": "0.1.4", - "comments": { - "none": [ - { - "comment": "Remove api-extractor related files", - "author": "ruaraki@microsoft.com", - "commit": "56ccce452d0dbbd5c24f53ebdd467f2d01dda694", - "package": "@fluentui-react-native/use-slot" - } - ] - } - }, - { - "date": "Wed, 04 Aug 2021 06:26:25 GMT", - "tag": "@fluentui-react-native/use-slot_v0.1.4", - "version": "0.1.4", - "comments": { - "patch": [ - { - "comment": "radio on macOS", - "author": "67026167+chiuam@users.noreply.github.com", - "commit": "68dd61bd45ca8ffacc1580e468f3c01810e3093e", - "package": "@fluentui-react-native/use-slot" - } - ] - } - }, - { - "date": "Fri, 30 Jul 2021 06:26:34 GMT", - "tag": "@fluentui-react-native/use-slot_v0.1.3", - "version": "0.1.3", - "comments": { - "none": [ - { - "comment": "Bump @fluentui-react-native/merge-props to v0.3.2", - "author": "4123478+tido64@users.noreply.github.com", - "commit": "925cf4eb5fe6bfadf20208b5b3d3a4454fc5c9fa", - "package": "@fluentui-react-native/use-slot" - } - ] - } - }, - { - "date": "Tue, 27 Jul 2021 22:17:20 GMT", - "tag": "@fluentui-react-native/use-slot_v0.1.3", - "version": "0.1.3", - "comments": { - "patch": [ - { - "comment": "add compressible utility and a snapshot test / demo to framework", - "author": "jasonmo@microsoft.com", - "commit": "1cd8611e1e88848ab66f7c0e9113c946f968260c", - "package": "@fluentui-react-native/use-slot" - } - ] - } - }, - { - "date": "Tue, 27 Jul 2021 18:55:58 GMT", - "tag": "@fluentui-react-native/use-slot_v0.1.2", - "version": "0.1.2", - "comments": { - "none": [ - { - "comment": "Bump @fluentui-react-native/merge-props to v0.3.2", - "author": "4123478+tido64@users.noreply.github.com", - "commit": "07d04d166b13ff79e07a8c4cbc2e06afb1e4c3e6", - "package": "@fluentui-react-native/use-slot" - } - ] - } - }, - { - "date": "Mon, 26 Jul 2021 20:41:04 GMT", - "tag": "@fluentui-react-native/use-slot_v0.1.2", - "version": "0.1.2", - "comments": { - "patch": [ - { - "comment": "add new use-slot package, move framework to consume that package", - "author": "jasonmo@microsoft.com", - "commit": "ad02fbbe9eaa5fbe6ec6126ccb719a1280f108a6", - "package": "@fluentui-react-native/use-slot" - } - ] - } - } - ] -} diff --git a/packages/framework/use-slot/CHANGELOG.md b/packages/framework/use-slot/CHANGELOG.md deleted file mode 100644 index 8295ff27187..00000000000 --- a/packages/framework/use-slot/CHANGELOG.md +++ /dev/null @@ -1,358 +0,0 @@ -# Change Log - @fluentui-react-native/use-slot - -## 0.8.1 - -### Patch Changes - -- 6c2d6e6: Update packages to esm, with new builds with project references -- 6805bf5: Change base furn packages to stricter types -- Updated dependencies [6c2d6e6] -- Updated dependencies [6805bf5] - - @fluentui-react-native/framework-base@0.4.1 - -## 0.8.0 - -### Minor Changes - -- b5a686c: Switch packages to type module - -### Patch Changes - -- ce37a6d: Remove the final configuration package (moving it to scripts) and switch to running various package validation tasks from the root -- ad85949: Configuration and dependency changes -- f10715d: remove oxlint config package and move it into scripts -- 1598157: Switch tsconfig settings to more modern settings -- Updated dependencies [ce37a6d] -- Updated dependencies [b5a686c] -- Updated dependencies [ad85949] -- Updated dependencies [f10715d] -- Updated dependencies [1598157] - - @fluentui-react-native/framework-base@0.4.0 - -## 0.7.3 - -### Patch Changes - -- 9cf4444: Migrate from ESLint to oxlint -- Updated dependencies [9cf4444] - - @fluentui-react-native/framework-base@0.3.3 - -## 0.7.2 - -### Patch Changes - -- 8a7b549: Update to the latest rnx-kit versions, fix some typing issues, and correct the vscode settings -- Updated dependencies [8a7b549] - - @fluentui-react-native/framework-base@0.3.2 - -## 0.7.1 - -### Patch Changes - -- 0d6e9c1: chore: migrate to `oxfmt` -- ac6e7af: Ensure packages have a default export that references the typescript entrypoint and clean up build dependency ordering -- Updated dependencies [0d6e9c1] -- Updated dependencies [ac6e7af] - - @fluentui-react-native/framework-base@0.3.1 - -## 0.7.0 - -### Minor Changes - -- d7adbdd: # Migration from Beachball to Changesets - - This changeset represents the migration from Beachball to Changesets for version management and consolidates all changes from 440+ beachball change files that were in the `change/` directory. - - All 75 affected packages receive a minor version bump to acknowledge the accumulated changes from the beachball era. - - ## What Changed - - Going forward, all version management uses Changesets via `yarn changeset`. The following beachball infrastructure has been removed: - - - ❌ 440+ beachball change files from `change/` directory - - ❌ `beachball` package dependency - - ❌ Beachball scripts from `package.json` - - ❌ `beachball.config.js` configuration file - - ❌ Beachball publish steps from Azure Pipelines - - ## New Workflow - - ✅ **Create changes**: Run `yarn changeset` to document changes - ✅ **Version bump PRs**: Automatically created by GitHub Actions - ✅ **Publishing**: Handled by Azure Pipelines using `changeset publish` - ✅ **Validation**: CI validates changesets and blocks major version bumps - - For details, see `CHANGESETS_SETUP.md` and `CONTRIBUTING.md`. - -### Patch Changes - -- d1d8c26: We were pinning @types/react and react-native-macos via resolutions. This fixes that which uncovered a bunch of type inconsistencies that needed to be addressed. -- Updated dependencies [d7adbdd] -- Updated dependencies [d1d8c26] - - @fluentui-react-native/framework-base@0.3.0 - - - - - -## 0.6.12 - -Tue, 05 Aug 2025 18:50:39 GMT - -### Patches - -- move repo to pnpm, create babel and jest config packages, fix dependencies (jasonmo@microsoft.com) - -## 0.6.8 - -Tue, 22 Jul 2025 19:06:24 GMT - -### Patches - -- centralize jsx rendering functionality in framework-base (jasonmo@microsoft.com) - -## 0.6.7 - -Wed, 16 Jul 2025 20:06:46 GMT - -### Patches - -- create common framework-base package to share common utiltities more broadly (jasonmo@microsoft.com) - -## 0.6.6 - -Tue, 15 Jul 2025 23:26:52 GMT - -### Patches - -- fix: run beachball sync (sanajmi@microsoft.com) -- add react-native entrypoints that ensure metro targets TS files rather than JS files for bundling (jasonmo@microsoft.com) -- fix repo linting with new configs and version, fixing linting errors as well (jasonmo@microsoft.com) -- update builds to use node16 settings and modern export maps (jasonmo@microsoft.com) - -## 0.6.3 - -Thu, 10 Jul 2025 19:20:14 GMT - -### Patches - -- Add support for React Native 0.74 (4123478+tido64@users.noreply.github.com) - -## 0.6.2 - -Wed, 29 Jan 2025 23:43:52 GMT - -### Patches - -- remove enzyme tests (sanajmi@microsoft.com) - -## 0.6.0 - -Thu, 11 Apr 2024 18:08:40 GMT - -### Minor changes - -- Update to RN 0.73 (30809111+acoates-ms@users.noreply.github.com) - -## 0.5.2 - -Fri, 01 Mar 2024 20:39:08 GMT - -### Patches - -- Use workspace for version of local package (ruaraki@microsoft.com) - -## 0.5.1 - -Thu, 22 Feb 2024 23:27:44 GMT - -### Patches - -- Rework package json version notation (rofang@microsoft.com) - -## 0.5.0 - -Thu, 09 Nov 2023 02:39:11 GMT - -### Minor changes - -- Update to react-native 0.72 (30809111+acoates-ms@users.noreply.github.com) -- Bump @fluentui-react-native/merge-props to v0.8.0 - -## 0.4.2 - -Wed, 09 Aug 2023 22:02:15 GMT - -### Patches - -- Bump @fluentui-react-native/merge-props to v0.7.1 - -## 0.4.1 - -Mon, 12 Jun 2023 17:46:44 GMT - -### Patches - -- Bump @fluentui-react-native/merge-props to v0.7.0 - -## 0.4.0 - -Mon, 05 Jun 2023 19:26:25 GMT - -### Minor changes - -- Upgrade to React Native 0.71 (sanajmi@microsoft.com) -- Bump @fluentui-react-native/merge-props to v0.6.0 - -## 0.3.4 - -Fri, 24 Feb 2023 20:40:00 GMT - -### Patches - -- Order imports (78454019+lyzhan7@users.noreply.github.com) -- Bump @fluentui-react-native/merge-props to v0.5.3 - -## 0.3.3 - -Thu, 02 Feb 2023 01:29:39 GMT - -### Patches - -- Add eslint to packages missing a config (30809111+acoates-ms@users.noreply.github.com) - -## 0.3.2 - -Wed, 01 Feb 2023 22:33:17 GMT - -### Patches - -- Bump @fluentui-react-native/merge-props to v0.5.2 - -## 0.3.1 - -Fri, 30 Sep 2022 00:54:37 GMT - -### Patches - -- Update react-native to 0.68 (krsiler@microsoft.com) -- Bump @fluentui-react-native/merge-props to v0.5.1 - -## 0.3.0 - -Thu, 14 Jul 2022 18:09:50 GMT - -### Minor changes - -- Update to React Native 0.66 (sanajmi@microsoft.com) -- Bump @fluentui-react-native/merge-props to v0.5.0 - -## 0.2.6 - -Thu, 31 Mar 2022 07:27:48 GMT - -### Patches - -- Bump @fluentui-react-native/merge-props to v0.4.3 - -## 0.2.5 - -Tue, 21 Dec 2021 20:56:31 GMT - -### Patches - -- Fix chevron on menu button (ruaraki@microsoft.com) - -## 0.2.4 - -Mon, 20 Dec 2021 22:56:01 GMT - -### Patches - -- Add repository property to all package.json files (ruaraki@microsoft.com) -- Bump @fluentui-react-native/merge-props to v0.4.2 - -## 0.2.3 - -Sat, 18 Dec 2021 04:15:05 GMT - -### Patches - -- Update to TypeScript 4.5.4. (afoxman@microsoft.com) -- Bump @fluentui-react-native/merge-props to v0.4.1 - -## 0.2.2 - -Fri, 17 Dec 2021 22:06:58 GMT - -### Patches - -- Revert "Remove content prop (#1257)" (ruaraki@microsoft.com) - -## 0.2.1 - -Fri, 17 Dec 2021 19:53:21 GMT - -### Patches - -- Fix chevron on menu button (ruaraki@microsoft.com) - -## 0.2.0 - -Wed, 17 Nov 2021 19:28:07 GMT - -### Minor changes - -- Update to react-native 0.64 (afoxman@microsoft.com) - -### Patches - -- Bump @fluentui-react-native/merge-props to v0.4.0 (afoxman@microsoft.com) - -## 0.1.7 - -Mon, 25 Oct 2021 19:24:43 GMT - -### Patches - -- Use tslib where spreadArray is used (ruaraki@microsoft.com) - -## 0.1.6 - -Tue, 28 Sep 2021 21:08:25 GMT - -### Patches - -- Delete NativeButton (67026167+chiuam@users.noreply.github.com) - -## 0.1.5 - -Sat, 07 Aug 2021 00:40:04 GMT - -### Patches - -- enable usePressableState with stock Pressable component (jasonmo@microsoft.com) - -## 0.1.4 - -Wed, 04 Aug 2021 06:26:25 GMT - -### Patches - -- radio on macOS (67026167+chiuam@users.noreply.github.com) - -## 0.1.3 - -Tue, 27 Jul 2021 22:17:20 GMT - -### Patches - -- add compressible utility and a snapshot test / demo to framework (jasonmo@microsoft.com) - -## 0.1.2 - -Mon, 26 Jul 2021 20:41:04 GMT - -### Patches - -- add new use-slot package, move framework to consume that package (jasonmo@microsoft.com) diff --git a/packages/framework/use-slot/README.md b/packages/framework/use-slot/README.md deleted file mode 100644 index 38e0a66770e..00000000000 --- a/packages/framework/use-slot/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# @fluentui-react-native/use-slot - -A pattern and framework for building layering components together in an efficient, pluggable, and customizable manner. - -When building libraries of components, larger and more complex components are often built out of smaller and more focused components. This is great conceptually because it allows good separation of concerns and enables better and more modular designs. The boundaries in this layering are not only defined by the definition of your component, whether it be a function or a class based component, but by `React.createElement`. The `createElement` calls are what JSX is syntactic sugar for, and provides the layering for things like hooks. - -## Looking a bit more deeply at rendering JSX - -When the render tree is returned via `` etc, these calls are turned into calls to `React.createElement` - -- This creates an entry in the virtual DOM -- This entry has benefits such as allowing render passes to be initiated at that node -- It also effectively creates storage for hooks. Hooks are stored almost like stack frames for each component layer. This is why hooks cannot be conditional from render to render, it would cause stack misalignment. - -If instead a function based component is rendered via a direct function call, e.g. `return Text(textProps)` this entry will not be created. - -- This does not create a DOM entry, which saves overhead in the overall react tree -- This is only safe if the component does not use hooks and is a function component (not a class based one) - -There are pros and cons to calling `createElement`. In most cases it is safer to do so. But with certain component patterns, particularly simple wrapping, this creates unnecessary overhead and as HOCs are built up this can become substantial. The patterns in this package attempt to alleviate this by allowing components to be authored in a way that they can be compressed safely. - -## Staged Components - -To solve this reliably for function components it is necessary to separate the hook calls from element tree generation. The pattern in this package is to write render functions that return a continuation function that will return the JSX tree. This pattern is as follows: - -```tsx -const twoPartRender = (props: MyProps) => { - // do the hook calls in this section */ - const theme = React.useContext(ThemeContext); - const [state, setState] = React.useState(() => doSomeStateSetup())); - - // now return a (props) or (props, children) or (props, ...children) function that finishes the render - return (additional: MyProps, ...children: React.ReactNode[]) => { - const merged = { ...props, ...additional }; - return ({children}) - } -} -``` - -### `stagedComponent` - -This type of function is not recognizable on its own as a component. This package exports a helper function `stagedComponent` that both: - -- Turns a two part render function into a component that react can recognize -- Adds the means for the framework to access the initial two part function if it knows how to handle it - -The helper will return a `React.FunctionComponent` that will forward props (without children) to the first call, then pass children to the continuation function. - -## `useSlot` - -Consuming these by hand are a bit tedious. To aid in this the `useSlot` hook function is provided. Besides enabling automatic tree compression (for component types that support it), this also allows components to be authored as pluggable bits in the render tree. Usage looks something like: - -```ts -/** @jsxRuntime classic */ -/** @jsx withSlots */ - -const baseStyle = { - /* some text styling defaults set here */ -}; - -// prop type that adds the ability to swap out the internal Text for something else -type TextWithOverrideProps = TextProps & { override?: React.FunctionComponent }; - -const StyledText = (props: React.PropsWithChildren) => { - // split the children from props to forward them - const { children, override, ...rest } = props; - - // create a merged set of props. The mergeStyle utility here will avoid creating unnecessary permutations of styles - const mergedProps = { ...rest, style: mergeStyles(baseStyle, rest.style) }; - - // create a slot that can be used to render, props passed in here will be remembered in render. If override is set the slot will be changed, otherwise Text will be used - const InnerText = useSlot(override || Text, mergedProps); - - // now just render using that slot - return {children}; -}; -``` - -## `withSlots` - -The `withSlots` helper is required to render the slots correctly. The @jsx directive in the previous example is all that is required to make this work. This provides a helper to the @jsx processing utility that will render as a function if possible, or via createElement if not. diff --git a/packages/framework/use-slot/jest.config.cjs b/packages/framework/use-slot/jest.config.cjs deleted file mode 100644 index b391f5b6623..00000000000 --- a/packages/framework/use-slot/jest.config.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('@fluentui-react-native/scripts/jest-config'); diff --git a/packages/framework/use-slot/package.json b/packages/framework/use-slot/package.json deleted file mode 100644 index f419a0c5594..00000000000 --- a/packages/framework/use-slot/package.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "name": "@fluentui-react-native/use-slot", - "version": "0.8.1", - "description": "Hook function to use a component as a pluggable slot", - "keywords": [], - "license": "MIT", - "author": "", - "repository": { - "type": "git", - "url": "https://github.com/microsoft/fluentui-react-native", - "directory": "packages/framework/use-slot" - }, - "type": "module", - "main": "./lib/index.js", - "module": "./lib/index.js", - "types": "lib/index.d.ts", - "exports": { - ".": { - "types": "./lib/index.d.ts", - "react-native": "./src/index.ts", - "import": "./lib/index.js", - "default": "./src/index.ts" - } - }, - "scripts": { - "build": "tsgo -b", - "clean": "fluentui-scripts clean", - "depcheck": "fluentui-scripts depcheck", - "format": "fluentui-scripts format", - "lint": "fluentui-scripts lint", - "start": "fluentui-scripts dev", - "start-test": "fluentui-scripts jest-watch", - "test": "fluentui-scripts jest", - "update-snapshots": "fluentui-scripts jest -u" - }, - "dependencies": { - "@fluentui-react-native/framework-base": "workspace:*" - }, - "devDependencies": { - "@babel/core": "catalog:", - "@fluentui-react-native/scripts": "workspace:*", - "@react-native-community/cli": "^20.0.0", - "@react-native-community/cli-platform-android": "^20.0.0", - "@react-native-community/cli-platform-ios": "^20.0.0", - "@react-native/babel-preset": "^0.81.0", - "@react-native/metro-config": "^0.81.0", - "@types/jest": "^29.0.0", - "@types/react": "~19.1.4", - "@types/react-test-renderer": "^19.1.0", - "react": "19.1.4", - "react-native": "^0.81.6", - "react-test-renderer": "19.1.4" - }, - "peerDependencies": { - "@types/react": "~18.2.0 || ~19.0.0 || ~19.1.4", - "react": "18.2.0 || 19.0.0 || 19.1.4" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - }, - "furn": { - "jestPlatform": "android" - }, - "rnx-kit": { - "kitType": "library", - "alignDeps": { - "capabilities": [ - "core-android-dev-only", - "core-dev-only", - "core-ios-dev-only", - "react", - "react-test-renderer" - ] - }, - "extends": "@fluentui-react-native/scripts/kit-config" - } -} diff --git a/packages/framework/use-slot/src/index.ts b/packages/framework/use-slot/src/index.ts deleted file mode 100644 index d32ca386540..00000000000 --- a/packages/framework/use-slot/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { useSlot } from './useSlot.ts'; -export type { ComponentType } from './useSlot.ts'; - -// 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'; -export type { ComposableFunction, FinalRender, NativeReactType, SlotFn, StagedRender } from '@fluentui-react-native/framework-base'; diff --git a/packages/framework/use-slot/src/useSlot.ts b/packages/framework/use-slot/src/useSlot.ts deleted file mode 100644 index bbd71ec1bc8..00000000000 --- a/packages/framework/use-slot/src/useSlot.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as React from 'react'; - -import { mergeProps, getPhasedRender, directComponent, renderForJsxRuntime, filterProps } from '@fluentui-react-native/framework-base'; -import type { PropsFilter, FunctionComponent } from '@fluentui-react-native/framework-base'; - -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 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, - 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 - 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); - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [component, filter, slotData], - ); -} diff --git a/packages/framework/use-slot/tsconfig.json b/packages/framework/use-slot/tsconfig.json deleted file mode 100644 index e195da31c23..00000000000 --- a/packages/framework/use-slot/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "@fluentui-react-native/scripts/tsconfig-strict", - "compilerOptions": { - "outDir": "lib", - "rootDir": "src", - "composite": true, - "tsBuildInfoFile": ".cache/tsconfig.tsbuildinfo" - }, - "include": ["src"], - "references": [ - { - "path": "../../framework-base/tsconfig.json" - }, - { - "path": "../../../scripts/tsconfig.json" - } - ] -} diff --git a/packages/framework/use-slots/package.json b/packages/framework/use-slots/package.json index 5080cc5e932..0f08dd715ea 100644 --- a/packages/framework/use-slots/package.json +++ b/packages/framework/use-slots/package.json @@ -34,8 +34,7 @@ "update-snapshots": "fluentui-scripts jest -u" }, "dependencies": { - "@fluentui-react-native/framework-base": "workspace:*", - "@fluentui-react-native/use-slot": "workspace:*" + "@fluentui-react-native/framework-base": "workspace:*" }, "devDependencies": { "@babel/core": "catalog:", diff --git a/packages/framework/use-slots/src/buildUseSlots.test.tsx b/packages/framework/use-slots/src/buildUseSlots.test.tsx index e76f0c78d26..ddd866b58fe 100644 --- a/packages/framework/use-slots/src/buildUseSlots.test.tsx +++ b/packages/framework/use-slots/src/buildUseSlots.test.tsx @@ -6,7 +6,7 @@ import { View, Text } from 'react-native'; import { stagedComponent } from '@fluentui-react-native/framework-base'; import * as renderer from 'react-test-renderer'; -import { buildUseSlots } from './buildUseSlots.ts'; +import { buildUseSlots } from './buildUseSlots'; type SlotProps1 = { outer: ViewProps; diff --git a/packages/framework/use-slots/src/buildUseSlots.ts b/packages/framework/use-slots/src/buildUseSlots.ts index 8a7cdfcd74d..ef29403bf26 100644 --- a/packages/framework/use-slots/src/buildUseSlots.ts +++ b/packages/framework/use-slots/src/buildUseSlots.ts @@ -1,5 +1,6 @@ -import { useSlot, type ComponentType } from '@fluentui-react-native/use-slot'; -import type { FunctionComponent, PropsFilter } from '@fluentui-react-native/framework-base'; +import React from 'react'; +import { useSlot } from '@fluentui-react-native/framework-base'; +import type { FunctionComponent, PropsTransform } from '@fluentui-react-native/framework-base'; // type AsObject = T extends object ? T : never @@ -11,8 +12,8 @@ type UseStyling = (...props: unknown[]) => TSlotProps; export type Slots = { [K in keyof TSlotProps]: FunctionComponent }; export type UseSlotOptions = { - slots: { [K in keyof TSlotProps]: ComponentType }; - filters?: { [K in keyof TSlotProps]?: PropsFilter }; + slots: { [K in keyof TSlotProps]: React.ComponentType }; + filters?: { [K in keyof TSlotProps]?: PropsTransform }; useStyling?: TSlotProps | GetSlotProps; }; diff --git a/packages/framework/use-slots/src/index.ts b/packages/framework/use-slots/src/index.ts index 801bb75b002..d4631f26a25 100644 --- a/packages/framework/use-slots/src/index.ts +++ b/packages/framework/use-slots/src/index.ts @@ -1,2 +1,2 @@ -export { buildUseSlots } from './buildUseSlots.ts'; -export type { GetSlotProps, Slots, UseSlotOptions, UseSlotsBase } from './buildUseSlots.ts'; +export { buildUseSlots } from './buildUseSlots'; +export type { GetSlotProps, Slots, UseSlotOptions, UseSlotsBase } from './buildUseSlots'; diff --git a/packages/framework/use-slots/src/useSlots.samples.test.tsx b/packages/framework/use-slots/src/useSlots.samples.test.tsx index 7716e5f963c..a3e005f402d 100644 --- a/packages/framework/use-slots/src/useSlots.samples.test.tsx +++ b/packages/framework/use-slots/src/useSlots.samples.test.tsx @@ -6,7 +6,7 @@ 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.ts'; +import { buildUseSlots } from './buildUseSlots'; /** * This file contains samples and description to help explain what the useSlots hook does and why it is useful @@ -136,7 +136,8 @@ describe('useSlots sample code test suite', () => { * inner closure directly, without going through createElement. Entries passed into the JSX, including children, are what appear in the * props of the inner closure. (In this example `extra`) * - * NOTE: this requires using the withSlots helper via the jsx directive. This knows how to pick apart the entries and just call the second + * NOTE: this requires opting into the custom JSX runtime via the `@jsxImportSource @fluentui-react-native/framework-base` + * pragma at the top of the file. This knows how to pick apart the entries and just call the second * part of the function */ return {children}; diff --git a/packages/framework/use-slots/tsconfig.json b/packages/framework/use-slots/tsconfig.json index 8b3713018f7..256f00585b8 100644 --- a/packages/framework/use-slots/tsconfig.json +++ b/packages/framework/use-slots/tsconfig.json @@ -13,9 +13,6 @@ }, { "path": "../../../scripts/tsconfig.json" - }, - { - "path": "../use-slot/tsconfig.json" } ] } diff --git a/packages/framework/use-styling/src/buildProps.test.ts b/packages/framework/use-styling/src/buildProps.test.ts index 4fe34336a5a..30d65395696 100644 --- a/packages/framework/use-styling/src/buildProps.test.ts +++ b/packages/framework/use-styling/src/buildProps.test.ts @@ -1,6 +1,6 @@ import { getMemoCache } from '@fluentui-react-native/framework-base'; -import { buildProps } from './buildProps.ts'; +import { buildProps } from './buildProps'; type ITheme = { foo?: string; bar?: string }; type ITokens = { a?: string; b?: string; c?: string; d?: string }; diff --git a/packages/framework/use-styling/src/buildUseStyling.test.ts b/packages/framework/use-styling/src/buildUseStyling.test.ts index 46e6f002f43..bf5552196a2 100644 --- a/packages/framework/use-styling/src/buildUseStyling.test.ts +++ b/packages/framework/use-styling/src/buildUseStyling.test.ts @@ -1,8 +1,8 @@ import { getMemoCache } from '@fluentui-react-native/framework-base'; -import { buildProps } from './buildProps.ts'; -import type { ThemeHelper, UseStylingOptions } from './buildUseStyling.ts'; -import { buildUseStyling } from './buildUseStyling.ts'; +import { buildProps } from './buildProps'; +import type { ThemeHelper, UseStylingOptions } from './buildUseStyling'; +import { buildUseStyling } from './buildUseStyling'; let lastInstance = 0; diff --git a/packages/framework/use-styling/src/buildUseStyling.ts b/packages/framework/use-styling/src/buildUseStyling.ts index 941b5485b1c..d10dab20c5f 100644 --- a/packages/framework/use-styling/src/buildUseStyling.ts +++ b/packages/framework/use-styling/src/buildUseStyling.ts @@ -2,8 +2,8 @@ import type { GetTypedMemoValue } from '@fluentui-react-native/framework-base'; import type { HasLayer, TokenSettings } from '@fluentui-react-native/use-tokens'; import { applyPropsToTokens, applyTokenLayers, buildUseTokens } from '@fluentui-react-native/use-tokens'; -import type { TokensThatAreAlsoProps, BuildSlotProps } from './buildProps.ts'; -import { refinePropsFunctions } from './buildProps.ts'; +import type { TokensThatAreAlsoProps, BuildSlotProps } from './buildProps'; +import { refinePropsFunctions } from './buildProps'; /** * Options used to build up a useStyling hook diff --git a/packages/framework/use-styling/src/index.ts b/packages/framework/use-styling/src/index.ts index d63463a4fa5..c71670f70ef 100644 --- a/packages/framework/use-styling/src/index.ts +++ b/packages/framework/use-styling/src/index.ts @@ -1,6 +1,6 @@ -export { buildUseStyling } from './buildUseStyling.ts'; -export type { ThemeHelper, UseStyling, UseStylingOptions } from './buildUseStyling.ts'; -export { buildProps, refinePropsFunctions } from './buildProps.ts'; -export type { BuildPropsBase, BuildSlotProps, RefinableBuildPropsBase, RefineFunctionBase, TokensThatAreAlsoProps } from './buildProps.ts'; +export { buildUseStyling } from './buildUseStyling'; +export type { ThemeHelper, UseStyling, UseStylingOptions } from './buildUseStyling'; +export { buildProps, refinePropsFunctions } from './buildProps'; +export type { BuildPropsBase, BuildSlotProps, RefinableBuildPropsBase, RefineFunctionBase, TokensThatAreAlsoProps } from './buildProps'; export type { HasLayer, TokenSettings, TokensFromTheme } from '@fluentui-react-native/use-tokens'; export { applyTokenLayers } from '@fluentui-react-native/use-tokens'; diff --git a/packages/framework/use-styling/src/useStyling.samples.test.tsx b/packages/framework/use-styling/src/useStyling.samples.test.tsx index 702516591d3..8b9e557965c 100644 --- a/packages/framework/use-styling/src/useStyling.samples.test.tsx +++ b/packages/framework/use-styling/src/useStyling.samples.test.tsx @@ -5,9 +5,9 @@ import { Text, View } from 'react-native'; import * as renderer from 'react-test-renderer'; -import { buildProps } from './buildProps.ts'; -import type { ThemeHelper, UseStylingOptions } from './buildUseStyling.ts'; -import { buildUseStyling } from './buildUseStyling.ts'; +import { buildProps } from './buildProps'; +import type { ThemeHelper, UseStylingOptions } from './buildUseStyling'; +import { buildUseStyling } from './buildUseStyling'; /** * Sample super simple theming implementation, shared by all the samples. This is intended to be illustrative, diff --git a/packages/framework/use-tokens/src/applyPropsToTokens.test.ts b/packages/framework/use-tokens/src/applyPropsToTokens.test.ts index d03b29c9a3b..3e209bf6115 100644 --- a/packages/framework/use-tokens/src/applyPropsToTokens.test.ts +++ b/packages/framework/use-tokens/src/applyPropsToTokens.test.ts @@ -1,6 +1,6 @@ import { getTypedMemoCache } from '@fluentui-react-native/framework-base'; -import { applyPropsToTokens } from './applyPropsToTokens.ts'; +import { applyPropsToTokens } from './applyPropsToTokens'; interface Tokens { uno?: string; diff --git a/packages/framework/use-tokens/src/applyTokenLayers.test.ts b/packages/framework/use-tokens/src/applyTokenLayers.test.ts index bdbb3672bcd..c957a7afbb8 100644 --- a/packages/framework/use-tokens/src/applyTokenLayers.test.ts +++ b/packages/framework/use-tokens/src/applyTokenLayers.test.ts @@ -1,6 +1,6 @@ import { getMemoCache } from '@fluentui-react-native/framework-base'; -import { applyTokenLayers } from './applyTokenLayers.ts'; +import { applyTokenLayers } from './applyTokenLayers'; type Tokens = { a?: string; diff --git a/packages/framework/use-tokens/src/buildUseTokens.test.ts b/packages/framework/use-tokens/src/buildUseTokens.test.ts index 2010809f117..6fdfcfc9f01 100644 --- a/packages/framework/use-tokens/src/buildUseTokens.test.ts +++ b/packages/framework/use-tokens/src/buildUseTokens.test.ts @@ -1,4 +1,4 @@ -import { buildUseTokens } from './buildUseTokens.ts'; +import { buildUseTokens } from './buildUseTokens'; interface Tokens { a?: string; diff --git a/packages/framework/use-tokens/src/customizable.ts b/packages/framework/use-tokens/src/customizable.ts index 59378bf15c2..31e1e1b9e02 100644 --- a/packages/framework/use-tokens/src/customizable.ts +++ b/packages/framework/use-tokens/src/customizable.ts @@ -1,6 +1,6 @@ import type React from 'react'; -import type { TokenSettings, UseTokens } from './buildUseTokens.ts'; +import type { TokenSettings, UseTokens } from './buildUseTokens'; import type { FurnJSX } from '@fluentui-react-native/framework-base'; /** diff --git a/packages/framework/use-tokens/src/index.ts b/packages/framework/use-tokens/src/index.ts index 3c22100eb00..71d47668a65 100644 --- a/packages/framework/use-tokens/src/index.ts +++ b/packages/framework/use-tokens/src/index.ts @@ -1,8 +1,8 @@ -export { applyPropsToTokens } from './applyPropsToTokens.ts'; -export { applyTokenLayers } from './applyTokenLayers.ts'; -export type { HasLayer } from './applyTokenLayers.ts'; -export { buildUseTokens } from './buildUseTokens.ts'; -export type { GetComponentInfo, TokenSettings, TokensFromTheme, UseTokens, UseTokensCore } from './buildUseTokens.ts'; -export { customizable } from './customizable.ts'; -export type { CustomizableComponent, InjectableComponent } from './customizable.ts'; -export { patchTokens } from './patchTokens.ts'; +export { applyPropsToTokens } from './applyPropsToTokens'; +export { applyTokenLayers } from './applyTokenLayers'; +export type { HasLayer } from './applyTokenLayers'; +export { buildUseTokens } from './buildUseTokens'; +export type { GetComponentInfo, TokenSettings, TokensFromTheme, UseTokens, UseTokensCore } from './buildUseTokens'; +export { customizable } from './customizable'; +export type { CustomizableComponent, InjectableComponent } from './customizable'; +export { patchTokens } from './patchTokens'; diff --git a/packages/framework/use-tokens/src/patchTokens.test.ts b/packages/framework/use-tokens/src/patchTokens.test.ts index 9174164d3d4..8a333e4ddd1 100644 --- a/packages/framework/use-tokens/src/patchTokens.test.ts +++ b/packages/framework/use-tokens/src/patchTokens.test.ts @@ -1,6 +1,6 @@ import { getMemoCache } from '@fluentui-react-native/framework-base'; -import { patchTokens } from './patchTokens.ts'; +import { patchTokens } from './patchTokens'; interface Tokens { uno?: string; diff --git a/packages/framework/use-tokens/src/useTokens.samples.test.tsx b/packages/framework/use-tokens/src/useTokens.samples.test.tsx index 521d12fb4c7..a7bdfd1961e 100644 --- a/packages/framework/use-tokens/src/useTokens.samples.test.tsx +++ b/packages/framework/use-tokens/src/useTokens.samples.test.tsx @@ -7,7 +7,7 @@ import { immutableMerge } from '@fluentui-react-native/framework-base'; import { mergeStyles } from '@fluentui-react-native/framework-base'; import * as renderer from 'react-test-renderer'; -import { buildUseTokens } from './buildUseTokens.ts'; +import { buildUseTokens } from './buildUseTokens'; /** * Sample super simple theming implementation, shared by all the samples. This is intended to be illustrative, diff --git a/packages/utils/adapters/src/filterProps.ts b/packages/utils/adapters/src/filterProps.ts index ae79d100976..486db3e1fa1 100644 --- a/packages/utils/adapters/src/filterProps.ts +++ b/packages/utils/adapters/src/filterProps.ts @@ -1,42 +1,42 @@ import { getViewMask, getTextMask, getImageMask } from './filters'; /** - * Filters props based on the provided mask. Each filter function is memoized to only compute the mask once, - * as it doesn't change during the lifecycle of the app. + * Prop transform functions that filter a set of props down to only those valid for the target native + * component (View, Text, or Image). They take a props object and return a filtered props object, + * matching the PropsTransform shape used by the slot/composition frameworks. Each mask is computed once + * and memoized as it does not change during the lifecycle of the app. + * + * If no props need to be removed the original object is returned to preserve referential identity. */ +function createPropsMaskTransform(getMask: () => Record) { + let mask: Record | undefined; + return (props: T): T => { + if (!props || typeof props !== 'object') { + return props; + } + mask ??= getMask(); + let result: T | undefined; + for (const key of Object.keys(props)) { + if (!mask[key]) { + result ??= { ...props }; + delete (result as Record)[key]; + } + } + return result ?? props; + }; +} /** - * Filter view props - * @param propName - The name of the prop to check against the view mask + * Filter a set of props down to only valid View props. */ -export const filterViewProps = (() => { - let viewMask: Record | undefined; - return (propName: string): boolean => { - viewMask ??= getViewMask(); - return Boolean(viewMask[propName]); - }; -})(); +export const filterViewProps = createPropsMaskTransform(getViewMask); /** - * Filter text props - * @param propName - The name of the prop to check against the text mask + * Filter a set of props down to only valid Text props. */ -export const filterTextProps = (() => { - let textMask: Record | undefined; - return (propName: string): boolean => { - textMask ??= getTextMask(); - return Boolean(textMask[propName]); - }; -})(); +export const filterTextProps = createPropsMaskTransform(getTextMask); /** - * Filter image props - * @param propName - The name of the prop to check against the image mask + * Filter a set of props down to only valid Image props. */ -export const filterImageProps = (() => { - let imageMask: Record | undefined; - return (propName: string): boolean => { - imageMask ??= getImageMask(); - return Boolean(imageMask[propName]); - }; -})(); +export const filterImageProps = createPropsMaskTransform(getImageMask); diff --git a/scripts/configs/tsconfig/tsconfig.json b/scripts/configs/tsconfig/tsconfig.json index f3338475a01..e9a3eff654a 100644 --- a/scripts/configs/tsconfig/tsconfig.json +++ b/scripts/configs/tsconfig/tsconfig.json @@ -3,7 +3,6 @@ "compilerOptions": { "strictBindCallApply": false, "strictNullChecks": false, - "noImplicitAny": false, - "rewriteRelativeImportExtensions": true + "noImplicitAny": false } } diff --git a/scripts/configs/tsconfig/tsconfig.strict.json b/scripts/configs/tsconfig/tsconfig.strict.json index fcbe913550b..aa0ee07290b 100644 --- a/scripts/configs/tsconfig/tsconfig.strict.json +++ b/scripts/configs/tsconfig/tsconfig.strict.json @@ -6,7 +6,7 @@ "module": "esnext", "moduleResolution": "bundler", "outDir": "lib", - "rewriteRelativeImportExtensions": true, + "rewriteRelativeImportExtensions": false, "jsx": "react-jsx", "types": ["jest", "node"] } diff --git a/scripts/src/worker/test-links.mts b/scripts/src/worker/test-links.mts index 027693d1cd4..6f30fc8b57a 100644 --- a/scripts/src/worker/test-links.mts +++ b/scripts/src/worker/test-links.mts @@ -27,7 +27,7 @@ import { dirname, join, relative, resolve } from 'node:path'; const CONFIG_PATH = '.github/markdown-link-check-config.json'; // Directories that never contain checkable markdown and would only slow the walk. -const SKIP_DIRS = new Set(['node_modules', 'lib', 'lib-commonjs', 'dist', '.git']); +const SKIP_DIRS = new Set(['node_modules', 'lib', 'lib-commonjs', 'dist', '.git', 'Pods']); /** * Resolve the set of workspace directories from the root package.json's diff --git a/tsconfig.json b/tsconfig.json index 383b9005738..9a4bab2e810 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -187,9 +187,6 @@ { "path": "packages/framework/themed-stylesheet/tsconfig.json" }, - { - "path": "packages/framework/use-slot/tsconfig.json" - }, { "path": "packages/framework/use-slots/tsconfig.json" }, diff --git a/yarn.lock b/yarn.lock index 88dfb7850ef..0d61ed1d305 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2886,6 +2886,7 @@ __metadata: resolution: "@fluentui-react-native/agentic-components@workspace:packages/agentic-components" dependencies: "@babel/core": "catalog:" + "@fluentui-react-native/framework-base": "workspace:*" "@fluentui-react-native/scripts": "workspace:*" "@react-native-community/cli": "npm:^20.0.0" "@react-native-community/cli-platform-android": "npm:^20.0.0" @@ -3946,7 +3947,7 @@ __metadata: resolution: "@fluentui-react-native/experimental-appearance-additions@workspace:packages/experimental/AppearanceAdditions" dependencies: "@babel/core": "catalog:" - "@fluentui-react-native/framework": "workspace:*" + "@fluentui-react-native/framework-base": "workspace:*" "@fluentui-react-native/scripts": "workspace:*" "@react-native-community/cli": "npm:^20.0.0" "@react-native-community/cli-platform-android": "npm:^20.0.0" @@ -4364,11 +4365,18 @@ __metadata: dependencies: "@babel/core": "catalog:" "@fluentui-react-native/scripts": "workspace:*" + "@react-native-community/cli": "npm:^20.0.0" + "@react-native-community/cli-platform-android": "npm:^20.0.0" + "@react-native-community/cli-platform-ios": "npm:^20.0.0" + "@react-native/babel-preset": "npm:^0.81.0" + "@react-native/metro-config": "npm:^0.81.0" "@types/jest": "npm:^29.0.0" "@types/node": "catalog:" "@types/react": "npm:~19.1.4" + "@types/react-test-renderer": "npm:^19.1.0" react: "npm:19.1.4" react-native: "npm:^0.81.6" + react-test-renderer: "npm:19.1.4" peerDependencies: "@types/react": ~18.2.0 || ~19.0.0 || ~19.1.4 react: 18.2.0 || 19.0.0 || 19.1.4 @@ -4389,7 +4397,6 @@ __metadata: "@fluentui-react-native/scripts": "workspace:*" "@fluentui-react-native/theme-types": "workspace:*" "@fluentui-react-native/tokens": "workspace:*" - "@fluentui-react-native/use-slot": "workspace:*" "@fluentui-react-native/use-slots": "workspace:*" "@fluentui-react-native/use-styling": "workspace:*" "@fluentui-react-native/use-tokens": "workspace:*" @@ -4774,7 +4781,6 @@ __metadata: dependencies: "@babel/core": "catalog:" "@fluentui-react-native/button": "workspace:*" - "@fluentui-react-native/framework": "workspace:*" "@fluentui-react-native/framework-base": "workspace:*" "@fluentui-react-native/menu": "workspace:*" "@fluentui-react-native/scripts": "workspace:*" @@ -5900,33 +5906,6 @@ __metadata: languageName: unknown linkType: soft -"@fluentui-react-native/use-slot@workspace:*, @fluentui-react-native/use-slot@workspace:packages/framework/use-slot": - version: 0.0.0-use.local - resolution: "@fluentui-react-native/use-slot@workspace:packages/framework/use-slot" - dependencies: - "@babel/core": "catalog:" - "@fluentui-react-native/framework-base": "workspace:*" - "@fluentui-react-native/scripts": "workspace:*" - "@react-native-community/cli": "npm:^20.0.0" - "@react-native-community/cli-platform-android": "npm:^20.0.0" - "@react-native-community/cli-platform-ios": "npm:^20.0.0" - "@react-native/babel-preset": "npm:^0.81.0" - "@react-native/metro-config": "npm:^0.81.0" - "@types/jest": "npm:^29.0.0" - "@types/react": "npm:~19.1.4" - "@types/react-test-renderer": "npm:^19.1.0" - react: "npm:19.1.4" - react-native: "npm:^0.81.6" - react-test-renderer: "npm:19.1.4" - peerDependencies: - "@types/react": ~18.2.0 || ~19.0.0 || ~19.1.4 - react: 18.2.0 || 19.0.0 || 19.1.4 - peerDependenciesMeta: - "@types/react": - optional: true - languageName: unknown - linkType: soft - "@fluentui-react-native/use-slots@workspace:*, @fluentui-react-native/use-slots@workspace:packages/framework/use-slots": version: 0.0.0-use.local resolution: "@fluentui-react-native/use-slots@workspace:packages/framework/use-slots" @@ -5934,7 +5913,6 @@ __metadata: "@babel/core": "catalog:" "@fluentui-react-native/framework-base": "workspace:*" "@fluentui-react-native/scripts": "workspace:*" - "@fluentui-react-native/use-slot": "workspace:*" "@react-native-community/cli": "npm:^20.0.0" "@react-native-community/cli-platform-android": "npm:^20.0.0" "@react-native-community/cli-platform-ios": "npm:^20.0.0"