diff --git a/change/@fluentui-react-jsx-runtime-92a3c6b2-e767-4128-8320-7e237ef69eaa.json b/change/@fluentui-react-jsx-runtime-92a3c6b2-e767-4128-8320-7e237ef69eaa.json
new file mode 100644
index 00000000000000..3579190fc7d420
--- /dev/null
+++ b/change/@fluentui-react-jsx-runtime-92a3c6b2-e767-4128-8320-7e237ef69eaa.json
@@ -0,0 +1,7 @@
+{
+ "type": "prerelease",
+ "comment": "feat: implements custom JSX pragma",
+ "packageName": "@fluentui/react-jsx-runtime",
+ "email": "bernardo.sunderhus@gmail.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/@fluentui-react-utilities-6ec7d77e-2bd1-4bcc-a0fb-0f71642d43bd.json b/change/@fluentui-react-utilities-6ec7d77e-2bd1-4bcc-a0fb-0f71642d43bd.json
new file mode 100644
index 00000000000000..9b7eeb1cf33652
--- /dev/null
+++ b/change/@fluentui-react-utilities-6ec7d77e-2bd1-4bcc-a0fb-0f71642d43bd.json
@@ -0,0 +1,7 @@
+{
+ "type": "minor",
+ "comment": "feat: ensure compatibility with custom JSX pragma",
+ "packageName": "@fluentui/react-utilities",
+ "email": "bernardo.sunderhus@gmail.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/react-components/react-jsx-runtime/package.json b/packages/react-components/react-jsx-runtime/package.json
index 8ad4f1b06971dc..59ebdd0eb36666 100644
--- a/packages/react-components/react-jsx-runtime/package.json
+++ b/packages/react-components/react-jsx-runtime/package.json
@@ -1,7 +1,6 @@
{
"name": "@fluentui/react-jsx-runtime",
"version": "9.0.0-alpha.0",
- "private": true,
"description": "React components for building web experiences",
"main": "lib-commonjs/index.js",
"module": "lib/index.js",
@@ -30,6 +29,7 @@
"@fluentui/scripts-tasks": "*"
},
"dependencies": {
+ "@fluentui/react-utilities": "^9.7.3",
"@swc/helpers": "^0.4.14"
},
"peerDependencies": {
diff --git a/packages/react-components/react-jsx-runtime/src/index.ts b/packages/react-components/react-jsx-runtime/src/index.ts
index aacbad0068e241..388451abd35029 100644
--- a/packages/react-components/react-jsx-runtime/src/index.ts
+++ b/packages/react-components/react-jsx-runtime/src/index.ts
@@ -1,2 +1,2 @@
-// TODO: replace with real exports
-export {};
+export { jsx } from './jsx';
+export { Fragment } from 'react';
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..85b668ae011b31
--- /dev/null
+++ b/packages/react-components/react-jsx-runtime/src/jsx-dev-runtime.ts
@@ -0,0 +1,18 @@
+///
+
+import * as React from 'react';
+import { jsx as createElement, extractChildrenFromProps } from './jsx';
+
+export function jsxDEV
(
+ type: React.ElementType
,
+ props?: P | null,
+ key?: string,
+ isStaticChildren?: boolean,
+ source?: unknown,
+ self?: unknown,
+): React.ReactElement
| null {
+ // extractChildrenFromProps is required since jsxDev signature differs from React.createElement signature
+ return createElement(type, props, ...extractChildrenFromProps(props));
+}
+
+export { Fragment } from 'react/jsx-dev-runtime';
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..bae0a1772ecdd1
--- /dev/null
+++ b/packages/react-components/react-jsx-runtime/src/jsx-runtime.ts
@@ -0,0 +1,24 @@
+///
+
+import * as React from 'react';
+import { jsx as createElement, extractChildrenFromProps } from './jsx';
+
+export function jsx
(
+ type: React.ElementType
,
+ props?: P | null,
+ key?: string,
+): React.ReactElement
| null {
+ // extractChildrenFromProps is required since jsx signature differs from React.createElement signature
+ return createElement(type, props, ...extractChildrenFromProps(props));
+}
+
+export function jsxs
(
+ type: React.ElementType
,
+ props?: P | null,
+ key?: string,
+): React.ReactElement
| null {
+ // extractChildrenFromProps is required since jsxs signature differs from React.createElement signature
+ return createElement(type, props, ...extractChildrenFromProps(props));
+}
+
+export { Fragment } from 'react/jsx-runtime';
diff --git a/packages/react-components/react-jsx-runtime/src/jsx.test.tsx b/packages/react-components/react-jsx-runtime/src/jsx.test.tsx
new file mode 100644
index 00000000000000..729cf02315cff2
--- /dev/null
+++ b/packages/react-components/react-jsx-runtime/src/jsx.test.tsx
@@ -0,0 +1,84 @@
+/* eslint-disable jsdoc/check-tag-names */
+/** @jsxRuntime classic */
+/** @jsxFrag Fragment */
+/** @jsx jsx */
+/* eslint-enable jsdoc/check-tag-names */
+
+import {
+ ComponentProps,
+ ComponentState,
+ Slot,
+ getSlots,
+ getSlotsNext,
+ resolveShorthand,
+} from '@fluentui/react-utilities';
+import { jsx, Fragment } from './index';
+import { render } from '@testing-library/react';
+
+type TestComponentSlots = { slot: Slot<'div'> };
+type TestComponentState = ComponentState;
+type TestComponentProps = ComponentProps> & {
+ getSlots: typeof getSlots;
+};
+
+const TestComponent = (props: TestComponentProps) => {
+ const state: TestComponentState = {
+ components: {
+ slot: 'div',
+ },
+ slot: resolveShorthand(
+ props.slot ?? {
+ children: (C, p) => (
+ <>
+ before
+
+ after
+ >
+ ),
+ },
+ {
+ required: true,
+ defaultProps: {
+ children: this is internal children
,
+ },
+ },
+ ),
+ };
+ const { slots, slotProps } = props.getSlots(state);
+ return ;
+};
+
+describe('jsx', () => {
+ it('should lose internal children while using getSlots', () => {
+ const result = render();
+ expect(result.container).toMatchInlineSnapshot(`
+
+
+ before
+
+
+
+ after
+
+
+ `);
+ });
+ it('should keep internal children while using getSlotsNext', () => {
+ const result = render();
+ expect(result.container).toMatchInlineSnapshot(`
+
+
+ before
+
+
+
+ this is internal children
+
+
+
+ after
+
+
+ `);
+ });
+});
diff --git a/packages/react-components/react-jsx-runtime/src/jsx.ts b/packages/react-components/react-jsx-runtime/src/jsx.ts
new file mode 100644
index 00000000000000..47811383c2e7b6
--- /dev/null
+++ b/packages/react-components/react-jsx-runtime/src/jsx.ts
@@ -0,0 +1,89 @@
+import * as React from 'react';
+import {
+ SlotRenderFunction,
+ UnknownSlotProps,
+ SLOT_EXTERNAL_CHILDREN_SYMBOL,
+ SLOT_INTERNAL_CHILDREN_SYMBOL,
+} from '@fluentui/react-utilities';
+
+type WithMetadata = Props & {
+ [SLOT_EXTERNAL_CHILDREN_SYMBOL]: React.ReactNode | SlotRenderFunction;
+ [SLOT_INTERNAL_CHILDREN_SYMBOL]: React.ReactNode | SlotRenderFunction;
+};
+
+/**
+ * Equivalent to React.createElement but supporting v9 slot API
+ */
+export function jsx(
+ type: React.ElementType
,
+ props?: P | null,
+ ...children: React.ReactNode[]
+): React.ReactElement
| null {
+ return isSlotComponent(props)
+ ? jsxFromSlotComponent(type, props, ...children)
+ : React.createElement(type, props, ...children);
+}
+
+function jsxFromSlotComponent(
+ type: React.ElementType,
+ propsWithMetadata: WithMetadata,
+ ...overrideChildren: React.ReactNode[]
+): React.ReactElement | null {
+ const { children, renderFn, props } = normalizeChildren(propsWithMetadata, overrideChildren);
+ if (renderFn) {
+ return React.createElement(React.Fragment, {}, renderFn(type, { ...props, children })) as React.ReactElement;
+ }
+
+ return React.createElement(type, { ...props, children } as Props);
+}
+
+export function extractChildrenFromProps>(
+ props?: Props | null,
+): React.ReactNode[] {
+ if (!props) {
+ return [];
+ }
+ return Array.isArray(props.children) ? props.children : [props.children];
+}
+
+function normalizeChildren(
+ propsWithMetadata: WithMetadata,
+ overrideChildren: React.ReactNode[] = [],
+): {
+ props: Props;
+ children: React.ReactNode;
+ renderFn?: SlotRenderFunction;
+} {
+ const {
+ [SLOT_EXTERNAL_CHILDREN_SYMBOL]: slotExternalChildren,
+ [SLOT_INTERNAL_CHILDREN_SYMBOL]: slotInternalChildren,
+ ...props
+ } = propsWithMetadata;
+ const isRenderFn = typeof slotExternalChildren === 'function';
+ const notNormalizedChildren: React.ReactNode | React.ReactNode[] =
+ (overrideChildren.length > 0 ? overrideChildren : undefined) ??
+ (isRenderFn ? slotInternalChildren : slotExternalChildren) ??
+ slotInternalChildren ??
+ propsWithMetadata.children;
+ const normalizedChildren = Array.isArray(notNormalizedChildren)
+ ? React.createElement(React.Fragment, {}, ...notNormalizedChildren)
+ : notNormalizedChildren;
+ const renderFn = isRenderFn ? (slotExternalChildren as SlotRenderFunction) : undefined;
+ if (isRenderFn) {
+ const { children: _, ...propsWithoutChildren } = props as UnknownSlotProps;
+ return {
+ children: normalizedChildren,
+ renderFn,
+ props: propsWithoutChildren as Props,
+ };
+ }
+ return {
+ children: normalizedChildren,
+ renderFn,
+ props: props as UnknownSlotProps as Props,
+ };
+}
+
+export function isSlotComponent(props?: Props | null): props is WithMetadata {
+ return Boolean(props?.hasOwnProperty(SLOT_INTERNAL_CHILDREN_SYMBOL));
+}
diff --git a/packages/react-components/react-jsx-runtime/src/types.d.ts b/packages/react-components/react-jsx-runtime/src/types.d.ts
new file mode 100644
index 00000000000000..922fbea70b0a04
--- /dev/null
+++ b/packages/react-components/react-jsx-runtime/src/types.d.ts
@@ -0,0 +1,31 @@
+declare module 'react/jsx-runtime' {
+ import type * as React from 'react';
+
+ export const Fragment: symbol;
+
+ export function jsx(
+ type: React.ElementType
,
+ props?: P | null,
+ key?: string,
+ ): React.ReactElement
| null;
+
+ export function jsxs
(
+ type: React.ElementType
,
+ props?: P | null,
+ key?: string,
+ ): React.ReactElement
| null;
+}
+declare module 'react/jsx-dev-runtime' {
+ import type * as React from 'react';
+
+ export const Fragment: symbol;
+
+ export function jsxDEV
(
+ type: React.ElementType
,
+ props?: P | null,
+ key?: string,
+ isStaticChildren?: boolean,
+ source?: unknown,
+ self?: unknown,
+ ): React.ReactElement
| null;
+}
diff --git a/packages/react-components/react-utilities/etc/react-utilities.api.md b/packages/react-components/react-utilities/etc/react-utilities.api.md
index 81c8e94469a4ab..985a481231f311 100644
--- a/packages/react-components/react-utilities/etc/react-utilities.api.md
+++ b/packages/react-components/react-utilities/etc/react-utilities.api.md
@@ -70,6 +70,12 @@ export function getSlots(state: ComponentState): {
slotProps: ObjectSlotProps;
};
+// @public
+export function getSlotsNext(state: ComponentState): {
+ slots: Slots;
+ slotProps: ObjectSlotProps;
+};
+
// @internal
export function getTriggerChild(children: TriggerProps['children']): (React_2.ReactElement> & {
ref?: React_2.Ref;
@@ -143,6 +149,12 @@ export type Slot>;
}[AlternateAs] | null : 'Error: First parameter to Slot must not be not a union of types. See documentation of Slot type.';
+// @internal
+export const SLOT_EXTERNAL_CHILDREN_SYMBOL: unique symbol;
+
+// @internal
+export const SLOT_INTERNAL_CHILDREN_SYMBOL: unique symbol;
+
// @public
export type SlotClassNames = {
[SlotName in keyof Slots]-?: string;
@@ -175,6 +187,11 @@ export type TriggerProps = {
children?: React_2.ReactElement | ((props: TriggerChildProps) => React_2.ReactElement | null) | null;
};
+// @public
+export type UnknownSlotProps = Pick, 'children' | 'className' | 'style'> & {
+ as?: keyof JSX.IntrinsicElements;
+};
+
// @internal
export const useControllableState: (options: UseControllableStateOptions) => [State, React_2.Dispatch>];
diff --git a/packages/react-components/react-utilities/src/compose/constants.ts b/packages/react-components/react-utilities/src/compose/constants.ts
new file mode 100644
index 00000000000000..55e6c0a70eae3f
--- /dev/null
+++ b/packages/react-components/react-utilities/src/compose/constants.ts
@@ -0,0 +1,10 @@
+/**
+ * @internal
+ * internal symbol used to keep defaultProps.children available
+ */
+export const SLOT_INTERNAL_CHILDREN_SYMBOL = Symbol('fui.slotInternalChildren');
+/**
+ * @internal
+ * internal symbol used to keep slot.children available
+ */
+export const SLOT_EXTERNAL_CHILDREN_SYMBOL = Symbol('fui.slotExternalChildren');
diff --git a/packages/react-components/react-utilities/src/compose/getSlots.ts b/packages/react-components/react-utilities/src/compose/getSlots.ts
index 01f3dcef509058..561b7af0957466 100644
--- a/packages/react-components/react-utilities/src/compose/getSlots.ts
+++ b/packages/react-components/react-utilities/src/compose/getSlots.ts
@@ -8,7 +8,9 @@ import type {
SlotPropsRecord,
SlotRenderFunction,
UnionToIntersection,
+ UnknownSlotProps,
} from './types';
+import { SLOT_EXTERNAL_CHILDREN_SYMBOL, SLOT_INTERNAL_CHILDREN_SYMBOL } from './constants';
export type Slots = {
[K in keyof S]: ExtractSlotProps extends AsIntrinsicElement
@@ -19,7 +21,7 @@ export type Slots = {
: React.ElementType>;
};
-type ObjectSlotProps = {
+export type ObjectSlotProps = {
[K in keyof S]-?: ExtractSlotProps extends AsIntrinsicElement
? // For intrinsic element types, return the intersection of all possible
// element's props, to be compatible with the As type returned by Slots<>
@@ -68,10 +70,17 @@ function getSlot(
state: ComponentState,
slotName: K,
): readonly [React.ElementType | null, R[K]] {
- if (state[slotName] === undefined) {
+ const props = state[slotName];
+
+ if (props === undefined) {
return [null, undefined as R[K]];
}
- const { children, as: asProp, ...rest } = state[slotName]!;
+
+ // Symbols must be deleted to ensure new custom pragma won't recognize this as the new version element
+ delete (props as { [SLOT_EXTERNAL_CHILDREN_SYMBOL]: unknown })[SLOT_EXTERNAL_CHILDREN_SYMBOL];
+ delete (props as { [SLOT_INTERNAL_CHILDREN_SYMBOL]: unknown })[SLOT_INTERNAL_CHILDREN_SYMBOL];
+
+ const { children, as: asProp, ...rest } = props;
const slot = (
state.components?.[slotName] === undefined || typeof state.components[slotName] === 'string'
@@ -89,8 +98,7 @@ function getSlot(
];
}
- const shouldOmitAsProp = typeof slot === 'string' && state[slotName]?.as;
- const slotProps = (shouldOmitAsProp ? omit(state[slotName]!, ['as']) : state[slotName]) as R[K];
-
+ const shouldOmitAsProp = typeof slot === 'string' && asProp;
+ const slotProps = (shouldOmitAsProp ? omit(props, ['as']) : (props as UnknownSlotProps)) as R[K];
return [slot, slotProps];
}
diff --git a/packages/react-components/react-utilities/src/compose/getSlotsNext.test.tsx b/packages/react-components/react-utilities/src/compose/getSlotsNext.test.tsx
new file mode 100644
index 00000000000000..fc824df2685416
--- /dev/null
+++ b/packages/react-components/react-utilities/src/compose/getSlotsNext.test.tsx
@@ -0,0 +1,148 @@
+import * as React from 'react';
+import { getSlotsNext } from './getSlotsNext';
+import type { Slot } from './types';
+
+describe('getSlotsNext', () => {
+ type FooProps = { id?: string; children?: React.ReactNode };
+ const Foo = (props: FooProps) => ;
+
+ it('returns provided component type for root if the as prop is not provided', () => {
+ type Slots = { root: Slot<'div'> };
+ expect(getSlotsNext({ root: {}, components: { root: 'div' } })).toEqual({
+ slots: { root: 'div' },
+ slotProps: { root: {} },
+ });
+ });
+
+ it('returns root slot as a span with no props', () => {
+ type Slots = { root: Slot<'div', 'span'> };
+ expect(getSlotsNext({ root: { as: 'span' }, components: { root: 'div' } })).toEqual({
+ slots: { root: 'span' },
+ slotProps: { root: {} },
+ });
+ });
+
+ it('does not omit invalid props for the rendered element', () => {
+ type Slots = { root: Slot<'button'> };
+ const invalidProp = { href: 'href' } as React.ButtonHTMLAttributes;
+ expect(
+ getSlotsNext({ root: { as: 'button', id: 'id', ...invalidProp }, components: { root: 'button' } }),
+ ).toEqual({
+ slots: { root: 'button' },
+ slotProps: { root: { id: 'id', href: 'href' } },
+ });
+ });
+
+ it('returns root slot as an anchor, leaving the href intact', () => {
+ type Slots = { root: Slot<'a'> };
+ expect(getSlotsNext({ root: { as: 'a', id: 'id', href: 'href' }, components: { root: 'a' } })).toEqual({
+ slots: { root: 'a' },
+ slotProps: { root: { id: 'id', href: 'href' } },
+ });
+ });
+
+ it('returns a component slot with no children', () => {
+ type Slots = {
+ root: Slot<'div'>;
+ icon: Slot;
+ };
+ expect(
+ getSlotsNext({
+ icon: {},
+ components: { root: 'div', icon: Foo },
+ root: { as: 'div' },
+ }),
+ ).toEqual({
+ slots: { root: 'div', icon: Foo },
+ slotProps: { root: {}, icon: {} },
+ });
+ });
+
+ it('returns slot as button', () => {
+ type Slots = {
+ root: Slot<'div', 'span'>;
+ icon: Slot<'button'>;
+ };
+ expect(
+ getSlotsNext({
+ components: { icon: 'button', root: 'div' },
+ root: { as: 'span' },
+ icon: { id: 'id', children: 'children' },
+ }),
+ ).toEqual({
+ slots: { root: 'span', icon: 'button' },
+ slotProps: { root: {}, icon: { id: 'id', children: 'children' } },
+ });
+ });
+
+ it('returns slot as anchor and includes supported props (href)', () => {
+ type Slots = {
+ root: Slot<'div'>;
+ icon: Slot<'a'>;
+ };
+ expect(
+ getSlotsNext({
+ root: { as: 'div' },
+ components: { root: 'div', icon: 'a' },
+ icon: { id: 'id', href: 'href', children: 'children' },
+ }),
+ ).toEqual({
+ slots: { root: 'div', icon: 'a' },
+ slotProps: { root: {}, icon: { id: 'id', href: 'href', children: 'children' } },
+ });
+ });
+
+ it('returns a component and includes all props', () => {
+ type Slots = {
+ root: Slot<'div'>;
+ icon: Slot<'a'> | Slot;
+ };
+ expect(
+ getSlotsNext({
+ components: { root: 'div', icon: Foo },
+ root: { as: 'div' },
+ icon: { id: 'id', href: 'href', children: 'children' },
+ }),
+ ).toEqual({
+ slots: { root: 'div', icon: Foo },
+ slotProps: { root: {}, icon: { id: 'id', href: 'href', children: 'children' } },
+ });
+ });
+
+ it('slot children functions should just pass functions forward', () => {
+ type Slots = {
+ root: Slot<'div'>;
+ icon: Slot<'a'>;
+ };
+ const renderIcon = (C: React.ElementType, p: {}) => ;
+ expect(
+ getSlotsNext({
+ components: { root: 'div', icon: Foo },
+ root: { as: 'div' },
+ icon: { id: 'bar', children: renderIcon },
+ }),
+ ).toEqual({
+ slots: { root: 'div', icon: Foo },
+ slotProps: { root: {}, icon: { id: 'bar', children: renderIcon } },
+ });
+ });
+
+ it('can render a primitive input with no children', () => {
+ type Slots = {
+ root: Slot<'div'>;
+ input: Slot<'input'>;
+ icon?: Slot<'a'>;
+ };
+ expect(
+ getSlotsNext({
+ root: { as: 'div' },
+ components: { root: 'div', input: 'input', icon: 'a' },
+ input: {},
+ icon: undefined,
+ }),
+ ).toEqual({
+ slots: { root: 'div', input: 'input', icon: null },
+ slotProps: { root: {}, input: {}, icon: undefined },
+ });
+ });
+});
diff --git a/packages/react-components/react-utilities/src/compose/getSlotsNext.ts b/packages/react-components/react-utilities/src/compose/getSlotsNext.ts
new file mode 100644
index 00000000000000..4f5e11b0e91964
--- /dev/null
+++ b/packages/react-components/react-utilities/src/compose/getSlotsNext.ts
@@ -0,0 +1,47 @@
+import * as React from 'react';
+import type { ComponentState, SlotPropsRecord, UnknownSlotProps } from './types';
+import { ObjectSlotProps, Slots } from './getSlots';
+
+/**
+ * Similar to `getSlots`, main difference is that it's compatible with new custom jsx pragma
+ */
+export function getSlotsNext(
+ state: ComponentState,
+): {
+ slots: Slots;
+ slotProps: ObjectSlotProps;
+} {
+ const slots = {} as Slots;
+ const slotProps = {} as R;
+
+ const slotNames: (keyof R)[] = Object.keys(state.components);
+ for (const slotName of slotNames) {
+ const [slot, props] = getSlotNext(state, slotName);
+ slots[slotName] = slot as Slots[typeof slotName];
+ slotProps[slotName] = props;
+ }
+ return { slots, slotProps: slotProps as unknown as ObjectSlotProps };
+}
+
+function getSlotNext(
+ state: ComponentState,
+ slotName: K,
+): readonly [React.ElementType | null, R[K]] {
+ const props = state[slotName];
+
+ if (props === undefined) {
+ return [null, undefined as R[K]];
+ }
+ const { as: asProp, ...propsWithoutAs } = props;
+
+ const slot = (
+ state.components?.[slotName] === undefined || typeof state.components[slotName] === 'string'
+ ? asProp || state.components?.[slotName] || 'div'
+ : state.components[slotName]
+ ) as React.ElementType;
+
+ const shouldOmitAsProp = typeof slot === 'string' && asProp;
+ const slotProps: UnknownSlotProps = shouldOmitAsProp ? propsWithoutAs : props;
+
+ return [slot, slotProps as R[K]];
+}
diff --git a/packages/react-components/react-utilities/src/compose/index.ts b/packages/react-components/react-utilities/src/compose/index.ts
index 99571d50ab1ba9..fec1ddf85added 100644
--- a/packages/react-components/react-utilities/src/compose/index.ts
+++ b/packages/react-components/react-utilities/src/compose/index.ts
@@ -2,3 +2,5 @@ export * from './getSlots';
export * from './resolveShorthand';
export * from './types';
export * from './isResolvedShorthand';
+export * from './constants';
+export * from './getSlotsNext';
diff --git a/packages/react-components/react-utilities/src/compose/resolveShorthand.test.tsx b/packages/react-components/react-utilities/src/compose/resolveShorthand.test.tsx
index 1885cc5ad165d2..aa41fb44b50a53 100644
--- a/packages/react-components/react-utilities/src/compose/resolveShorthand.test.tsx
+++ b/packages/react-components/react-utilities/src/compose/resolveShorthand.test.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import { resolveShorthand } from './resolveShorthand';
import type { Slot } from './types';
+import { SLOT_EXTERNAL_CHILDREN_SYMBOL, SLOT_INTERNAL_CHILDREN_SYMBOL } from './constants';
type TestProps = {
slotA?: Slot<'div'>;
@@ -15,21 +16,33 @@ describe('resolveShorthand', () => {
const props: TestProps = { slotA: 'hello' };
const resolvedProps = resolveShorthand(props.slotA);
- expect(resolvedProps).toEqual({ children: 'hello' });
+ expect(resolvedProps).toEqual({
+ children: 'hello',
+ [SLOT_EXTERNAL_CHILDREN_SYMBOL]: 'hello',
+ [SLOT_INTERNAL_CHILDREN_SYMBOL]: undefined,
+ });
});
it('resolves a JSX element', () => {
const props: TestProps = { slotA: hello
};
const resolvedProps = resolveShorthand(props.slotA);
- expect(resolvedProps).toEqual({ children: hello
});
+ expect(resolvedProps).toEqual({
+ children: hello
,
+ [SLOT_EXTERNAL_CHILDREN_SYMBOL]: hello
,
+ [SLOT_INTERNAL_CHILDREN_SYMBOL]: undefined,
+ });
});
it('resolves a number', () => {
const props: TestProps = { slotA: 42 };
const resolvedProps = resolveShorthand(props.slotA);
- expect(resolvedProps).toEqual({ children: 42 });
+ expect(resolvedProps).toEqual({
+ children: 42,
+ [SLOT_EXTERNAL_CHILDREN_SYMBOL]: 42,
+ [SLOT_INTERNAL_CHILDREN_SYMBOL]: undefined,
+ });
});
it('resolves an object as its copy', () => {
diff --git a/packages/react-components/react-utilities/src/compose/resolveShorthand.ts b/packages/react-components/react-utilities/src/compose/resolveShorthand.ts
index 7cf5a91f21c4be..5f657879916636 100644
--- a/packages/react-components/react-utilities/src/compose/resolveShorthand.ts
+++ b/packages/react-components/react-utilities/src/compose/resolveShorthand.ts
@@ -1,5 +1,6 @@
import { isValidElement } from 'react';
import type { SlotShorthandValue, UnknownSlotProps } from './types';
+import { SLOT_EXTERNAL_CHILDREN_SYMBOL, SLOT_INTERNAL_CHILDREN_SYMBOL } from './constants';
export type ResolveShorthandOptions = Required extends true
? { required: true; defaultProps?: Props }
@@ -26,11 +27,17 @@ export const resolveShorthand: ResolveShorthandFunction = (value, options) => {
let resolvedShorthand = {} as UnknownSlotProps;
- if (typeof value === 'string' || typeof value === 'number' || Array.isArray(value) || isValidElement(value)) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if (typeof value === 'string' || typeof value === 'number' || Array.isArray(value) || isValidElement(value)) {
resolvedShorthand.children = value;
} else if (typeof value === 'object') {
resolvedShorthand = { ...value };
}
- return defaultProps ? { ...defaultProps, ...resolvedShorthand } : resolvedShorthand;
+ return {
+ ...defaultProps,
+ ...resolvedShorthand,
+ [SLOT_EXTERNAL_CHILDREN_SYMBOL]: resolvedShorthand.children,
+ [SLOT_INTERNAL_CHILDREN_SYMBOL]: defaultProps?.children,
+ };
};
diff --git a/packages/react-components/react-utilities/src/index.ts b/packages/react-components/react-utilities/src/index.ts
index 743ee85e64d6d1..a95032278b593c 100644
--- a/packages/react-components/react-utilities/src/index.ts
+++ b/packages/react-components/react-utilities/src/index.ts
@@ -1,4 +1,11 @@
-export { getSlots, resolveShorthand, isResolvedShorthand } from './compose/index';
+export {
+ getSlots,
+ getSlotsNext,
+ resolveShorthand,
+ isResolvedShorthand,
+ SLOT_EXTERNAL_CHILDREN_SYMBOL,
+ SLOT_INTERNAL_CHILDREN_SYMBOL,
+} from './compose/index';
export type {
ExtractSlotProps,
ComponentProps,
@@ -12,6 +19,7 @@ export type {
SlotPropsRecord,
SlotRenderFunction,
SlotShorthandValue,
+ UnknownSlotProps,
} from './compose/index';
export {
diff --git a/tsconfig.base.json b/tsconfig.base.json
index b994a894ae86fd..ba88b1270f9964 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -48,6 +48,7 @@
"@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-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"],