From 67b355f2ae6bebd5365ec854f948c40ccbaf913a Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Thu, 10 Aug 2023 15:58:30 +0000 Subject: [PATCH] feat: supports new automatic JSX runtime --- ...-911c855a-9f72-4f62-8d13-d9f768887ada.json | 7 + .../etc/react-jsx-runtime.api.md | 2 +- .../react-jsx-runtime/package.json | 12 + .../src/createElement.test.tsx | 18 +- .../react-jsx-runtime/src/createElement.ts | 50 +-- .../react-jsx-runtime/src/jsx-dev-runtime.ts | 26 ++ .../src/jsx-runtime.test.tsx | 341 ++++++++++++++++++ .../react-jsx-runtime/src/jsx-runtime.ts | 34 ++ .../src/jsx/createElementFromSlotComponent.ts | 28 ++ .../src/jsx/jsxDEVFromSlotComponent.ts | 30 ++ .../src/jsx/jsxDynamicFromSlotComponent.ts | 29 ++ .../src/jsx/jsxStaticFromSlotComponent.ts | 29 ++ .../react-jsx-runtime/src/utils/DevRuntime.ts | 6 + .../react-jsx-runtime/src/utils/Runtime.ts | 7 + .../src/utils/createCompatSlotComponent.ts | 13 + .../getMetadataFromSlotComponent.test.ts | 62 ++++ .../src/utils/getMetadataFromSlotComponent.ts | 25 ++ .../react-jsx-runtime/src/utils/types.ts | 9 + tsconfig.base.all.json | 4 + tsconfig.base.json | 4 + 20 files changed, 678 insertions(+), 58 deletions(-) create mode 100644 change/@fluentui-react-jsx-runtime-911c855a-9f72-4f62-8d13-d9f768887ada.json create mode 100644 packages/react-components/react-jsx-runtime/src/jsx-dev-runtime.ts create mode 100644 packages/react-components/react-jsx-runtime/src/jsx-runtime.test.tsx create mode 100644 packages/react-components/react-jsx-runtime/src/jsx-runtime.ts create mode 100644 packages/react-components/react-jsx-runtime/src/jsx/createElementFromSlotComponent.ts create mode 100644 packages/react-components/react-jsx-runtime/src/jsx/jsxDEVFromSlotComponent.ts create mode 100644 packages/react-components/react-jsx-runtime/src/jsx/jsxDynamicFromSlotComponent.ts create mode 100644 packages/react-components/react-jsx-runtime/src/jsx/jsxStaticFromSlotComponent.ts create mode 100644 packages/react-components/react-jsx-runtime/src/utils/DevRuntime.ts create mode 100644 packages/react-components/react-jsx-runtime/src/utils/Runtime.ts create mode 100644 packages/react-components/react-jsx-runtime/src/utils/createCompatSlotComponent.ts create mode 100644 packages/react-components/react-jsx-runtime/src/utils/getMetadataFromSlotComponent.test.ts create mode 100644 packages/react-components/react-jsx-runtime/src/utils/getMetadataFromSlotComponent.ts create mode 100644 packages/react-components/react-jsx-runtime/src/utils/types.ts diff --git a/change/@fluentui-react-jsx-runtime-911c855a-9f72-4f62-8d13-d9f768887ada.json b/change/@fluentui-react-jsx-runtime-911c855a-9f72-4f62-8d13-d9f768887ada.json new file mode 100644 index 00000000000000..0fbb5abd271f58 --- /dev/null +++ b/change/@fluentui-react-jsx-runtime-911c855a-9f72-4f62-8d13-d9f768887ada.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "feat: supports new automatic JSX runtime", + "packageName": "@fluentui/react-jsx-runtime", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-jsx-runtime/etc/react-jsx-runtime.api.md b/packages/react-components/react-jsx-runtime/etc/react-jsx-runtime.api.md index 9ec8f93f3f9b8c..df39490861ddf4 100644 --- a/packages/react-components/react-jsx-runtime/etc/react-jsx-runtime.api.md +++ b/packages/react-components/react-jsx-runtime/etc/react-jsx-runtime.api.md @@ -8,7 +8,7 @@ import { Fragment } from 'react'; import * as React_2 from 'react'; // @public (undocumented) -export function createElement

(type: React_2.ElementType

, props?: P | null, ...children: React_2.ReactNode[]): React_2.ReactElement

| null; +export function createElement

(type: React_2.ElementType

, props?: P | null, ...children: React_2.ReactNode[]): React_2.ReactElement

; export { Fragment } diff --git a/packages/react-components/react-jsx-runtime/package.json b/packages/react-components/react-jsx-runtime/package.json index 8662a4aef49312..339a3fd0f9ddef 100644 --- a/packages/react-components/react-jsx-runtime/package.json +++ b/packages/react-components/react-jsx-runtime/package.json @@ -51,6 +51,18 @@ "import": "./lib/index.js", "require": "./lib-commonjs/index.js" }, + "./jsx-dev-runtime": { + "types": "./dist/jsx-dev-runtime.d.ts", + "node": "./lib-commonjs/jsx-dev-runtime.js", + "import": "./lib/jsx-dev-runtime.js", + "require": "./lib-commonjs/jsx-dev-runtime.js" + }, + "./jsx-runtime": { + "types": "./dist/jsx-runtime.d.ts", + "node": "./lib-commonjs/jsx-runtime.js", + "import": "./lib/jsx-runtime.js", + "require": "./lib-commonjs/jsx-runtime.js" + }, "./package.json": "./package.json" } } diff --git a/packages/react-components/react-jsx-runtime/src/createElement.test.tsx b/packages/react-components/react-jsx-runtime/src/createElement.test.tsx index 75c605fd981310..d86020619a34a5 100644 --- a/packages/react-components/react-jsx-runtime/src/createElement.test.tsx +++ b/packages/react-components/react-jsx-runtime/src/createElement.test.tsx @@ -1,17 +1,9 @@ /** @jsxRuntime classic */ -/** @jsxFrag Fragment */ /** @jsx createElement */ import { render } from '@testing-library/react'; -import { - ComponentProps, - ComponentState, - Slot, - assertSlots, - getSlotsNext, - resolveShorthand, - slot, -} from '@fluentui/react-utilities'; +import { assertSlots, getSlotsNext, resolveShorthand, slot } from '@fluentui/react-utilities'; +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; import { createElement } from './createElement'; describe('createElement with getSlotsNext', () => { @@ -55,6 +47,7 @@ describe('createElement with getSlotsNext', () => {

1
2
+
3
, ); @@ -66,6 +59,9 @@ describe('createElement with getSlotsNext', () => {
2
+
+ 3 +
`); }); @@ -347,7 +343,5 @@ describe('createElement with assertSlots', () => { expect(result.container.firstChild).toMatchInlineSnapshot(``); }); - - it.todo("should pass 'as' property to base element that aren't html element"); }); }); diff --git a/packages/react-components/react-jsx-runtime/src/createElement.ts b/packages/react-components/react-jsx-runtime/src/createElement.ts index a234a7eec2c10a..ff492abb1265d8 100644 --- a/packages/react-components/react-jsx-runtime/src/createElement.ts +++ b/packages/react-components/react-jsx-runtime/src/createElement.ts @@ -1,61 +1,21 @@ import * as React from 'react'; -import { - UnknownSlotProps, - isSlot, - SlotComponentType, - SLOT_ELEMENT_TYPE_SYMBOL, - SLOT_RENDER_FUNCTION_SYMBOL, -} from '@fluentui/react-utilities'; +import { isSlot } from '@fluentui/react-utilities'; +import { createElementFromSlotComponent } from './jsx/createElementFromSlotComponent'; +import { createCompatSlotComponent } from './utils/createCompatSlotComponent'; export function createElement

( type: React.ElementType

, props?: P | null, ...children: React.ReactNode[] -): React.ReactElement

| null { +): React.ReactElement

{ // TODO: // this is for backwards compatibility with getSlotsNext // it should be removed once getSlotsNext is obsolete if (isSlot

(props)) { - return createElementFromSlotComponent( - { ...props, [SLOT_ELEMENT_TYPE_SYMBOL]: type } as SlotComponentType

, - children, - ); + return createElementFromSlotComponent(createCompatSlotComponent(type, props), children); } if (isSlot

(type)) { return createElementFromSlotComponent(type, children); } return React.createElement(type, props, ...children); } - -function createElementFromSlotComponent( - type: SlotComponentType, - overrideChildren: React.ReactNode[], -): React.ReactElement | null { - const { - as, - [SLOT_ELEMENT_TYPE_SYMBOL]: baseElementType, - [SLOT_RENDER_FUNCTION_SYMBOL]: renderFunction, - ...propsWithoutMetadata - } = type; - const props = propsWithoutMetadata as UnknownSlotProps as Props; - - const elementType = typeof baseElementType === 'string' ? as ?? baseElementType : baseElementType; - - if (typeof elementType !== 'string' && as) { - props.as = as; - } - - if (renderFunction) { - if (overrideChildren.length > 0) { - props.children = React.createElement(React.Fragment, {}, ...overrideChildren); - } - - return React.createElement( - React.Fragment, - {}, - renderFunction(elementType as React.ElementType, props), - ) as React.ReactElement; - } - - return React.createElement(elementType, props, ...overrideChildren); -} 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 new file mode 100644 index 00000000000000..06a103f49ba479 --- /dev/null +++ b/packages/react-components/react-jsx-runtime/src/jsx-dev-runtime.ts @@ -0,0 +1,26 @@ +import type * as React from 'react'; +import { isSlot } from '@fluentui/react-utilities'; +import { jsxDEVFromSlotComponent } from './jsx/jsxDEVFromSlotComponent'; +import { createCompatSlotComponent } from './utils/createCompatSlotComponent'; +import { DevRuntime } from './utils/DevRuntime'; + +export { Fragment } from 'react'; + +export function jsxDEV

( + type: React.ElementType

, + props: P, + key?: React.Key, + source?: boolean, + self?: unknown, +): React.ReactElement

{ + // TODO: + // this is for backwards compatibility with getSlotsNext + // it should be removed once getSlotsNext is obsolete + if (isSlot

(props)) { + return jsxDEVFromSlotComponent(createCompatSlotComponent(type, props), null, key, source, self); + } + if (isSlot

(type)) { + return jsxDEVFromSlotComponent(type, props, key, source, self); + } + return DevRuntime.jsxDEV(type, props, key, source, self); +} 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 new file mode 100644 index 00000000000000..5f72f4d437929e --- /dev/null +++ b/packages/react-components/react-jsx-runtime/src/jsx-runtime.test.tsx @@ -0,0 +1,341 @@ +/** @jsxImportSource @fluentui/react-jsx-runtime */ + +import { render } from '@testing-library/react'; +import { assertSlots, getSlotsNext, resolveShorthand, slot } from '@fluentui/react-utilities'; +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; + +describe('createElement with getSlotsNext', () => { + describe('general behavior tests', () => { + it('handles a string', () => { + const result = render(

Hello world
); + + expect(result.container.firstChild).toMatchInlineSnapshot(` +
+ Hello world +
+ `); + }); + + it('handles an array', () => { + const result = render( +
+ {Array.from({ length: 3 }, (_, i) => ( +
{i}
+ ))} +
, + ); + + expect(result.container.firstChild).toMatchInlineSnapshot(` +
+
+ 0 +
+
+ 1 +
+
+ 2 +
+
+ `); + }); + + it('handles an array of children', () => { + const result = render( +
+
1
+
2
+
, + ); + + expect(result.container.firstChild).toMatchInlineSnapshot(` +
+
+ 1 +
+
+ 2 +
+
+ `); + }); + }); + + describe('custom behavior tests', () => { + it('keeps children from "defaultProps" in a render callback', () => { + type TestComponentSlots = { slot: Slot<'div'> }; + type TestComponentState = ComponentState; + type TestComponentProps = ComponentProps>; + + const TestComponent = (props: TestComponentProps) => { + const state: TestComponentState = { + components: { slot: 'div' }, + + slot: resolveShorthand(props.slot, { + defaultProps: { children: 'Default Children', id: 'slot' }, + }), + }; + const { slots, slotProps } = getSlotsNext(state); + + return ; + }; + + const children = jest.fn().mockImplementation((Component, props) => ( +
+ +
+ )); + const result = render(); + + expect(children).toHaveBeenCalledTimes(1); + expect(children).toHaveBeenCalledWith('div', { children: 'Default Children', id: 'slot' }); + + expect(result.container.firstChild).toMatchInlineSnapshot(` +
+
+ Default Children +
+
+ `); + }); + + it('keeps children from a render template in a render callback', () => { + type TestComponentSlots = { outer: Slot<'div'>; inner: Slot<'div'> }; + type TestComponentState = ComponentState; + type TestComponentProps = ComponentProps>; + + const TestComponent = (props: TestComponentProps) => { + const state: TestComponentState = { + components: { inner: 'div', outer: 'div' }, + + inner: resolveShorthand(props.inner, { defaultProps: { id: 'inner' } }), + outer: resolveShorthand(props.outer, { defaultProps: { id: 'outer' } }), + }; + const { slots, slotProps } = getSlotsNext(state); + + return ( + + + + ); + }; + + const children = jest.fn().mockImplementation((Component, props) => ( +
+ +
+ )); + const result = render(); + + 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 +
+ `); + + expect(result.container.firstChild).toMatchInlineSnapshot(` +
+
+
+ Inner children +
+
+
+ `); + }); + }); +}); + +describe('createElement with assertSlots', () => { + describe('general behavior tests', () => { + it('handles a string', () => { + const result = render(
Hello world
); + + expect(result.container.firstChild).toMatchInlineSnapshot(` +
+ Hello world +
+ `); + }); + + it('handles an array', () => { + const result = render( +
+ {Array.from({ length: 3 }, (_, i) => ( +
{i}
+ ))} +
, + ); + + expect(result.container.firstChild).toMatchInlineSnapshot(` +
+
+ 0 +
+
+ 1 +
+
+ 2 +
+
+ `); + }); + + it('handles an array of children', () => { + const result = render( +
+
0
+
1
+
2
+
, + ); + + expect(result.container.firstChild).toMatchInlineSnapshot(` +
+
+ 0 +
+
+ 1 +
+
+ 2 +
+
+ `); + }); + }); + + describe('custom behavior tests', () => { + it('keeps children from "defaultProps" in a render callback', () => { + type TestComponentSlots = { + someSlot: NonNullable>; + }; + type TestComponentProps = ComponentProps>; + type TestComponentState = ComponentState; + + const TestComponent = (props: TestComponentProps) => { + const state: TestComponentState = { + components: { someSlot: 'div' }, + someSlot: slot.always(props.someSlot, { + elementType: 'div', + defaultProps: { children: 'Default Children', id: 'slot' }, + }), + }; + assertSlots(state); + return ; + }; + + const children = jest.fn().mockImplementation((Component, props) => ( +
+ +
+ )); + const result = render(); + + expect(children).toHaveBeenCalledTimes(1); + expect(children).toHaveBeenCalledWith('div', { children: 'Default Children', id: 'slot' }); + + expect(result.container.firstChild).toMatchInlineSnapshot(` +
+
+ Default Children +
+
+ `); + }); + + it('keeps children from a render template in a render callback', () => { + type TestComponentSlots = { outer: NonNullable>; inner: 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' }), + outer: slot.always(props.outer, { defaultProps: { id: 'outer' }, elementType: 'div' }), + }; + assertSlots(state); + return ( + + + + ); + }; + + const children = jest.fn().mockImplementation((Component, props) => ( +
+ +
+ )); + const result = render(); + + 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 +
+ `); + + expect(result.container.firstChild).toMatchInlineSnapshot(` +
+
+
+ Inner children +
+
+
+ `); + }); + + it("should support 'as' property to opt-out of base element type", () => { + type TestComponentSlots = { slot: NonNullable> }; + type TestComponentState = ComponentState; + type TestComponentProps = ComponentProps>; + + const TestComponent = (props: TestComponentProps) => { + const state: TestComponentState = { + components: { slot: 'div' }, + slot: slot.always(props.slot, { elementType: 'div' }), + }; + assertSlots(state); + return ; + }; + + const result = render(); + + expect(result.container.firstChild).toMatchInlineSnapshot(``); + }); + }); +}); diff --git a/packages/react-components/react-jsx-runtime/src/jsx-runtime.ts b/packages/react-components/react-jsx-runtime/src/jsx-runtime.ts new file mode 100644 index 00000000000000..6c5bb260dc56a3 --- /dev/null +++ b/packages/react-components/react-jsx-runtime/src/jsx-runtime.ts @@ -0,0 +1,34 @@ +import type * as React from 'react'; +import { isSlot } from '@fluentui/react-utilities'; +import { jsxDynamicFromSlotComponent } from './jsx/jsxDynamicFromSlotComponent'; +import { jsxStaticFromSlotComponent } from './jsx/jsxStaticFromSlotComponent'; +import { createCompatSlotComponent } from './utils/createCompatSlotComponent'; +import { Runtime } from './utils/Runtime'; + +export { Fragment } from 'react'; + +export function jsx

(type: React.ElementType

, props: P, key?: React.Key): React.ReactElement

{ + // TODO: + // this is for backwards compatibility with getSlotsNext + // it should be removed once getSlotsNext is obsolete + if (isSlot

(props)) { + return jsxDynamicFromSlotComponent(createCompatSlotComponent(type, props), null, key); + } + if (isSlot

(type)) { + return jsxDynamicFromSlotComponent(type, props, key); + } + return Runtime.jsx(type, props, key); +} + +export function jsxs

(type: React.ElementType

, props: P, key?: React.Key): React.ReactElement

{ + // TODO: + // this is for backwards compatibility with getSlotsNext + // it should be removed once getSlotsNext is obsolete + if (isSlot

(props)) { + return jsxStaticFromSlotComponent(createCompatSlotComponent(type, props), null, key); + } + if (isSlot

(type)) { + return jsxStaticFromSlotComponent(type, props, key); + } + return Runtime.jsxs(type, props, key); +} diff --git a/packages/react-components/react-jsx-runtime/src/jsx/createElementFromSlotComponent.ts b/packages/react-components/react-jsx-runtime/src/jsx/createElementFromSlotComponent.ts new file mode 100644 index 00000000000000..3c9b4e73fbafc4 --- /dev/null +++ b/packages/react-components/react-jsx-runtime/src/jsx/createElementFromSlotComponent.ts @@ -0,0 +1,28 @@ +import * as React from 'react'; +import type { SlotComponentType, UnknownSlotProps } from '@fluentui/react-utilities'; +import { getMetadataFromSlotComponent } from '../utils/getMetadataFromSlotComponent'; + +/** + * @internal + * creates a ReactElement from a slot declaration + */ +export function createElementFromSlotComponent( + type: SlotComponentType, + overrideChildren: React.ReactNode[], +): React.ReactElement { + const { elementType, renderFunction, props } = getMetadataFromSlotComponent(type); + + if (renderFunction) { + if (overrideChildren.length > 0) { + props.children = React.createElement(React.Fragment, {}, ...overrideChildren); + } + + return React.createElement( + React.Fragment, + {}, + renderFunction(elementType as React.ElementType, props), + ) as React.ReactElement; + } + + return React.createElement(elementType, props, ...overrideChildren); +} diff --git a/packages/react-components/react-jsx-runtime/src/jsx/jsxDEVFromSlotComponent.ts b/packages/react-components/react-jsx-runtime/src/jsx/jsxDEVFromSlotComponent.ts new file mode 100644 index 00000000000000..dd9f5a47b69f79 --- /dev/null +++ b/packages/react-components/react-jsx-runtime/src/jsx/jsxDEVFromSlotComponent.ts @@ -0,0 +1,30 @@ +import * as React from 'react'; +import type { SlotComponentType, UnknownSlotProps } from '@fluentui/react-utilities'; +import { getMetadataFromSlotComponent } from '../utils/getMetadataFromSlotComponent'; +import { DevRuntime } from '../utils/DevRuntime'; + +export function jsxDEVFromSlotComponent( + 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 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/jsxDynamicFromSlotComponent.ts b/packages/react-components/react-jsx-runtime/src/jsx/jsxDynamicFromSlotComponent.ts new file mode 100644 index 00000000000000..1898acb4cf0dff --- /dev/null +++ b/packages/react-components/react-jsx-runtime/src/jsx/jsxDynamicFromSlotComponent.ts @@ -0,0 +1,29 @@ +import * as React from 'react'; +import type { SlotComponentType, UnknownSlotProps } from '@fluentui/react-utilities'; +import { getMetadataFromSlotComponent } from '../utils/getMetadataFromSlotComponent'; +import { Runtime } from '../utils/Runtime'; + +/** + * @internal + */ +export function jsxDynamicFromSlotComponent( + 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/jsxStaticFromSlotComponent.ts b/packages/react-components/react-jsx-runtime/src/jsx/jsxStaticFromSlotComponent.ts new file mode 100644 index 00000000000000..4409294991baee --- /dev/null +++ b/packages/react-components/react-jsx-runtime/src/jsx/jsxStaticFromSlotComponent.ts @@ -0,0 +1,29 @@ +import * as React from 'react'; +import type { SlotComponentType, UnknownSlotProps } from '@fluentui/react-utilities'; +import { getMetadataFromSlotComponent } from '../utils/getMetadataFromSlotComponent'; +import { Runtime } from '../utils/Runtime'; + +/** + * @internal + */ +export function jsxStaticFromSlotComponent( + 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.jsxs( + React.Fragment, + { + children: renderFunction(elementType, props), + }, + key, + ) as React.ReactElement; + } + + return Runtime.jsxs(elementType, props, key); +} diff --git a/packages/react-components/react-jsx-runtime/src/utils/DevRuntime.ts b/packages/react-components/react-jsx-runtime/src/utils/DevRuntime.ts new file mode 100644 index 00000000000000..b62362bbdd10b6 --- /dev/null +++ b/packages/react-components/react-jsx-runtime/src/utils/DevRuntime.ts @@ -0,0 +1,6 @@ +import * as ReactDevRuntime from 'react/jsx-dev-runtime'; +import type { JSXRuntime } from './types'; + +export const DevRuntime = ReactDevRuntime as { + jsxDEV: JSXRuntime; +}; diff --git a/packages/react-components/react-jsx-runtime/src/utils/Runtime.ts b/packages/react-components/react-jsx-runtime/src/utils/Runtime.ts new file mode 100644 index 00000000000000..981ed7ee2a4963 --- /dev/null +++ b/packages/react-components/react-jsx-runtime/src/utils/Runtime.ts @@ -0,0 +1,7 @@ +import * as ReactRuntime from 'react/jsx-runtime'; +import type { JSXRuntime } from './types'; + +export const Runtime = ReactRuntime as { + jsx: JSXRuntime; + jsxs: JSXRuntime; +}; diff --git a/packages/react-components/react-jsx-runtime/src/utils/createCompatSlotComponent.ts b/packages/react-components/react-jsx-runtime/src/utils/createCompatSlotComponent.ts new file mode 100644 index 00000000000000..467c2813c728a9 --- /dev/null +++ b/packages/react-components/react-jsx-runtime/src/utils/createCompatSlotComponent.ts @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { SLOT_ELEMENT_TYPE_SYMBOL } from '@fluentui/react-utilities'; +import type { SlotComponentType } from '@fluentui/react-utilities'; + +// TODO: +// this is for backwards compatibility with getSlotsNext +// it should be removed once getSlotsNext is obsolete +export function createCompatSlotComponent

(type: React.ElementType

, props: P): SlotComponentType

{ + return { + ...props, + [SLOT_ELEMENT_TYPE_SYMBOL]: type, + } as SlotComponentType

; +} diff --git a/packages/react-components/react-jsx-runtime/src/utils/getMetadataFromSlotComponent.test.ts b/packages/react-components/react-jsx-runtime/src/utils/getMetadataFromSlotComponent.test.ts new file mode 100644 index 00000000000000..ff190f3ae41c06 --- /dev/null +++ b/packages/react-components/react-jsx-runtime/src/utils/getMetadataFromSlotComponent.test.ts @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { SLOT_ELEMENT_TYPE_SYMBOL, SLOT_RENDER_FUNCTION_SYMBOL } from '@fluentui/react-utilities'; +import type { SlotComponentType, SlotRenderFunction } from '@fluentui/react-utilities'; +import { getMetadataFromSlotComponent } from './getMetadataFromSlotComponent'; + +type TestProps = React.HTMLAttributes & { as?: 'div' | 'span' }; + +describe('getMetadataFromSlotComponent', () => { + it('gets metadata from slot component', () => { + expect( + getMetadataFromSlotComponent({ + [SLOT_ELEMENT_TYPE_SYMBOL]: 'div', + tabIndex: 0, + } as SlotComponentType), + ).toEqual({ + elementType: 'div', + props: { tabIndex: 0 }, + renderFunction: undefined, + }); + }); + + it('handles render props', () => { + expect( + getMetadataFromSlotComponent({ + [SLOT_ELEMENT_TYPE_SYMBOL]: 'div', + [SLOT_RENDER_FUNCTION_SYMBOL]: jest.fn() as SlotRenderFunction, + tabIndex: 0, + } as SlotComponentType), + ).toEqual({ + elementType: 'div', + props: { tabIndex: 0 }, + renderFunction: expect.any(Function), + }); + }); + it("should override elementType with 'as' when base element is HTML element", () => { + expect( + getMetadataFromSlotComponent({ + [SLOT_ELEMENT_TYPE_SYMBOL]: 'div', + as: 'span', + tabIndex: 0, + } as SlotComponentType), + ).toEqual({ + elementType: 'span', + props: { tabIndex: 0 }, + renderFunction: undefined, + }); + }); + it("should pass 'as' property to base element that aren't HTML element", () => { + const fn = (props: TestProps) => null; + expect( + getMetadataFromSlotComponent({ + [SLOT_ELEMENT_TYPE_SYMBOL]: fn, + as: 'div', + tabIndex: 0, + } as SlotComponentType), + ).toEqual({ + elementType: fn, + props: { tabIndex: 0, as: 'div' }, + renderFunction: undefined, + }); + }); +}); diff --git a/packages/react-components/react-jsx-runtime/src/utils/getMetadataFromSlotComponent.ts b/packages/react-components/react-jsx-runtime/src/utils/getMetadataFromSlotComponent.ts new file mode 100644 index 00000000000000..8c95fd3ba80049 --- /dev/null +++ b/packages/react-components/react-jsx-runtime/src/utils/getMetadataFromSlotComponent.ts @@ -0,0 +1,25 @@ +import type * as React from 'react'; +import { SLOT_ELEMENT_TYPE_SYMBOL, SLOT_RENDER_FUNCTION_SYMBOL } from '@fluentui/react-utilities'; +import type { SlotComponentType, UnknownSlotProps } from '@fluentui/react-utilities'; + +/** + * @internal + */ +export function getMetadataFromSlotComponent(type: SlotComponentType) { + const { + as, + [SLOT_ELEMENT_TYPE_SYMBOL]: baseElementType, + [SLOT_RENDER_FUNCTION_SYMBOL]: renderFunction, + ...propsWithoutMetadata + } = type; + const props = propsWithoutMetadata as UnknownSlotProps as Props; + + const elementType = ( + typeof baseElementType === 'string' ? as ?? baseElementType : baseElementType + ) as React.ElementType; + + if (typeof elementType !== 'string' && as) { + props.as = as; + } + return { elementType, props, renderFunction }; +} diff --git a/packages/react-components/react-jsx-runtime/src/utils/types.ts b/packages/react-components/react-jsx-runtime/src/utils/types.ts new file mode 100644 index 00000000000000..4099a658aeb45c --- /dev/null +++ b/packages/react-components/react-jsx-runtime/src/utils/types.ts @@ -0,0 +1,9 @@ +import type * as React from 'react'; + +export type JSXRuntime =

( + type: React.ElementType

, + props: P | null, + key?: React.Key, + source?: unknown, + self?: unknown, +) => React.ReactElement

; diff --git a/tsconfig.base.all.json b/tsconfig.base.all.json index b70a0a4b540677..93dfd6b78c44ac 100644 --- a/tsconfig.base.all.json +++ b/tsconfig.base.all.json @@ -113,6 +113,10 @@ "@fluentui/react-infobutton": ["packages/react-components/react-infobutton/src/index.ts"], "@fluentui/react-input": ["packages/react-components/react-input/src/index.ts"], "@fluentui/react-jsx-runtime": ["packages/react-components/react-jsx-runtime/src/index.ts"], + "@fluentui/react-jsx-runtime/jsx-runtime": ["packages/react-components/react-jsx-runtime/src/jsx-runtime.ts"], + "@fluentui/react-jsx-runtime/jsx-dev-runtime": [ + "packages/react-components/react-jsx-runtime/src/jsx-dev-runtime.ts" + ], "@fluentui/react-label": ["packages/react-components/react-label/src/index.ts"], "@fluentui/react-link": ["packages/react-components/react-link/src/index.ts"], "@fluentui/react-menu": ["packages/react-components/react-menu/src/index.ts"], diff --git a/tsconfig.base.json b/tsconfig.base.json index a0656b78d30c9e..717a1beb47ba1b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -47,6 +47,10 @@ "@fluentui/react-infobutton": ["packages/react-components/react-infobutton/src/index.ts"], "@fluentui/react-input": ["packages/react-components/react-input/src/index.ts"], "@fluentui/react-jsx-runtime": ["packages/react-components/react-jsx-runtime/src/index.ts"], + "@fluentui/react-jsx-runtime/jsx-runtime": ["packages/react-components/react-jsx-runtime/src/jsx-runtime.ts"], + "@fluentui/react-jsx-runtime/jsx-dev-runtime": [ + "packages/react-components/react-jsx-runtime/src/jsx-dev-runtime.ts" + ], "@fluentui/react-label": ["packages/react-components/react-label/src/index.ts"], "@fluentui/react-link": ["packages/react-components/react-link/src/index.ts"], "@fluentui/react-menu": ["packages/react-components/react-menu/src/index.ts"],