diff --git a/.changeset/nice-stamps-win.md b/.changeset/nice-stamps-win.md new file mode 100644 index 0000000000..ca22c6dd2a --- /dev/null +++ b/.changeset/nice-stamps-win.md @@ -0,0 +1,11 @@ +--- +"@fluentui-react-native/interactive-hooks": patch +"@fluentui-react-native/use-slot": patch +"@fluentui-react-native/button": patch +"@fluentui-react-native/switch": patch +"@fluentui-react-native/chip": patch +"@fluentui-react-native/framework-base": patch +"@fluentui-react-native/adapters": patch +--- + +Change base furn packages to stricter types diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 8976dc9d51..37568cce15 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -4,6 +4,10 @@ on: pull_request permissions: contents: read +env: + VisualStudioVersion: '17.0' + WindowsTargetPlatformVersion: '10.0.26100.0' + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -162,7 +166,7 @@ jobs: windows: name: Windows PR - runs-on: windows-latest + runs-on: windows-2022 timeout-minutes: 60 steps: - name: Checkout @@ -212,7 +216,7 @@ jobs: working-directory: apps/fluent-tester - name: Run E2E RNW tests - shell: pwsh + shell: powershell run: | .\launchE2EWindows.ps1 -enableDumpFiles working-directory: apps/E2E diff --git a/.yarnrc.yml b/.yarnrc.yml index 24e7f3c211..43a93b4fc9 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -24,7 +24,7 @@ catalog: "@rnx-kit/oxlint-config": ^1.0.3 "@rnx-kit/reporter": ^0.2.0 "@rnx-kit/tools-packages": ^0.1.3 - "@rnx-kit/tools-react-native": ^2.3.7 + "@rnx-kit/tools-react-native": ^2.3.8 "@rnx-kit/tools-typescript": ^0.2.0 "@rnx-kit/tools-workspaces": ^0.2.3 "@rnx-kit/tsconfig": ^3.0.3 @@ -51,6 +51,7 @@ catalog: knip: ^6.12.1 lage: ^2.14.19 oxc-resolver: ^11.17.0 + react-native-test-app: ^5.3.3 rimraf: ^6.1.2 webdriverio: ^9.24.0 diff --git a/apps/fluent-tester/ios/Podfile.lock b/apps/fluent-tester/ios/Podfile.lock index 2e677092e2..d667ad465d 100644 --- a/apps/fluent-tester/ios/Podfile.lock +++ b/apps/fluent-tester/ios/Podfile.lock @@ -4,15 +4,15 @@ PODS: - fast_float (8.0.0) - FBLazyVector (0.81.6) - fmt (11.0.2) - - FRNAppearanceAdditions (0.8.3): + - FRNAppearanceAdditions (0.8.4): - React - - FRNAvatar (0.22.3): + - FRNAvatar (0.22.4): - MicrosoftFluentUI (= 0.13.1) - React - - FRNDatePicker (0.12.3): + - FRNDatePicker (0.12.4): - MicrosoftFluentUI (= 0.13.1) - React - - FRNFontMetrics (0.7.3): + - FRNFontMetrics (0.7.4): - React - glog (0.3.5) - hermes-engine (0.81.6): @@ -2424,7 +2424,7 @@ PODS: - React-perflogger (= 0.81.6) - React-utils (= 0.81.6) - SocketRocket - - ReactNativeHost (0.5.17): + - ReactNativeHost (0.5.19): - boost - DoubleConversion - fast_float @@ -2453,7 +2453,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - ReactTestApp-DevSupport (5.1.10): + - ReactTestApp-DevSupport (5.3.3): - React-Core - React-jsi - ReactTestApp-Resources (1.0.0-dev) @@ -2538,7 +2538,7 @@ DEPENDENCIES: - ReactAppDependencyProvider (from `build/generated/ios`) - ReactCodegen (from `build/generated/ios`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - - "ReactNativeHost (from `../../../node_modules/.store/react-native-test-app-virtual-d4246a9700/node_modules/@rnx-kit/react-native-host`)" + - "ReactNativeHost (from `../../../node_modules/.store/react-native-test-app-virtual-6d85e861d1/node_modules/@rnx-kit/react-native-host`)" - ReactTestApp-DevSupport (from `../node_modules/react-native-test-app`) - ReactTestApp-Resources (from `..`) - RNSVG (from `../node_modules/react-native-svg`) @@ -2701,7 +2701,7 @@ EXTERNAL SOURCES: ReactCommon: :path: "../node_modules/react-native/ReactCommon" ReactNativeHost: - :path: "../../../node_modules/.store/react-native-test-app-virtual-d4246a9700/node_modules/@rnx-kit/react-native-host" + :path: "../../../node_modules/.store/react-native-test-app-virtual-6d85e861d1/node_modules/@rnx-kit/react-native-host" ReactTestApp-DevSupport: :path: "../node_modules/react-native-test-app" ReactTestApp-Resources: @@ -2713,15 +2713,15 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb + DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5 fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 FBLazyVector: 812782f0506ba4c4a1b9f4b793e6de1bfdfc4c25 fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd - FRNAppearanceAdditions: 60f9fb4552fb3d92a63df00aa8f76fd6e8efb942 - FRNAvatar: ffa0facb219806b0e5083d2388bc61e590f5df19 - FRNDatePicker: 312243694338f941080b15f6fac4d0dac8f54137 - FRNFontMetrics: 363f87c2aee015325a399388dda266ed8f1f65a0 - glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 + FRNAppearanceAdditions: 58fe872ed668bac5b1f6b3f81b61c7f9fccd6788 + FRNAvatar: 586585a440e2b23c9ac872fd1fa2ddb7819cd7bd + FRNDatePicker: 6cf33c0f00f68606e2e301689670a7f90a9e5f37 + FRNFontMetrics: dba3c15a2feef87ee987d7c502cc3e2ba15adb5b + glog: fdfdfe5479092de0c4bdbebedd9056951f092c4f hermes-engine: 7219f6e751ad6ec7f3d7ec121830ee34dae40749 MicrosoftFluentUI: dde98d8ed3fc306d9ddd0a6f0bc0c1f24fe5275e RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 @@ -2787,13 +2787,13 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 50d5506ddd091d4ed24b88b661ce9a0e17347357 ReactCodegen: dae737e6acce32f4e6dda1c5c7ad0e7fd7235b71 ReactCommon: 8fafefb37d562f9e4bbbc9081ca4501f178a97ec - ReactNativeHost: 66a23d7106b451f1a1087f565a45c839abef1e73 - ReactTestApp-DevSupport: 2852f266ad4b393124413d4290c16aff35892cf4 + ReactNativeHost: 8680b5bbc3afdbfeccc206ed0f3230d7c3df691b + ReactTestApp-DevSupport: 0527915bd439c17242bab26d2136cc837b5eebd2 ReactTestApp-Resources: de7bab7391b329e4e22614a22a7d350b5f08939c RNSVG: 7ee150379dc2a5ed4c9fdac92a5d6efab98e9cf7 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: 3196a843cc5f4bc433f1a41cf17cdf804a01a529 -PODFILE CHECKSUM: b3aedc74b779b6e577dfef780f9684a0bb0daaae +PODFILE CHECKSUM: fec1d0c29d19b06181402b781cf7db0e3b52a2c8 COCOAPODS: 1.16.2 diff --git a/apps/fluent-tester/macos/Podfile.lock b/apps/fluent-tester/macos/Podfile.lock index 670b26132f..def1409b60 100644 --- a/apps/fluent-tester/macos/Podfile.lock +++ b/apps/fluent-tester/macos/Podfile.lock @@ -4,10 +4,10 @@ PODS: - fast_float (8.0.0) - FBLazyVector (0.81.2) - fmt (11.0.2) - - FRNAvatar (0.22.3): + - FRNAvatar (0.22.4): - MicrosoftFluentUI (= 0.13.1) - React - - FRNCallout (0.28.3): + - FRNCallout (0.28.4): - boost - DoubleConversion - fast_float @@ -36,13 +36,13 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - FRNCheckbox (0.18.3): + - FRNCheckbox (0.18.4): - React - - FRNMenuButton (0.14.3): + - FRNMenuButton (0.14.4): - React - - FRNRadioButton (0.22.3): + - FRNRadioButton (0.22.4): - React - - FRNVibrancyView (0.4.3): + - FRNVibrancyView (0.4.4): - React - glog (0.3.5) - hermes-engine (0.81.5): @@ -137,7 +137,7 @@ PODS: - fmt (= 11.0.2) - glog - RCTDeprecation (0.81.2) - - RCTFocusZone (0.22.3): + - RCTFocusZone (0.22.4): - React - RCTRequired (0.81.2) - RCTTypeSafety (0.81.2): @@ -2394,7 +2394,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - ReactTestApp-DevSupport (5.2.3): + - ReactTestApp-DevSupport (5.3.3): - React-Core - React-jsi - ReactTestApp-Resources (1.0.0-dev) @@ -2482,7 +2482,7 @@ DEPENDENCIES: - ReactAppDependencyProvider (from `build/generated/ios`) - ReactCodegen (from `build/generated/ios`) - ReactCommon/turbomodule/core (from `../node_modules/react-native-macos/ReactCommon`) - - "ReactNativeHost (from `../../../node_modules/.store/react-native-test-app-virtual-779a5f15d5/node_modules/@rnx-kit/react-native-host`)" + - "ReactNativeHost (from `../../../node_modules/.store/react-native-test-app-virtual-6d85e861d1/node_modules/@rnx-kit/react-native-host`)" - ReactTestApp-DevSupport (from `../node_modules/react-native-test-app`) - ReactTestApp-Resources (from `..`) - RNSVG (from `../node_modules/react-native-svg`) @@ -2651,7 +2651,7 @@ EXTERNAL SOURCES: ReactCommon: :path: "../node_modules/react-native-macos/ReactCommon" ReactNativeHost: - :path: "../../../node_modules/.store/react-native-test-app-virtual-779a5f15d5/node_modules/@rnx-kit/react-native-host" + :path: "../../../node_modules/.store/react-native-test-app-virtual-6d85e861d1/node_modules/@rnx-kit/react-native-host" ReactTestApp-DevSupport: :path: "../node_modules/react-native-test-app" ReactTestApp-Resources: @@ -2663,22 +2663,22 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: cea1d4f90a3a59537f3deb03ff5656489d7133dd - DoubleConversion: d31b1eb37f6d6f456530c4fd9124b857d6889cab + DoubleConversion: 5b92c4507c560bb62e7aa1acdf2785ea3ff08b3b fast_float: 20817c22759af6ac8d4d67e6e059b8b499953656 FBLazyVector: 0e3076dbb16169b0afc2d701a24a526423b5e76b fmt: 24e7591456deb60b4a77518f83d9a916ac84223f - FRNAvatar: ffa0facb219806b0e5083d2388bc61e590f5df19 - FRNCallout: bc2178e4478504e21c01a3c1c8617c91783eae8c - FRNCheckbox: 1cba85c9c8c75257970f8ffcc4a5e2602c22be7c - FRNMenuButton: 709f3218ce9a421c154ea0570acbeb3f535629d3 - FRNRadioButton: a0e58ac8a7be290778a30add5bdf41dc245e3483 - FRNVibrancyView: 96debdb9a1d2d9315c25748450a33fe2cb8b6321 - glog: 0b31c25149b9d350b2666c7d459229861a00ec07 + FRNAvatar: 586585a440e2b23c9ac872fd1fa2ddb7819cd7bd + FRNCallout: f3234f07e6f49334c0f1de7cb7185494f2915db5 + FRNCheckbox: a5afb9bb4e6405d39a30f997f7e69d6e5672533d + FRNMenuButton: e9e49a3afa5eb5ecbe4a36c41047bce763f661c7 + FRNRadioButton: 6f7db3424604a413b4cca73c5a22033c79f92d43 + FRNVibrancyView: a9cf18fcaee2a7d9f9ea292924ebf1b3acb0a5b2 + glog: ba31c1afa7dcf1915a109861bccdb4421be6175b hermes-engine: 9f4dfe93326146a1c99eb535b1cb0b857a3cd172 MicrosoftFluentUI: dde98d8ed3fc306d9ddd0a6f0bc0c1f24fe5275e RCT-Folly: c803cf33238782d5fd21a5e02d44f64068e0e130 RCTDeprecation: b60b889eafa75f46c3d6be5332681efbb16ad0c7 - RCTFocusZone: 1b8ab01688bbb8ef6357bd8789e5c6f38ddac4d9 + RCTFocusZone: a6d62c46c530e790479915bc9faa578137e9e2a6 RCTRequired: 070d7f0ef937e7a93aab20761cef55419f16b5de RCTTypeSafety: dd2ad1eb0c0c01ba41222d115be141f9a348c636 React: 304df3d34c8c0281fd1fd3a506e69f234095425d @@ -2741,7 +2741,7 @@ SPEC CHECKSUMS: ReactCodegen: 9cb74b74196a04d189f74995f8f11d1228316fb4 ReactCommon: 9f8189efbc1aa52926df2791a2e47b3340353849 ReactNativeHost: 8680b5bbc3afdbfeccc206ed0f3230d7c3df691b - ReactTestApp-DevSupport: a966d21f704e95b197479b4f47cad1cf25808385 + ReactTestApp-DevSupport: 0527915bd439c17242bab26d2136cc837b5eebd2 ReactTestApp-Resources: 7625eab22d0a702577b048d60b5fb3f2e2f5b2a7 RNSVG: 7ee150379dc2a5ed4c9fdac92a5d6efab98e9cf7 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 diff --git a/apps/fluent-tester/package.json b/apps/fluent-tester/package.json index ec7bd00a53..b61acd93e1 100644 --- a/apps/fluent-tester/package.json +++ b/apps/fluent-tester/package.json @@ -95,7 +95,7 @@ "oxc-resolver": "catalog:", "path-dirname": "^1.0.2", "react-native-svg-transformer": "^1.0.0", - "react-native-test-app": "^5.2.3", + "react-native-test-app": "catalog:", "react-test-renderer": "19.1.4", "webdriverio": "catalog:" }, diff --git a/apps/tester-core/package.json b/apps/tester-core/package.json index 3566f47395..a8432fe904 100644 --- a/apps/tester-core/package.json +++ b/apps/tester-core/package.json @@ -119,7 +119,7 @@ "react-native-macos": "^0.81.0", "react-native-svg": "^15.12.1", "react-native-svg-transformer": "^1.0.0", - "react-native-test-app": "^5.2.3", + "react-native-test-app": "catalog:", "react-native-windows": "^0.81.0", "react-test-renderer": "19.1.4", "webdriverio": "catalog:" diff --git a/apps/win32/package.json b/apps/win32/package.json index aec0df3555..4473e8445a 100644 --- a/apps/win32/package.json +++ b/apps/win32/package.json @@ -59,7 +59,7 @@ "metro-config": "^0.83.3", "oxc-resolver": "catalog:", "react-native-svg-transformer": "^1.0.0", - "react-native-test-app": "^5.2.3", + "react-native-test-app": "catalog:", "react-test-renderer": "19.1.4", "rimraf": "catalog:" }, diff --git a/packages/framework-base/src/component-patterns/phasedComponent.ts b/packages/framework-base/src/component-patterns/phasedComponent.ts index 6193707cd2..8614211e5d 100644 --- a/packages/framework-base/src/component-patterns/phasedComponent.ts +++ b/packages/framework-base/src/component-patterns/phasedComponent.ts @@ -17,20 +17,22 @@ export function getPhasedRender(component: React.ComponentType): // 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) { + } else { // 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; - }; + if (staged) { + return (props: TProps) => { + const { children, ...rest } = props as React.PropsWithChildren; + const inner = staged(rest as TProps, ...React.Children.toArray(children)); + // staged render functions were not consistently marking contents as composable, though they were treated + // as such in useHook. To maintain compatibility we mark the returned function as composable here. This was + // dangerous, but this shim is necessary for backward compatibility. The newer pattern is explicit about this. + if (typeof inner === 'function' && !(inner as LegacyDirectComponent)._canCompose) { + return Object.assign(inner, { _canCompose: true }); + } + return inner; + }; + } } } return undefined; @@ -43,9 +45,9 @@ export function getPhasedRender(component: React.ComponentType): */ export function phasedComponent(getInnerPhase: PhasedRender): FunctionComponent { return Object.assign( - (props: React.PropsWithChildren) => { + (props: TProps) => { // pull out children from props - const { children, ...outerProps } = props; + const { children, ...outerProps } = props as React.PropsWithChildren; const Inner = getInnerPhase(outerProps as TProps); return renderForJsxRuntime(Inner, { children }); }, diff --git a/packages/framework-base/src/component-patterns/render.ts b/packages/framework-base/src/component-patterns/render.ts index 2235845071..ceef1ad405 100644 --- a/packages/framework-base/src/component-patterns/render.ts +++ b/packages/framework-base/src/component-patterns/render.ts @@ -1,6 +1,7 @@ import React from 'react'; import * as ReactJSX from 'react/jsx-runtime'; import type { RenderType, RenderResult, DirectComponent, LegacyDirectComponent } from './render.types.ts'; +import { extractChildren, splitPropsAndChildren } from '../utilities/typeUtils.ts'; export type CustomRender = () => RenderResult; @@ -20,13 +21,13 @@ function asLegacyDirectComponent(type: RenderType): LegacyDirectComponen export function renderForJsxRuntime( type: React.ElementType, - props: React.PropsWithChildren, + props: TProps, key?: React.Key, - jsxFn: typeof ReactJSX.jsx = undefined, + jsxFn?: typeof ReactJSX.jsx, ): RenderResult { const legacyDirect = asLegacyDirectComponent(type); if (legacyDirect) { - const { children, ...rest } = props; + const [rest, children] = splitPropsAndChildren(props); const newProps = { ...rest, key }; return legacyDirect(newProps, ...React.Children.toArray(children)) as RenderResult; } @@ -38,14 +39,14 @@ export function renderForJsxRuntime( // auto-detect whether to use jsx or jsxs based on number of children, 0 or 1 = jsx, more than 1 = jsxs if (!jsxFn) { - if (React.Children.count(props.children) > 1) { + if (React.Children.count(extractChildren(props)) > 1) { jsxFn = ReactJSX.jsxs; } else { jsxFn = ReactJSX.jsx; } } // Extract key from props to avoid React 19 warning about spreading key prop - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { key: propsKey, ...propsWithoutKey } = props as any; // Use explicitly passed key, or fall back to key from props const finalKey = key ?? propsKey; diff --git a/packages/framework-base/src/component-patterns/render.types.ts b/packages/framework-base/src/component-patterns/render.types.ts index df68822103..8079be58e6 100644 --- a/packages/framework-base/src/component-patterns/render.types.ts +++ b/packages/framework-base/src/component-patterns/render.types.ts @@ -100,7 +100,7 @@ export type SlotFn = { * Children will be passed as part of the props for component rendering. The `children` prop will be * automatically inferred and typed correctly by the prop type. */ -export type PhasedRender = (props: TProps) => React.ComponentType>; +export type PhasedRender = (props: TProps) => React.ComponentType; /** * Component type for a component that can be rendered in two phases, with the attached phased render function. diff --git a/packages/framework-base/src/immutable-merge/Merge.test.ts b/packages/framework-base/src/immutable-merge/Merge.test.ts index 4985beecbc..0f579e3491 100644 --- a/packages/framework-base/src/immutable-merge/Merge.test.ts +++ b/packages/framework-base/src/immutable-merge/Merge.test.ts @@ -99,11 +99,6 @@ const mergeOptions: MergeOptions = { }, }; -interface IDeepObj { - a: { b: { c: number } }; - b: { c: { d: { d: string } } }; -} - const deep1 = { a: { b: { c: 1 } }, b: { c: { d: { d: 'foo' } } }, @@ -183,9 +178,9 @@ describe('Immutable merge unit tests', () => { const obj1 = { a: 'a', b: 1 }; const obj2 = { b: 2, c: true }; const merged = { a: 'a', b: 2, c: true }; - expect(immutableMerge(obj1, obj2)).toEqual(merged); - expect(immutableMergeCore(0, obj1, obj2)).toEqual(merged); - expect(immutableMergeCore(true, obj1, obj2)).toEqual(merged); + expect(immutableMerge(obj1, obj2)).toEqual(merged); + expect(immutableMergeCore(0, obj1, obj2)).toEqual(merged); + expect(immutableMergeCore(true, obj1, obj2)).toEqual(merged); }); const dm1 = { @@ -199,14 +194,14 @@ describe('Immutable merge unit tests', () => { }; test('deep merge', () => { - expect(immutableMerge(dm1, dm2)).toEqual({ + expect(immutableMerge(dm1, dm2)).toEqual({ a: { b: { c: { foo: 'foo', bar: 'bar2', baz: 'baz' } }, i: 'world' }, d: { e: 1, f: { g: 'hello', h: 2 }, j: 4 }, }); }); test('merge zero levels', () => { - expect(immutableMergeCore(0, dm1, dm2)).toEqual(dm2); + expect(immutableMergeCore(0, dm1, dm2)).toEqual(dm2); }); test('merge one level deep', () => { @@ -214,8 +209,8 @@ describe('Immutable merge unit tests', () => { a: dm2.a, d: { ...dm1.d, ...dm2.d }, }; - expect(immutableMergeCore(1, dm1, dm2)).toEqual(result); - expect(immutableMergeCore({ object: 0 }, dm1, dm2)).toEqual(result); + expect(immutableMergeCore(1, dm1, dm2)).toEqual(result); + expect(immutableMergeCore({ object: 0 }, dm1, dm2)).toEqual(result); }); test('merge with empty object', () => { @@ -226,14 +221,14 @@ describe('Immutable merge unit tests', () => { }); test('merge sett1 and sett2', () => { - const merged = immutableMergeCore(mergeOptions, sett1, sett2) as IFakeSettings; + const merged = immutableMergeCore(mergeOptions, sett1, sett2); expect(merged).toEqual(sett1plus2); - expect(merged!.root.style).toBe(sett1.root.style); + expect(merged!.root!.style).toBe(sett1.root!.style); expect(merged!.fakeSlot!.style).toBe(sett2.fakeSlot!.style); }); test('merge sett1 and sett3', () => { - const merged = immutableMergeCore(mergeOptions, sett1, sett3) as IFakeSettings; + const merged = immutableMergeCore(mergeOptions, sett1, sett3); expect(merged).toEqual(sett1plus3); expect(merged!.fakeSlot).toBe(sett1.fakeSlot); }); @@ -244,7 +239,7 @@ describe('Immutable merge unit tests', () => { }); test('deepMerge', () => { - const merged = immutableMergeCore(-1, deep1, deep2) as IDeepObj; + const merged = immutableMergeCore(-1, deep1, deep2); expect(merged).toEqual(deepMerged); expect(merged.b.c.d).toBe(deep1.b.c.d); expect(merged.a.b).not.toBe(deep2.a.b); @@ -259,14 +254,14 @@ describe('Immutable merge unit tests', () => { const merged = processImmutable(changeMeOption1, singleToChange); expect(merged).toEqual(singleWithChanges); expect(merged).not.toBe(singleToChange); - expect((merged as any).b).toBe(singleToChange.b); + expect(merged.b).toBe(singleToChange.b); }); test('single process with change - alternative', () => { const merged = processImmutable(changeMeOption2, singleToChange); expect(merged).toEqual(singleWithChanges); expect(merged).not.toBe(singleToChange); - expect((merged as any).b).toBe(singleToChange.b); + expect(merged.b).toBe(singleToChange.b); }); const withArray1 = { @@ -296,15 +291,15 @@ describe('Immutable merge unit tests', () => { }; test('last writer wins for objects and non-objects', () => { - const merged = immutableMerge(withObj, withNonObj); + const merged = immutableMerge(withObj, withNonObj); expect(merged).toEqual(withNonObj); - const merged2 = immutableMerge(withNonObj, withObj); + const merged2 = immutableMerge(withNonObj, withObj); expect(merged2).toEqual(withObj); }); const arrayMerger = (...targets: any[]) => { const arrays = targets.filter((t) => Array.isArray(t)); - let result = []; + let result: any[] = []; for (const v of arrays) { if (v.length > 0) { result = result.concat(...v); diff --git a/packages/framework-base/src/immutable-merge/Merge.ts b/packages/framework-base/src/immutable-merge/Merge.ts index b77d9577d0..54f90e4f9e 100644 --- a/packages/framework-base/src/immutable-merge/Merge.ts +++ b/packages/framework-base/src/immutable-merge/Merge.ts @@ -1,3 +1,6 @@ +import { getEntityType, isObject } from '../utilities/typeUtils.ts'; +import type { ObjectMerger, ObjectMergerWithOptions } from '../utilities/mergeTypes.ts'; + /** * The basic options for recursion at a given level. Two types for two behaviors: * @@ -28,19 +31,6 @@ export type BuiltinRecursionHandlers = 'appendArray'; */ export type RecursionHandler = BuiltinRecursionHandlers | CustomRecursionHandler; -/** - * Base object type for merges, avoids using object since that is too broad. In particular things like null and arrays - * are not valid object types for the purposes of this library. - */ -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export type ObjectBase = {}; - -/** - * - */ -export type TypeofResult = 'undefined' | 'object' | 'boolean' | 'number' | 'string' | 'symbol' | 'bigint' | 'function'; -export type ExpandedTypeof = TypeofResult | 'array' | 'null'; - /** * configuration object for the merge, key names are matched with a few exceptions: * - object: matches non-array object types @@ -51,6 +41,12 @@ export interface MergeOptions { [objectTypeOrKeyName: string]: RecursionOption | RecursionHandler | MergeOptions; } +/** + * Union type for the options parameter of the merge core function, this allows for either a simple recursion option + * that applies to all keys and types, or a full configuration object for more control. + */ +export type MergeCoreOptions = RecursionOption | MergeOptions; + /** * built in handlers for the module */ @@ -72,25 +68,6 @@ function normalizeOptions(options: RecursionOption | MergeOptions): [MergeOption : [options, true]; } -/** - * Provide a more sensible type result that expands upon the built in typeof operator - * In particular this will differentiate arrays and nulls from standard objects - * @param val - value to check type - */ -function getEntityType(val: unknown): ExpandedTypeof { - switch (typeof val) { - case 'object': - if (val === null) { - return 'null'; - } else if (Array.isArray(val)) { - return 'array'; - } - return 'object'; - default: - return typeof val as TypeofResult; - } -} - /** resolve custom handlers if they are applicable */ function resolveIfHandler(option: RecursionHandler | RecursionOption | MergeOptions): CustomRecursionHandler | MergeOptions | undefined { return typeof option === 'function' ? option : typeof option === 'string' ? _builtinHandlers[option] : undefined; @@ -141,22 +118,13 @@ function getHandlerForPropertyOfType( return result; } -/** - * Assign properties of source objects to a new target object. This is just a type wrapper around Object.assign - * @param objs - array of objects to merge - * @returns the result of object assign on the objects, typed to T - */ -function assignToNewObject(...objs: T[]): T { - return Object.assign({}, ...objs); -} - /** * Filter a set of unknown values to only include those that extend ObjectBase * @param values - array of values to filter * @returns the filtered set of values */ -export function filterToObjects(values: unknown[]): T[] { - return values.filter((v) => v && getEntityType(v) === 'object' && Object.getOwnPropertyNames(v).length > 0) as T[]; +export function filterToObjects>(values: unknown[]): T[] { + return values.filter((v) => v && isObject(v) && Object.getOwnPropertyNames(v).length > 0) as T[]; } /** @@ -172,15 +140,19 @@ export function filterToObjects(values: unkno * is true the routine will progress through all branches of the hierarchy. Useful if using a processor function that needs to be run. * @param objs - an array of objects to merge together */ -function immutableMergeWorker(mergeOptions: RecursionOption | MergeOptions, singleMode: boolean, ...objs: T[]): T { - const setToMerge = filterToObjects(objs); +function immutableMergeWorker( + mergeOptions: RecursionOption | MergeOptions, + singleMode: boolean, + ...objs: unknown[] +): Record | undefined { + const setToMerge = filterToObjects(objs); const [options, mightRecurse] = normalizeOptions(mergeOptions); const processSingle = singleMode && setToMerge.length === 1; // there is work to do if there is more than one object to merge or if we are processing single objects if (setToMerge.length > 1 || (processSingle && setToMerge.length === 1)) { // now assign everything to get the normal property precedence (and merge all the keys) - let result = processSingle ? undefined : assignToNewObject(...setToMerge); + let result = processSingle ? undefined : Object.assign({}, ...setToMerge); const processSet = result || setToMerge[0]; for (const key in processSet) { @@ -193,11 +165,9 @@ function immutableMergeWorker(mergeOptions: RecursionOptio if (handler !== undefined) { const values = setToMerge.map((set) => set[key]).filter((v) => v !== undefined); const updatedVal = - typeof handler === 'function' - ? handler(...values) - : immutableMergeWorker(handler, singleMode, ...filterToObjects(values)); + typeof handler === 'function' ? handler(...values) : immutableMergeWorker(handler, singleMode, ...filterToObjects(values)); if (updatedVal !== originalVal) { - result = result || assignToNewObject(...setToMerge); + result = result || Object.assign({}, ...setToMerge); result[key] = updatedVal; } } @@ -222,9 +192,7 @@ function immutableMergeWorker(mergeOptions: RecursionOptio * * @param objs - variable input array of typed objects to merge */ -export function immutableMerge(...objs: (T | undefined)[]): T | undefined { - return immutableMergeWorker(true, false, ...objs); -} +export const immutableMerge: ObjectMerger = (...objs: unknown[]) => immutableMergeWorker(true, false, ...objs); /** * Version of immutable merge that can be configured to behave in a variety of manners. See the documentation for details. @@ -232,12 +200,8 @@ export function immutableMerge(...objs: (T | undefined)[]) * @param options - configuration options for the merge, this dictates what keys will be handled in what way * @param objs - set of objects to merge together */ -export function immutableMergeCore( - options: RecursionOption | MergeOptions, - ...objs: (T | undefined)[] -): T | undefined { - return immutableMergeWorker(options, false, ...objs); -} +export const immutableMergeCore: ObjectMergerWithOptions = (options: MergeCoreOptions, ...objs: unknown[]) => + immutableMergeWorker(options, false, ...objs); /** * Process one or more immutable objects ensuring that handlers are called on every entry that applies. If a single object @@ -250,6 +214,5 @@ export function immutableMergeCore( * @param processors - set of processor functions for handling keys * @param objs - one or more objects to process. If multiple objects are passed they will be merged */ -export function processImmutable(options: MergeOptions, ...objs: (T | undefined)[]): T | undefined { - return immutableMergeWorker(options, true, ...objs); -} +export const processImmutable: ObjectMergerWithOptions = (options: MergeOptions, ...objs: unknown[]) => + immutableMergeWorker(options, true, ...objs); diff --git a/packages/framework-base/src/index.ts b/packages/framework-base/src/index.ts index 58f5af762e..ee17d8e0fd 100644 --- a/packages/framework-base/src/index.ts +++ b/packages/framework-base/src/index.ts @@ -4,7 +4,6 @@ export type { BuiltinRecursionHandlers, CustomRecursionHandler, MergeOptions, - ObjectBase, RecursionHandler, RecursionOption, } from './immutable-merge/Merge.ts'; @@ -15,7 +14,6 @@ export { getMemoCache, getTypedMemoCache } from './memo-cache/getMemoCache.ts'; export { memoize } from './memo-cache/memoize.ts'; // merge-props exports -export type { StyleProp } from './merge-props/mergeStyles.types.ts'; export { mergeStyles } from './merge-props/mergeStyles.ts'; export { mergeProps } from './merge-props/mergeProps.ts'; @@ -58,3 +56,8 @@ export type { FurnJSX } from './jsx-namespace.ts'; // general utilities export { filterProps } from './utilities/filterProps.ts'; export type { PropsFilter } from './utilities/filterProps.ts'; + +// core type utilities exports +export type { StyleProp, ObjectBase, ObjectFallback } from './utilities/baseTypes.ts'; +export type { ObjectMerger, ObjectMergerWithOptions, StyleMerger } from './utilities/mergeTypes.ts'; +export type { ExpandedTypeof, TypeofResult } from './utilities/typeUtils.ts'; diff --git a/packages/framework-base/src/memo-cache/getCacheEntry.test.ts b/packages/framework-base/src/memo-cache/getCacheEntry.test.ts index eaf12873a4..ca74351f4c 100644 --- a/packages/framework-base/src/memo-cache/getCacheEntry.test.ts +++ b/packages/framework-base/src/memo-cache/getCacheEntry.test.ts @@ -40,34 +40,34 @@ describe('Memo cache unit tests', () => { test('string gets keyed correctly', () => { const base: TestEntry = {}; const key = 'foo'; - expect(getCacheEntry(base, [key])).toBe(base.str[key]); + expect(getCacheEntry(base, [key])).toBe(base.str![key]); }); test('number gets keyed correctly', () => { const base: TestEntry = {}; const val = 235; const key = val + ''; - expect(getCacheEntry(base, [val])).toBe(base.str[key]); + expect(getCacheEntry(base, [val])).toBe(base.str![key]); }); test('bool gets keyed correctly', () => { const base: TestEntry = {}; const val = true; const key = val + ''; - expect(getCacheEntry(base, [val])).toBe(base.str[key]); + expect(getCacheEntry(base, [val])).toBe(base.str![key]); }); test('false bool gets keyed correctly', () => { const base: TestEntry = {}; const val = false; const key = val + ''; - expect(getCacheEntry(base, [val])).toBe(base.str[key]); + expect(getCacheEntry(base, [val])).toBe(base.str![key]); }); test('object gets keyed correctly', () => { const base: TestEntry = {}; const key = {}; - expect(getCacheEntry(base, [key])).toBe(base.obj.get(key)); + expect(getCacheEntry(base, [key])).toBe(base.obj!.get(key)); }); test('function gets keyed correctly', () => { @@ -75,7 +75,7 @@ describe('Memo cache unit tests', () => { const key = () => { return 'hello world'; }; - expect(getCacheEntry(base, [key])).toBe(base.obj.get(key)); + expect(getCacheEntry(base, [key])).toBe(base.obj!.get(key)); }); test('basic string retrieval', () => { diff --git a/packages/framework-base/src/memo-cache/getCacheEntry.ts b/packages/framework-base/src/memo-cache/getCacheEntry.ts index 5d7b620645..c8f9f16db7 100644 --- a/packages/framework-base/src/memo-cache/getCacheEntry.ts +++ b/packages/framework-base/src/memo-cache/getCacheEntry.ts @@ -35,7 +35,13 @@ function jumpToCacheEntry(entry: CacheEntry, val: any): CacheEntry { if (typeof val === 'object' || typeof val === 'function') { // objects and functions will be treated as key values in a WeakMap const byObj = (entry.obj ??= new WeakMap()); - return byObj.get(val) || byObj.set(val, {}).get(val); + + let newEntry = byObj.get(val); + if (!newEntry) { + newEntry = {}; + byObj.set(val, newEntry); + } + return newEntry; } // otherwise convert everything to a string and store it in the str object (using it as a map) const key = val + ''; @@ -49,7 +55,7 @@ function jumpToCacheEntry(entry: CacheEntry, val: any): CacheEntry { * @param entry - entry to use as the base of the cache walk * @param args - array of arguments to use to progress deeper into the cache */ -export function getCacheEntry(entry: CacheEntry, args: unknown[]): CacheEntry { +export function getCacheEntry(entry: CacheEntry, args?: unknown[]): CacheEntry { // in the case where the args array exists and is > 0 length: // - walk the cache from entry, like a linked list, jumping to the next entry by key, building it up as you go // - otherwise if there are no args just use the noargs branch diff --git a/packages/framework-base/src/memo-cache/getMemoCache.test.ts b/packages/framework-base/src/memo-cache/getMemoCache.test.ts index a43b1ab8bc..934e432b29 100644 --- a/packages/framework-base/src/memo-cache/getMemoCache.test.ts +++ b/packages/framework-base/src/memo-cache/getMemoCache.test.ts @@ -58,8 +58,8 @@ describe('getMemoCache unit tests', () => { test('memo calls function only once for empty inputs', () => { const memoValue = getMemoCache(); const fn = getObjFactory(); - const [o1] = memoValue(fn, undefined); - const [o2] = memoValue(fn, undefined); + const [o1] = memoValue(fn, []); + const [o2] = memoValue(fn, []); expect(o2).toBe(o1); }); diff --git a/packages/framework-base/src/memo-cache/getMemoCache.ts b/packages/framework-base/src/memo-cache/getMemoCache.ts index ae656cc8dd..b02cfd5267 100644 --- a/packages/framework-base/src/memo-cache/getMemoCache.ts +++ b/packages/framework-base/src/memo-cache/getMemoCache.ts @@ -8,8 +8,8 @@ export type ValueFactory = () => T; * - Typed: the cache will enforce the type of both the factory and returned value * - Untyped: the cache will infer the type on each call from the factory return value */ -export type GetTypedMemoValue = (factory: T | ValueFactory, keys: unknown[]) => [T, GetTypedMemoValue]; -export type GetMemoValue = (factory: T | ValueFactory, keys: unknown[]) => [T, GetMemoValue]; +export type GetTypedMemoValue = (factory: T | ValueFactory, keys?: unknown[]) => [T, GetTypedMemoValue]; +export type GetMemoValue = (factory: T | ValueFactory, keys?: unknown[]) => [T, GetMemoValue]; /** base node used to remember references when a globalKey is set */ const _baseEntry: CacheEntry = {}; @@ -21,13 +21,13 @@ const _baseEntry: CacheEntry = {}; * @param factory - generally a function who's results will be cached, and returned via the set of keys * @param keys - an ordered array of values of any type, used as keys to look up the entry */ -function getMemoValueWorker(entry: CacheEntry, factory: T | ValueFactory, keys: unknown[]): [T, GetMemoValue] { +function getMemoValueWorker(entry: CacheEntry, factory: T | ValueFactory, keys?: unknown[]): [T, GetMemoValue] { const foundEntry = getCacheEntry(entry, keys); // check the key being set, not the value to disambiguate an undefined factory result/value from never having run the factory if (!Object.prototype.hasOwnProperty.call(foundEntry, 'value')) { foundEntry.value = typeof factory === 'function' ? (factory as ValueFactory)() : factory; } - return [foundEntry.value as T, (fact: U | ValueFactory, args: unknown[]) => getMemoValueWorker(foundEntry, fact, args)]; + return [foundEntry.value as T, (fact: U | ValueFactory, args?: unknown[]) => getMemoValueWorker(foundEntry, fact, args)]; } /** diff --git a/packages/framework-base/src/merge-props/index.ts b/packages/framework-base/src/merge-props/index.ts index e8a4571f2a..ddc67901d8 100644 --- a/packages/framework-base/src/merge-props/index.ts +++ b/packages/framework-base/src/merge-props/index.ts @@ -1,3 +1,2 @@ -export type { StyleProp } from './mergeStyles.types.ts'; export { mergeStyles } from './mergeStyles.ts'; export { mergeProps } from './mergeProps.ts'; diff --git a/packages/framework-base/src/merge-props/mergeProps.ts b/packages/framework-base/src/merge-props/mergeProps.ts index 2332b5037d..0bd62a3f20 100644 --- a/packages/framework-base/src/merge-props/mergeProps.ts +++ b/packages/framework-base/src/merge-props/mergeProps.ts @@ -1,5 +1,6 @@ import type { MergeOptions } from '../immutable-merge/Merge.ts'; import { immutableMergeCore, filterToObjects } from '../immutable-merge/Merge.ts'; +import type { ObjectMerger } from '../utilities/mergeTypes.ts'; import { mergeStyles } from './mergeStyles.ts'; @@ -15,6 +16,4 @@ const mergePropsOptions: MergeOptions = { * Merge props together, flattening and merging styles as appropriate * @param props - props to merge together */ -export function mergeProps(...props: (TProps | undefined)[]): TProps { - return immutableMergeCore(mergePropsOptions, ...filterToObjects(props)); -} +export const mergeProps: ObjectMerger = (...props: unknown[]) => immutableMergeCore(mergePropsOptions, ...filterToObjects(props)); diff --git a/packages/framework-base/src/merge-props/mergeStyles.test.ts b/packages/framework-base/src/merge-props/mergeStyles.test.ts index 3642e18910..5220111774 100644 --- a/packages/framework-base/src/merge-props/mergeStyles.test.ts +++ b/packages/framework-base/src/merge-props/mergeStyles.test.ts @@ -1,5 +1,5 @@ import { flattenStyle, mergeAndFlattenStyles, mergeStyles } from './mergeStyles.ts'; -import type { StyleProp } from './mergeStyles.types.ts'; +import type { StyleProp } from '../utilities/baseTypes.ts'; type OpaqueColorValue = symbol & { __TYPE__: 'Color' }; type ColorValue = string | OpaqueColorValue; diff --git a/packages/framework-base/src/merge-props/mergeStyles.ts b/packages/framework-base/src/merge-props/mergeStyles.ts index 75f1bc2d5d..46b4c611a6 100644 --- a/packages/framework-base/src/merge-props/mergeStyles.ts +++ b/packages/framework-base/src/merge-props/mergeStyles.ts @@ -1,16 +1,18 @@ import { immutableMerge } from '../immutable-merge/Merge.ts'; import { getMemoCache } from '../memo-cache/getMemoCache.ts'; +import type { StyleMerger } from '../utilities/mergeTypes.ts'; -import type { StyleProp } from './mergeStyles.types.ts'; +import type { StyleProp } from '../utilities/baseTypes.ts'; /** * Take a react-native style, which may be a recursive array, and return as a flattened - * style. This is analagous to the flatten routine that is part of the style sheet API + * style. This is analogous to the flatten routine that is part of the style sheet API * * @param style - StyleProp to flatten, this can be a TStyle or an array + * @internal */ -export function flattenStyle(style: StyleProp): T { - return Array.isArray(style) ? immutableMerge(...style.map((v) => flattenStyle(v))) : ((style || {}) as T); +export function flattenStyle(style: StyleProp): object { + return Array.isArray(style) ? (immutableMerge(...style.map((v) => flattenStyle(v))) as object) : style || {}; } /** @@ -19,31 +21,14 @@ export function flattenStyle(style: StyleProp): T { * @param styles - array of styles to merge together. The styles will be flattened as part of the process */ -// Overload for 2 arguments with potentially different types -export function mergeAndFlattenStyles( - style1: StyleProp, - style2: StyleProp, -): (T1 & T2) | undefined; - -// Overload for 3 arguments with potentially different types -export function mergeAndFlattenStyles( - style1: StyleProp, - style2: StyleProp, - style3: StyleProp, -): (T1 & T2 & T3) | undefined; - -// General fallback for any number of arguments of the same type -export function mergeAndFlattenStyles(...styles: StyleProp[]): TStyle | undefined; - -// Implementation -export function mergeAndFlattenStyles(...styles: StyleProp[]): object | undefined { +export const mergeAndFlattenStyles: StyleMerger = (...styles: StyleProp[]) => { // baseline merge and flatten the objects return immutableMerge( - ...styles.map((styleProp: StyleProp) => { + ...styles.map((styleProp: StyleProp) => { return flattenStyle(styleProp); }), ); -} +}; const _styleCache = getMemoCache(); @@ -51,38 +36,12 @@ const _styleCache = getMemoCache(); * Function overloads to allow merging styles of different types. * This is useful when merging token-based styles with React Native StyleProp types. */ - -// Overload for 1 argument, forces flattening of sub arrays -export function mergeStyles(style1: StyleProp): T1 | undefined; - -// Overload for 2 arguments with potentially different types -export function mergeStyles(style1: StyleProp, style2: StyleProp): (T1 & T2) | undefined; - -// Overload for 3 arguments with potentially different types -export function mergeStyles( - style1: StyleProp, - style2: StyleProp, - style3: StyleProp, -): (T1 & T2 & T3) | undefined; - -// Overload for 4 arguments with potentially different types -export function mergeStyles( - style1: StyleProp, - style2: StyleProp, - style3: StyleProp, - style4: StyleProp, -): (T1 & T2 & T3 & T4) | undefined; - -// General fallback for any number of arguments of the same type -export function mergeStyles(...styles: StyleProp[]): TStyle | undefined; - -// Implementation -export function mergeStyles(...styles: StyleProp[]): object | undefined { +export const mergeStyles: StyleMerger = (...styles: StyleProp[]) => { // filter the style set to just objects (which might be arrays or plain style objects) - const inputs = styles.filter((s) => typeof s === 'object') as object[]; + const inputs = styles.filter((s) => s !== null && typeof s === 'object'); // now memo the results if there is more than one element or if the one element is an array return inputs.length > 1 || (inputs.length === 1 && Array.isArray(inputs[0])) ? _styleCache(() => mergeAndFlattenStyles(undefined, ...inputs), inputs)[0] : inputs[0] || {}; -} +}; diff --git a/packages/framework-base/src/merge-props/mergeStyles.types.ts b/packages/framework-base/src/merge-props/mergeStyles.types.ts deleted file mode 100644 index 5a2e3b3856..0000000000 --- a/packages/framework-base/src/merge-props/mergeStyles.types.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * This is a copy of the react-native style prop type, copied here to avoid RN dependencies for web clients - */ -type Falsy = undefined | null | false | '' | 0; -type RecursiveArray = readonly (T | RecursiveArray)[] | (T | RecursiveArray)[]; -/** Keep a brand of 'T' so that calls to `StyleSheet.flatten` can take `RegisteredStyle` and return `T`. */ -type RegisteredStyle = number & { __registeredStyleBrand: T }; - -export type StyleProp = T | RegisteredStyle | RecursiveArray | Falsy> | Falsy; diff --git a/packages/framework-base/src/utilities/baseTypes.ts b/packages/framework-base/src/utilities/baseTypes.ts new file mode 100644 index 0000000000..b93dd70140 --- /dev/null +++ b/packages/framework-base/src/utilities/baseTypes.ts @@ -0,0 +1,28 @@ +/** + * This is a copy of the react-native style prop type, copied here to avoid RN dependencies this early in the dependency tree. + */ +type Falsy = undefined | null | false | ''; +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface RecursiveArray extends Array> {} +/** Keep a brand of 'T' so that calls to `StyleSheet.flatten` can take `RegisteredStyle` and return `T`. */ +type RegisteredStyle = number & { __registeredStyleBrand: T }; +export type StyleProp = T | RegisteredStyle | RecursiveArray | Falsy> | Falsy; + +/** + * This is the baseline for acceptance object types, meaning for T extends ObjectBase. The options here + * are: + * - {} an empty object, which works but is a bit too loose for general use + * - Record which is fine with types but doesn't work with + * interfaces as they have no implicit index signature + * - object which is the built in object type, slightly stricter than {} but still allows for interfaces + * + * There's no perfect option here but object is the best overall choice. + */ +export type ObjectBase = object; + +/** + * For fallback object types it is better to use the stricter Record type, as it + * is more likely to catch issues with unexpected properties and is still compatible with the + * ObjectBase type. + */ +export type ObjectFallback = Record; diff --git a/packages/framework-base/src/utilities/baseTypes.validate.ts b/packages/framework-base/src/utilities/baseTypes.validate.ts new file mode 100644 index 0000000000..3c5d6ae9d6 --- /dev/null +++ b/packages/framework-base/src/utilities/baseTypes.validate.ts @@ -0,0 +1,104 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/** + * Type validation that the base types behave as expected. This code is never run and is not included + * in other files, but will cause build breaks if the types no longer behave as expected. + */ + +import type { StyleProp, ObjectBase, ObjectFallback } from './baseTypes.ts'; +import type { StyleProp as RNStyleProp } from 'react-native'; + +/** + * Validate that StyleProp is compatible with React Native's StyleProp type, as this is a critical part of our type system for styles and we want to ensure it remains compatible with RN's types. + */ +export type ValidateStyleProp = StyleProp extends RNStyleProp ? true : never; + +type StyleBase = { + color?: string; + fontSize?: number; +}; + +type TestProps = { + p1?: string; + p2?: number; + p3?: boolean; + style?: StyleProp; +}; + +const typeProps: TestProps = { + p1: 'string', + p2: 123, + p3: true, + style: { + color: 'red', + fontSize: 16, + }, +}; + +interface IStyleBase { + color?: string; + fontSize?: number; +} + +interface ITestProps { + p1?: string; + p2?: number; + p3?: boolean; + style?: StyleProp; +} + +const interfaceProps: ITestProps = { + p1: 'string', + p2: 123, + p3: true, + style: { + color: 'red', + fontSize: 16, + }, +}; + +export function validateBaseTypes() { + // This function is never called, but if the types of the base types change in a way that breaks compatibility with expected types, this will cause a build error and alert us to the issue. + + // Test that StyleProp is compatible with React Native's StyleProp type + const stylePropTest: ValidateStyleProp = true; + const stylePropTest2: ValidateStyleProp = true; + + // just using the values to stop typescript complaints + if (!stylePropTest || !stylePropTest2) { + throw new Error("StyleProp is not compatible with React Native's StyleProp type"); + } + + // Test that ObjectBase is compatible with object and Record + + const objectBaseTest1: ObjectBase = {}; + const objectBaseTest2: ObjectBase = { key: 'value' }; + const objectBaseTest3: ObjectBase = new Date(); + const objectBaseTest4: ObjectBase = typeProps; + const objectBaseTest5: ObjectBase = interfaceProps; + const objectBaseTest6: ObjectFallback = {}; + const objectBaseTest7: ObjectFallback = { key: 'value' }; + // @ts-expect-error - this should error because Date is not compatible with Record due to its properties not being string keys and unknown values + const objectBaseTest8: ObjectFallback = new Date(); + const objectBaseTest9: ObjectFallback = typeProps; + // @ts-expect-error - this should error because interfaceProps is not compatible with Record due to the style property being a StyleProp type which is not compatible with Record + const objectBaseTest10: ObjectFallback = interfaceProps; + + // cross assignment + const baseFromFallback: ObjectBase = objectBaseTest7; + // @ts-expect-error - this should error because ObjectFallback is not compatible with ObjectBase due to ObjectBase allowing for more types of objects than ObjectFallback + const fallbackFromBase: ObjectFallback = objectBaseTest2; + + return { + ...objectBaseTest1, + ...objectBaseTest2, + ...objectBaseTest3, + ...objectBaseTest4, + ...objectBaseTest5, + ...objectBaseTest6, + ...objectBaseTest7, + ...objectBaseTest8, + ...objectBaseTest9, + ...objectBaseTest10, + ...baseFromFallback, + }; +} diff --git a/packages/framework-base/src/utilities/filterProps.ts b/packages/framework-base/src/utilities/filterProps.ts index c1c86435ab..d78c5c003f 100644 --- a/packages/framework-base/src/utilities/filterProps.ts +++ b/packages/framework-base/src/utilities/filterProps.ts @@ -1,13 +1,14 @@ import { mergeProps } from '../merge-props/mergeProps.ts'; +import { isObject } from './typeUtils.ts'; 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 (filter && isObject(props)) { + const propsToRemove = filter ? Object.keys(props).filter((key) => !filter(key)) : []; if (propsToRemove?.length > 0) { const propsToRemoveObj = Object.fromEntries(propsToRemove.map((prop) => [prop, undefined])) as TProps; - return mergeProps(props, propsToRemoveObj); + return mergeProps(props, propsToRemoveObj); } } return props; diff --git a/packages/framework-base/src/utilities/mergeTypes.ts b/packages/framework-base/src/utilities/mergeTypes.ts new file mode 100644 index 0000000000..ae9d67e9b8 --- /dev/null +++ b/packages/framework-base/src/utilities/mergeTypes.ts @@ -0,0 +1,55 @@ +import type { StyleProp, ObjectFallback } from './baseTypes.ts'; + +/** + * Overloaded function types for an object merger, similar to Object.assign but with better type inference and support for + * undefined values. + */ +export type ObjectMerger = { + // T1 defined overloads + (o1: T1, ...objs: undefined[]): T1; + (o1: T1, o2: T2, ...objs: undefined[]): T1 & T2; + (o1: T1, o2: T2, o3: T3, ...objs: undefined[]): T1 & T2 & T3; + // T1 undefined overloads + (o1: undefined, o2: T2, ...objs: undefined[]): T2; + (o1: undefined, o2: T2, o3: T3, ...objs: undefined[]): T2 & T3; + // T2 undefined overload + (o1: T1, o2: undefined, o3: T3, ...objs: undefined[]): T1 & T3; + // rest overloads + (...objs: unknown[]): T | undefined; +}; + +/** + * Overloaded function types for an object merger that takes options, similar to Object.assign but with better type inference and support for + * undefined values, and with an options parameter to control merge behavior. + */ +export type ObjectMergerWithOptions = { + // T1 defined overloads + (opt: TOptions, o1: T1, ...objs: undefined[]): T1; + (opt: TOptions, o1: T1, o2: T2, ...objs: undefined[]): T1 & T2; + (opt: TOptions, o1: T1, o2: T2, o3: T3, ...objs: undefined[]): T1 & T2 & T3; + // T1 undefined overloads + (opt: TOptions, o1: undefined, o2: T2, ...objs: undefined[]): T2; + (opt: TOptions, o1: undefined, o2: T2, o3: T3, ...objs: undefined[]): T2 & T3; + // T2 undefined overload + (opt: TOptions, o1: T1, o2: undefined, o3: T3, ...objs: undefined[]): T1 & T3; + // rest overloads + (opt: TOptions, ...objs: unknown[]): T | undefined; +}; + +/** + * Overloaded function types for a style merger, which is similar to an object merger but specifically for merging styles that may be in the form of StyleProp types. + * This includes support for merging styles of different types, which is useful when merging token-based styles with React Native StyleProp types. + */ +export type StyleMerger = { + // T1 defined overloads + (o1: StyleProp, ...objs: undefined[]): T1; + (o1: StyleProp, o2: StyleProp, ...objs: undefined[]): T1 & T2; + (o1: StyleProp, o2: StyleProp, o3: StyleProp, ...objs: undefined[]): T1 & T2 & T3; + // T1 undefined overloads + (o1: StyleProp, o2: StyleProp, ...objs: undefined[]): T2; + (o1: StyleProp, o2: StyleProp, o3: StyleProp, ...objs: undefined[]): T2 & T3; + // T2 undefined overload + (o1: StyleProp, o2: StyleProp, o3: StyleProp, ...objs: undefined[]): T1 & T3; + // rest overloads + (...objs: unknown[]): T | undefined; +}; diff --git a/packages/framework-base/src/utilities/typeUtils.ts b/packages/framework-base/src/utilities/typeUtils.ts new file mode 100644 index 0000000000..02850270f2 --- /dev/null +++ b/packages/framework-base/src/utilities/typeUtils.ts @@ -0,0 +1,53 @@ +/** + * + */ +export type TypeofResult = 'undefined' | 'object' | 'boolean' | 'number' | 'string' | 'symbol' | 'bigint' | 'function'; +export type ExpandedTypeof = TypeofResult | 'array' | 'null'; + +/** + * Provide a more sensible type result that expands upon the built in typeof operator + * In particular this will differentiate arrays and nulls from standard objects + * @param val - value to check type + */ +export function getEntityType(val: unknown): ExpandedTypeof { + switch (typeof val) { + case 'object': + if (val === null) { + return 'null'; + } else if (Array.isArray(val)) { + return 'array'; + } + return 'object'; + default: + return typeof val as TypeofResult; + } +} + +/** + * Assertion function for types related to objects (objects with string keys and some value types). + * This is used to narrow down types in situations where we want to ensure we are working with a plain + * object and not something else (like an array or null). + * @param value some value of unknown type + * @returns an assertion that the value is an object with string keys and unknown values (not an array or null) + */ +export function isObject>(value: unknown): value is T { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +/** + * Helper to split props into children and non-children props. + * @param props unknown props type object to split + * @returns a tuple of the non-children props and the children + */ +export function splitPropsAndChildren(props: TProps): [Omit, React.ReactNode] { + const { children, ...rest } = props as React.PropsWithChildren; + return [rest as Omit, children]; +} + +/** + * Helper to get the children from an unknown props type object. + */ +export function extractChildren(props: TProps): React.ReactNode { + const { children } = props as React.PropsWithChildren; + return children; +} diff --git a/packages/framework-base/tsconfig.json b/packages/framework-base/tsconfig.json index af17d65ef1..dd65e7b5f4 100644 --- a/packages/framework-base/tsconfig.json +++ b/packages/framework-base/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@fluentui-react-native/scripts/tsconfig", + "extends": "@fluentui-react-native/scripts/tsconfig-strict", "compilerOptions": { "outDir": "lib", "allowJs": true, diff --git a/packages/framework/use-slot/src/useSlot.test.tsx b/packages/framework/use-slot/src/useSlot.test.tsx index 7a6a839260..49615739ef 100644 --- a/packages/framework/use-slot/src/useSlot.test.tsx +++ b/packages/framework/use-slot/src/useSlot.test.tsx @@ -121,6 +121,7 @@ describe('useSlot tests', () => { }); const tree2 = component2!.toJSON(); expect(tree2).toMatchSnapshot(); + // @ts-expect-error - we know the structure of the tree here and want to compare the text nodes directly, this is not a general pattern expect(tree1!['HeaderCaptionText1']).toEqual(tree2!['HeaderCaptionText2']); }); }); diff --git a/packages/framework/use-slot/src/useSlot.ts b/packages/framework/use-slot/src/useSlot.ts index f35c32a101..bbd71ec1bc 100644 --- a/packages/framework/use-slot/src/useSlot.ts +++ b/packages/framework/use-slot/src/useSlot.ts @@ -48,7 +48,7 @@ export function useSlot( 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); + innerProps = mergeProps(propsToMerge, innerProps); } if (filter) { // filter the final props if a filter is specified diff --git a/packages/framework/use-slot/tsconfig.json b/packages/framework/use-slot/tsconfig.json index 256f00585b..e195da31c2 100644 --- a/packages/framework/use-slot/tsconfig.json +++ b/packages/framework/use-slot/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@fluentui-react-native/scripts/tsconfig", + "extends": "@fluentui-react-native/scripts/tsconfig-strict", "compilerOptions": { "outDir": "lib", "rootDir": "src", diff --git a/packages/utils/adapters/src/filterProps.ts b/packages/utils/adapters/src/filterProps.ts index 30db1220af..ae79d10097 100644 --- a/packages/utils/adapters/src/filterProps.ts +++ b/packages/utils/adapters/src/filterProps.ts @@ -1,6 +1,4 @@ import { getViewMask, getTextMask, getImageMask } from './filters'; -import type { ViewProps, TextProps, ImageProps } from 'react-native'; -import type { IFilterMask } from './filter.types'; /** * Filters props based on the provided mask. Each filter function is memoized to only compute the mask once, @@ -12,7 +10,7 @@ import type { IFilterMask } from './filter.types'; * @param propName - The name of the prop to check against the view mask */ export const filterViewProps = (() => { - let viewMask: IFilterMask | undefined; + let viewMask: Record | undefined; return (propName: string): boolean => { viewMask ??= getViewMask(); return Boolean(viewMask[propName]); @@ -24,7 +22,7 @@ export const filterViewProps = (() => { * @param propName - The name of the prop to check against the text mask */ export const filterTextProps = (() => { - let textMask: IFilterMask | undefined; + let textMask: Record | undefined; return (propName: string): boolean => { textMask ??= getTextMask(); return Boolean(textMask[propName]); @@ -36,7 +34,7 @@ export const filterTextProps = (() => { * @param propName - The name of the prop to check against the image mask */ export const filterImageProps = (() => { - let imageMask: IFilterMask | undefined; + let imageMask: Record | undefined; return (propName: string): boolean => { imageMask ??= getImageMask(); return Boolean(imageMask[propName]); diff --git a/packages/utils/adapters/tsconfig.json b/packages/utils/adapters/tsconfig.json index ce2d789490..46f18b0725 100644 --- a/packages/utils/adapters/tsconfig.json +++ b/packages/utils/adapters/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@fluentui-react-native/scripts/tsconfig", + "extends": "@fluentui-react-native/scripts/tsconfig-strict", "compilerOptions": { "outDir": "lib", "rootDir": "src", diff --git a/packages/utils/interactive-hooks/src/__tests__/events.types.test.ts b/packages/utils/interactive-hooks/src/__tests__/events.types.test.ts index 6685f5b264..8edae17b2a 100644 --- a/packages/utils/interactive-hooks/src/__tests__/events.types.test.ts +++ b/packages/utils/interactive-hooks/src/__tests__/events.types.test.ts @@ -3,7 +3,7 @@ import type { AccessibilityActionEvent, GestureResponderEvent } from 'react-nati import { isAccessibilityActionEvent, isGestureResponderEvent, isKeyPressEvent } from '../events.types'; import type { KeyPressEvent } from '../useKeyProps.types'; -const createMockEvent = (nativeEvent) => { +const createMockEvent = (nativeEvent: Record) => { return { nativeEvent: nativeEvent, currentTarget: null, @@ -33,7 +33,7 @@ const createMockEvent = (nativeEvent) => { }; }; -const mockGestureEvent: GestureResponderEvent = createMockEvent({ +const mockGestureEvent = createMockEvent({ changedTouches: [], identifier: '', locationX: 0, @@ -43,15 +43,15 @@ const mockGestureEvent: GestureResponderEvent = createMockEvent({ target: '', timestamp: 0, touches: [], -}); +}) as unknown as GestureResponderEvent; -const mockKeyPressEvent: KeyPressEvent = createMockEvent({ +const mockKeyPressEvent = createMockEvent({ key: 'enter', -}); +}) as unknown as KeyPressEvent; -const mockAccessibilityEvent: AccessibilityActionEvent = createMockEvent({ +const mockAccessibilityEvent = createMockEvent({ actionName: 'longpress', -}); +}) as unknown as AccessibilityActionEvent; describe('InteractionEvent type guard tests', () => { it('has correct output from isGestureResponderEvent when input is type GestureResponderEvent', () => { diff --git a/packages/utils/interactive-hooks/src/useAsPressable.ts b/packages/utils/interactive-hooks/src/useAsPressable.ts index db84d13a61..b79aa31db7 100644 --- a/packages/utils/interactive-hooks/src/useAsPressable.ts +++ b/packages/utils/interactive-hooks/src/useAsPressable.ts @@ -8,11 +8,12 @@ import type { IHoverState, IFocusState, IWithPressableEvents, + IWithPartialPressableEvents, } from './useAsPressable.types'; +import type { BlurEvent, FocusEvent, MouseEvent, GestureResponderEvent } from 'react-native'; import type { PressableFocusProps, PressableHoverProps, PressablePressProps } from './usePressableState.types'; -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -type ObjectBase = {}; +type ObjectBase = object; /** * hover specific state and callback helper @@ -22,7 +23,7 @@ function useHoverHelper(props: PressableHoverProps): [PressableHoverProps, IHove const { onHoverIn: onHoverInProp, onHoverOut: onHoverOutProp } = props; const onHoverIn = React.useCallback( - (e) => { + (e: MouseEvent) => { setHoverState({ hovered: true }); if (onHoverInProp) { onHoverInProp(e); @@ -32,7 +33,7 @@ function useHoverHelper(props: PressableHoverProps): [PressableHoverProps, IHove ); const onHoverOut = React.useCallback( - (e) => { + (e: MouseEvent) => { setHoverState({ hovered: false }); if (onHoverOutProp) { onHoverOutProp(e); @@ -50,7 +51,7 @@ function useFocusHelper(props: PressableFocusProps): [PressableFocusProps, IFocu const [focusState, setFocusState] = React.useState({ focused: false }); const { onBlur: onBlurProp, onFocus: onFocusProp } = props; const onFocus = React.useCallback( - (e) => { + (e: FocusEvent) => { setFocusState({ focused: true }); if (onFocusProp) { onFocusProp(e); @@ -60,7 +61,7 @@ function useFocusHelper(props: PressableFocusProps): [PressableFocusProps, IFocu ); const onBlur = React.useCallback( - (e) => { + (e: BlurEvent) => { setFocusState({ focused: false }); if (onBlurProp) { onBlurProp(e); @@ -79,7 +80,7 @@ function usePressHelper(props: PressablePressProps): [PressablePressProps, IPres const { onPressIn: onPressInProp, onPressOut: onPressOutProp } = props; const onPressIn = React.useCallback( - (e) => { + (e: GestureResponderEvent) => { setPressState({ pressed: true }); if (onPressInProp) { onPressInProp(e); @@ -89,7 +90,7 @@ function usePressHelper(props: PressablePressProps): [PressablePressProps, IPres ); const onPressOut = React.useCallback( - (e) => { + (e: GestureResponderEvent) => { setPressState({ pressed: false }); if (onPressOutProp) { onPressOutProp(e); @@ -106,7 +107,7 @@ function usePressHelper(props: PressablePressProps): [PressablePressProps, IPres * as each of these calls will create a new instance of the Pressability class. * @param props - input props for the component */ -export function useFocusState(props: IWithPressableOptions): [IWithPressableEvents, IFocusState] { +export function useFocusState(props: IWithPressableOptions): [IWithPartialPressableEvents, IFocusState] { const [focusProps, focusState] = useFocusHelper(props); return [{ ...props, ...usePressability({ ...props, ...focusProps }) }, focusState]; } @@ -116,7 +117,7 @@ export function useFocusState(props: IWithPressableOptions * as each of these calls will create a new instance of the Pressability class. * @param props - input props for the component */ -export function usePressState(props: IWithPressableOptions): [IWithPressableEvents, IPressState] { +export function usePressState(props: IWithPressableOptions): [IWithPartialPressableEvents, IPressState] { const [pressProps, pressState] = usePressHelper(props); return [{ ...props, ...usePressability({ ...props, ...pressProps }) }, pressState]; } @@ -126,7 +127,7 @@ export function usePressState(props: IWithPressableOptions * as each of these calls will create a new instance of the Pressability class. * @param props - input props for the component */ -export function useHoverState(props: IWithPressableOptions): [IWithPressableEvents, IHoverState] { +export function useHoverState(props: IWithPressableOptions): [IWithPartialPressableEvents, IHoverState] { const [hoverProps, hoverState] = useHoverHelper(props); return [{ ...props, ...usePressability({ ...props, ...hoverProps }) }, hoverState]; } @@ -142,7 +143,7 @@ export function useAsPressable(props: IWithPressableOption const pressabilityProps = usePressability({ ...props, ...hoverProps, ...focusProps, ...pressProps }); return { - props: { ...props, ...pressabilityProps }, + props: { ...props, ...pressabilityProps } as IWithPressableEvents, state: { ...hoverState, ...focusState, ...pressState }, }; } diff --git a/packages/utils/interactive-hooks/src/useAsPressable.types.ts b/packages/utils/interactive-hooks/src/useAsPressable.types.ts index 981b6ee926..88b0cc7221 100644 --- a/packages/utils/interactive-hooks/src/useAsPressable.types.ts +++ b/packages/utils/interactive-hooks/src/useAsPressable.types.ts @@ -1,7 +1,6 @@ import type { PressabilityConfig, EventHandlers } from './usePressability'; -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -type ObjectBase = {}; +type ObjectBase = object; export type IPressState = { pressed?: boolean; @@ -25,6 +24,8 @@ export type IWithPressableOptions = T & IPressableOptions; export type IWithPressableEvents = T & EventHandlers; +export type IWithPartialPressableEvents = T & Partial; + export type IPressableHooks = { props: IWithPressableEvents; state: IPressableState; diff --git a/packages/utils/interactive-hooks/src/useAsToggle.ts b/packages/utils/interactive-hooks/src/useAsToggle.ts index 7a93a3fb03..836974f56a 100644 --- a/packages/utils/interactive-hooks/src/useAsToggle.ts +++ b/packages/utils/interactive-hooks/src/useAsToggle.ts @@ -14,7 +14,11 @@ export type OnChangeCallback = () => void; * onChange() - Callback to toggle the component * state.isChecked - Whether or not component is currently checked or selected */ -export function useAsToggle(defaultChecked?: boolean, checked?: boolean, userCallback?: OnToggleCallback): [boolean, OnChangeCallback] { +export function useAsToggle( + defaultChecked?: boolean, + checked?: boolean, + userCallback?: OnToggleCallback, +): [boolean | undefined, OnChangeCallback] { const [isChecked, setChecked] = React.useState(defaultChecked ?? checked); const onChange = React.useCallback(() => { diff --git a/packages/utils/interactive-hooks/src/useAsToggleWithEvent.ts b/packages/utils/interactive-hooks/src/useAsToggleWithEvent.ts index 3d6a081b4e..eaca470c59 100644 --- a/packages/utils/interactive-hooks/src/useAsToggleWithEvent.ts +++ b/packages/utils/interactive-hooks/src/useAsToggleWithEvent.ts @@ -21,7 +21,7 @@ export function useAsToggleWithEvent( defaultChecked?: boolean, checked?: boolean, userCallback?: OnToggleWithEventCallback, -): [boolean, OnChangeWithEventCallback] { +): [boolean | undefined, OnChangeWithEventCallback] { const [isChecked, setChecked] = useControllableValue(checked, defaultChecked); const onChange = React.useCallback( diff --git a/packages/utils/interactive-hooks/src/usePressability.ts b/packages/utils/interactive-hooks/src/usePressability.ts index 4ff8f0a521..2099ca01f1 100644 --- a/packages/utils/interactive-hooks/src/usePressability.ts +++ b/packages/utils/interactive-hooks/src/usePressability.ts @@ -1,5 +1,6 @@ import type { PressableProps, GestureResponderEvent, BlurEvent, MouseEvent } from 'react-native'; +// @ts-expect-error - types are still in flow, we are explicitly creating a typed wrapper around this import usePressabilityBase from 'react-native/Libraries/Pressability/usePressability'; export type Rect = { diff --git a/packages/utils/interactive-hooks/src/usePressableState.ts b/packages/utils/interactive-hooks/src/usePressableState.ts index d35427b07f..fe74ecf3ab 100644 --- a/packages/utils/interactive-hooks/src/usePressableState.ts +++ b/packages/utils/interactive-hooks/src/usePressableState.ts @@ -11,6 +11,8 @@ import type { PressablePropsExtended, } from './usePressableState.types'; +import type { MouseEvent, FocusEvent, BlurEvent, GestureResponderEvent } from 'react-native'; + /** * hover specific state and callback helper */ @@ -19,7 +21,7 @@ export function useHoverHelper(props: PressableHoverProps): [PressableHoverProps const { onHoverIn, onHoverOut } = props; const _onHoverIn = React.useCallback( - (e) => { + (e: MouseEvent) => { setHoverState({ hovered: true }); onHoverIn?.(e); }, @@ -27,7 +29,7 @@ export function useHoverHelper(props: PressableHoverProps): [PressableHoverProps ); const _onHoverOut = React.useCallback( - (e) => { + (e: MouseEvent) => { setHoverState({ hovered: false }); onHoverOut?.(e); }, @@ -43,7 +45,7 @@ export function useFocusHelper(props: PressableFocusProps): [PressableFocusProps const [focusState, setFocusState] = React.useState({ focused: false }); const { onFocus, onBlur } = props; const _onFocus = React.useCallback( - (e) => { + (e: FocusEvent) => { setFocusState({ focused: true }); onFocus?.(e); }, @@ -51,7 +53,7 @@ export function useFocusHelper(props: PressableFocusProps): [PressableFocusProps ); const _onBlur = React.useCallback( - (e) => { + (e: BlurEvent) => { setFocusState({ focused: false }); onBlur?.(e); }, @@ -68,7 +70,7 @@ export function usePressHelper(props: PressablePressProps): [PressablePressProps const { onPressIn, onPressOut } = props; const _onPressIn = React.useCallback( - (e) => { + (e: GestureResponderEvent) => { setPressState({ pressed: true }); onPressIn?.(e); }, @@ -76,7 +78,7 @@ export function usePressHelper(props: PressablePressProps): [PressablePressProps ); const _onPressOut = React.useCallback( - (e) => { + (e: GestureResponderEvent) => { setPressState({ pressed: false }); onPressOut?.(e); }, diff --git a/packages/utils/interactive-hooks/tsconfig.json b/packages/utils/interactive-hooks/tsconfig.json index e7d430457d..32112995aa 100644 --- a/packages/utils/interactive-hooks/tsconfig.json +++ b/packages/utils/interactive-hooks/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@fluentui-react-native/scripts/tsconfig", + "extends": "@fluentui-react-native/scripts/tsconfig-strict", "compilerOptions": { "outDir": "lib", "rootDir": "src", diff --git a/yarn.lock b/yarn.lock index ce3ba0605c..b4719b5bd1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5074,7 +5074,7 @@ __metadata: react-native-macos: "npm:^0.81.0" react-native-svg: "npm:^15.12.1" react-native-svg-transformer: "npm:^1.0.0" - react-native-test-app: "npm:^5.2.3" + react-native-test-app: "catalog:" react-native-windows: "npm:^0.81.0" react-test-renderer: "npm:19.1.4" webdriverio: "catalog:" @@ -5165,7 +5165,7 @@ __metadata: react-native: "npm:^0.81.6" react-native-svg: "npm:^15.12.1" react-native-svg-transformer: "npm:^1.0.0" - react-native-test-app: "npm:^5.2.3" + react-native-test-app: "catalog:" react-test-renderer: "npm:19.1.4" rimraf: "catalog:" languageName: unknown @@ -5228,7 +5228,7 @@ __metadata: react-native-macos: "npm:^0.81.0" react-native-svg: "npm:^15.12.1" react-native-svg-transformer: "npm:^1.0.0" - react-native-test-app: "npm:^5.2.3" + react-native-test-app: "catalog:" react-native-windows: "npm:^0.81.0" react-test-renderer: "npm:19.1.4" webdriverio: "catalog:" @@ -20717,9 +20717,9 @@ __metadata: languageName: node linkType: hard -"react-native-test-app@npm:^5.2.3": - version: 5.2.3 - resolution: "react-native-test-app@npm:5.2.3" +"react-native-test-app@npm:^5.3.3": + version: 5.3.3 + resolution: "react-native-test-app@npm:5.3.3" dependencies: "@isaacs/cliui": "npm:^9.0.0" "@rnx-kit/react-native-host": "npm:^0.5.19" @@ -20733,7 +20733,7 @@ __metadata: "@callstack/react-native-visionos": 0.76 - 0.79 "@expo/config-plugins": ">=5.0" react: 18.2 - 19.2 - react-native: 0.76 - 0.85 || >=0.85.0-0 <0.86.0 + react-native: 0.76 - 0.86 || >=0.86.0-0 <0.87.0 react-native-macos: ^0.0.0-0 || 0.76 - 0.81 react-native-windows: ^0.0.0-0 || 0.76 - 0.83 peerDependenciesMeta: @@ -20750,7 +20750,7 @@ __metadata: init: scripts/init.mjs init-test-app: scripts/init.mjs install-windows-test-app: windows/app.mjs - checksum: 10c0/a87c47f93eba76bf2ea7a2f7bfdb2d1925b6bbeb00d67b9f365c5016e1a4ba063f5f1c1d7a86aab3f782b85921b1cbe802290010cca320ae66d762c24ee5683e + checksum: 10c0/fb1a8aef116e92f599976b27987ebf303a29ce42a1482e1bc352cfd4f82200cf42b091010e0c4a21d6c85076e8269468d2c1044cbc9a56af77acd3a4d1a4fcb5 languageName: node linkType: hard