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