diff --git a/change/@fluentui-react-jsx-runtime-6b5ad738-3061-4164-9bd3-1433b8b6a54e.json b/change/@fluentui-react-jsx-runtime-6b5ad738-3061-4164-9bd3-1433b8b6a54e.json new file mode 100644 index 00000000000000..138e9432b24b62 --- /dev/null +++ b/change/@fluentui-react-jsx-runtime-6b5ad738-3061-4164-9bd3-1433b8b6a54e.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: static children problem on render function", + "packageName": "@fluentui/react-jsx-runtime", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-jsx-runtime/src/jsx-dev-runtime.ts b/packages/react-components/react-jsx-runtime/src/jsx-dev-runtime.ts index 187fec98d24e1e..c0f07594f777dc 100644 --- a/packages/react-components/react-jsx-runtime/src/jsx-dev-runtime.ts +++ b/packages/react-components/react-jsx-runtime/src/jsx-dev-runtime.ts @@ -1,6 +1,7 @@ -import { DevRuntime } from './utils/DevRuntime'; import { createJSX } from './jsx/createJSX'; +import { jsxDEVSlot } from './jsx/jsxDEVSlot'; +import { DevRuntime } from './utils/DevRuntime'; export { Fragment } from 'react'; -export const jsxDEV = createJSX(DevRuntime.jsxDEV); +export const jsxDEV = createJSX(DevRuntime.jsxDEV, jsxDEVSlot); diff --git a/packages/react-components/react-jsx-runtime/src/jsx-runtime.test.tsx b/packages/react-components/react-jsx-runtime/src/jsx-runtime.test.tsx index 5f72f4d437929e..885a84981edb75 100644 --- a/packages/react-components/react-jsx-runtime/src/jsx-runtime.test.tsx +++ b/packages/react-components/react-jsx-runtime/src/jsx-runtime.test.tsx @@ -266,20 +266,29 @@ describe('createElement with assertSlots', () => { }); it('keeps children from a render template in a render callback', () => { - type TestComponentSlots = { outer: NonNullable>; inner: NonNullable> }; + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { + /* noop */ + }); + type TestComponentSlots = { + outer: NonNullable>; + inner1: NonNullable>; + inner2: NonNullable>; + }; type TestComponentState = ComponentState; type TestComponentProps = ComponentProps>; const TestComponent = (props: TestComponentProps) => { const state: TestComponentState = { - components: { outer: 'div', inner: 'div' }, - inner: slot.always(props.inner, { defaultProps: { id: 'inner' }, elementType: 'div' }), + components: { outer: 'div', inner1: 'div', inner2: 'div' }, + inner1: slot.always(props.inner1, { defaultProps: { id: 'inner-1' }, elementType: 'div' }), + inner2: slot.always(props.inner2, { defaultProps: { id: 'inner-2' }, elementType: 'div' }), outer: slot.always(props.outer, { defaultProps: { id: 'outer' }, elementType: 'div' }), }; assertSlots(state); return ( - + + ); }; @@ -289,17 +298,33 @@ describe('createElement with assertSlots', () => { )); - const result = render(); + const result = render( + , + ); + // your test code here + expect(errorSpy).not.toHaveBeenCalled(); + errorSpy.mockRestore(); expect(children).toHaveBeenCalledTimes(1); expect(children.mock.calls[0][0]).toBe('div'); expect(children.mock.calls[0][1].id).toBe('outer'); expect(children.mock.calls[0][1].children).toMatchInlineSnapshot(` -
- Inner children -
+ +
+ Inner children 1 +
+
+ Inner children 2 +
+
`); expect(result.container.firstChild).toMatchInlineSnapshot(` @@ -310,9 +335,14 @@ describe('createElement with assertSlots', () => { id="outer" >
- Inner children + Inner children 1 +
+
+ Inner children 2
diff --git a/packages/react-components/react-jsx-runtime/src/jsx-runtime.ts b/packages/react-components/react-jsx-runtime/src/jsx-runtime.ts index 7bc354d9158ae9..d0eff5b9fd627a 100644 --- a/packages/react-components/react-jsx-runtime/src/jsx-runtime.ts +++ b/packages/react-components/react-jsx-runtime/src/jsx-runtime.ts @@ -1,7 +1,9 @@ -import { Runtime } from './utils/Runtime'; import { createJSX } from './jsx/createJSX'; +import { jsxSlot } from './jsx/jsxSlot'; +import { jsxsSlot } from './jsx/jsxsSlot'; +import { Runtime } from './utils/Runtime'; export { Fragment } from 'react'; -export const jsx = createJSX(Runtime.jsx); -export const jsxs = createJSX(Runtime.jsxs); +export const jsx = createJSX(Runtime.jsx, jsxSlot); +export const jsxs = createJSX(Runtime.jsxs, jsxsSlot); diff --git a/packages/react-components/react-jsx-runtime/src/jsx/createJSX.ts b/packages/react-components/react-jsx-runtime/src/jsx/createJSX.ts index 3b334ea722e090..6401d1181ae0e8 100644 --- a/packages/react-components/react-jsx-runtime/src/jsx/createJSX.ts +++ b/packages/react-components/react-jsx-runtime/src/jsx/createJSX.ts @@ -1,39 +1,11 @@ +import { isSlot } from '@fluentui/react-utilities'; import * as React from 'react'; -import { isSlot, type SlotComponentType, type UnknownSlotProps } from '@fluentui/react-utilities'; -import { getMetadataFromSlotComponent } from '../utils/getMetadataFromSlotComponent'; -import type { JSXRuntime } from '../utils/types'; import { createCompatSlotComponent } from '../utils/createCompatSlotComponent'; +import { JSXRuntime, JSXSlotRuntime } from '../utils/types'; -/** - * @internal - */ -export const createJSX = (runtime: JSXRuntime) => { - const jsxFromSlotComponent = ( - type: SlotComponentType, - overrideProps: Props | null, - key?: React.Key, - source?: unknown, - self?: unknown, - ): React.ReactElement => { - const { elementType, renderFunction, props: slotProps } = getMetadataFromSlotComponent(type); - - const props: Props = { ...slotProps, ...overrideProps }; - - if (renderFunction) { - return runtime( - React.Fragment, - { - children: renderFunction(elementType, props), - }, - key, - source, - self, - ) as React.ReactElement; - } - return runtime(elementType, props, key, source, self); - }; - - return ( +export const createJSX = + (runtime: JSXRuntime, slotRuntime: JSXSlotRuntime) => + ( type: React.ElementType, overrideProps: Props | null, key?: React.Key, @@ -44,11 +16,10 @@ export const createJSX = (runtime: JSXRuntime) => { // this is for backwards compatibility with getSlotsNext // it should be removed once getSlotsNext is obsolete if (isSlot(overrideProps)) { - return jsxFromSlotComponent(createCompatSlotComponent(type, overrideProps), null, key, source, self); + return slotRuntime(createCompatSlotComponent(type, overrideProps), null, key, source, self); } if (isSlot(type)) { - return jsxFromSlotComponent(type, overrideProps, key, source, self); + return slotRuntime(type, overrideProps, key, source, self); } - return runtime(type, overrideProps, key, source, self); + return runtime(type, overrideProps, key); }; -}; diff --git a/packages/react-components/react-jsx-runtime/src/jsx/jsxDEVSlot.ts b/packages/react-components/react-jsx-runtime/src/jsx/jsxDEVSlot.ts new file mode 100644 index 00000000000000..49f3e1378ff8a6 --- /dev/null +++ b/packages/react-components/react-jsx-runtime/src/jsx/jsxDEVSlot.ts @@ -0,0 +1,48 @@ +import * as React from 'react'; +import { SlotComponentType, UnknownSlotProps } from '@fluentui/react-utilities'; +import { getMetadataFromSlotComponent } from '../utils/getMetadataFromSlotComponent'; +import { DevRuntime } from '../utils/DevRuntime'; + +export const jsxDEVSlot = ( + type: SlotComponentType, + overrideProps: Props | null, + key?: React.Key, + source?: unknown, + self?: unknown, +): React.ReactElement => { + const { elementType, renderFunction, props: slotProps } = getMetadataFromSlotComponent(type); + + const props: Props = { ...slotProps, ...overrideProps }; + + if (renderFunction) { + // if runtime is static + if (source === true) { + return DevRuntime.jsxDEV( + React.Fragment, + { + children: renderFunction(elementType, { + ...props, + /** + * If the runtime is static then children is an array and this array won't be keyed. + * Then we should wrap children by a static fragment + * as there's no way to know if renderFunction will render statically or dynamically + */ + children: DevRuntime.jsxDEV(React.Fragment, { children: props.children }, undefined, true, self), + }), + }, + key, + false, // by marking source as false we're declaring that this render is dynamic + self, + ) as React.ReactElement; + } + // if runtime is dynamic (source = false) things are simpler + return DevRuntime.jsxDEV( + React.Fragment, + { children: renderFunction(elementType, props) }, + key, + source, + self, + ) as React.ReactElement; + } + return DevRuntime.jsxDEV(elementType, props, key, source, self); +}; diff --git a/packages/react-components/react-jsx-runtime/src/jsx/jsxSlot.ts b/packages/react-components/react-jsx-runtime/src/jsx/jsxSlot.ts new file mode 100644 index 00000000000000..ebfa184124b92d --- /dev/null +++ b/packages/react-components/react-jsx-runtime/src/jsx/jsxSlot.ts @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { SlotComponentType, UnknownSlotProps } from '@fluentui/react-utilities'; +import { getMetadataFromSlotComponent } from '../utils/getMetadataFromSlotComponent'; +import { Runtime } from '../utils/Runtime'; + +export const jsxSlot = ( + type: SlotComponentType, + overrideProps: Props | null, + key?: React.Key, +): React.ReactElement => { + const { elementType, renderFunction, props: slotProps } = getMetadataFromSlotComponent(type); + + const props: Props = { ...slotProps, ...overrideProps }; + + if (renderFunction) { + return Runtime.jsx( + React.Fragment, + { children: renderFunction(elementType, props) }, + key, + ) as React.ReactElement; + } + return Runtime.jsx(elementType, props, key); +}; diff --git a/packages/react-components/react-jsx-runtime/src/jsx/jsxsSlot.ts b/packages/react-components/react-jsx-runtime/src/jsx/jsxsSlot.ts new file mode 100644 index 00000000000000..756df8bc6531b9 --- /dev/null +++ b/packages/react-components/react-jsx-runtime/src/jsx/jsxsSlot.ts @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { SlotComponentType, UnknownSlotProps } from '@fluentui/react-utilities'; +import { getMetadataFromSlotComponent } from '../utils/getMetadataFromSlotComponent'; +import { Runtime } from '../utils/Runtime'; + +export const jsxsSlot = ( + type: SlotComponentType, + overrideProps: Props | null, + key?: React.Key, +): React.ReactElement => { + const { elementType, renderFunction, props: slotProps } = getMetadataFromSlotComponent(type); + + const props: Props = { ...slotProps, ...overrideProps }; + + if (renderFunction) { + /** + * In static runtime then children is an array and this array won't be keyed. + * We should wrap children by a static fragment + * as there's no way to know if renderFunction will render statically or dynamically + */ + return Runtime.jsx( + React.Fragment, + { + children: renderFunction(elementType, { + ...props, + children: Runtime.jsxs(React.Fragment, { children: props.children }, undefined), + }), + }, + key, + ) as React.ReactElement; + } + return Runtime.jsxs(elementType, props, key); +}; diff --git a/packages/react-components/react-jsx-runtime/src/utils/types.ts b/packages/react-components/react-jsx-runtime/src/utils/types.ts index 4099a658aeb45c..218d95aa9e587d 100644 --- a/packages/react-components/react-jsx-runtime/src/utils/types.ts +++ b/packages/react-components/react-jsx-runtime/src/utils/types.ts @@ -1,4 +1,5 @@ import type * as React from 'react'; +import type { SlotComponentType, UnknownSlotProps } from '@fluentui/react-utilities'; export type JSXRuntime =

( type: React.ElementType

, @@ -7,3 +8,11 @@ export type JSXRuntime =

( source?: unknown, self?: unknown, ) => React.ReactElement

; + +export type JSXSlotRuntime = ( + type: SlotComponentType, + overrideProps: Props | null, + key?: React.Key, + source?: unknown, + self?: unknown, +) => React.ReactElement;