;
+};
+
+export function createElement(
+ type: React.ElementType
,
+ props?: P | null,
+ ...children: React.ReactNode[]
+): React.ReactElement
| null {
+ if (!isSlotComponent(props)) {
+ return React.createElement(type, props, ...children);
+ }
+
+ const result = normalizeRenderFunction(props, children);
+ return React.createElement(
+ React.Fragment,
+ {},
+ result.renderFunction(type, { ...result.props, children: result.children }),
+ ) as React.ReactElement
;
+}
+
+function normalizeRenderFunction(
+ propsWithMetadata: WithMetadata,
+ overrideChildren?: React.ReactNode[],
+): {
+ props: Props;
+ children: React.ReactNode;
+ renderFunction: SlotRenderFunction;
+} {
+ const { [SLOT_RENDER_FUNCTION_SYMBOL]: renderFunction, children: externalChildren, ...props } = propsWithMetadata;
+
+ const children: React.ReactNode =
+ Array.isArray(overrideChildren) && overrideChildren.length > 0
+ ? React.createElement(React.Fragment, {}, ...overrideChildren)
+ : externalChildren;
+
+ return {
+ children,
+ renderFunction,
+ props: props as UnknownSlotProps as Props,
+ };
+}
+
+export function isSlotComponent(props?: Props | null): props is WithMetadata {
+ return Boolean(props?.hasOwnProperty(SLOT_RENDER_FUNCTION_SYMBOL));
+}
diff --git a/packages/react-components/react-jsx-runtime/src/index.ts b/packages/react-components/react-jsx-runtime/src/index.ts
index aacbad0068e241..0276e539054a37 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 { createElement } from './createElement';
+export { Fragment } from 'react';
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..a03ffd720af387 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,9 @@ 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_RENDER_FUNCTION_SYMBOL: unique symbol;
+
// @public
export type SlotClassNames = {
[SlotName in keyof Slots]-?: string;
@@ -175,6 +184,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..f5c1593f9203ae
--- /dev/null
+++ b/packages/react-components/react-utilities/src/compose/constants.ts
@@ -0,0 +1,5 @@
+/**
+ * @internal
+ * Internal reference for the render function
+ */
+export const SLOT_RENDER_FUNCTION_SYMBOL = Symbol('fui.slotRenderFunction');
diff --git a/packages/react-components/react-utilities/src/compose/getSlots.ts b/packages/react-components/react-utilities/src/compose/getSlots.ts
index 01f3dcef509058..2799542d263088 100644
--- a/packages/react-components/react-utilities/src/compose/getSlots.ts
+++ b/packages/react-components/react-utilities/src/compose/getSlots.ts
@@ -8,6 +8,7 @@ import type {
SlotPropsRecord,
SlotRenderFunction,
UnionToIntersection,
+ UnknownSlotProps,
} from './types';
export type Slots = {
@@ -19,7 +20,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 +69,13 @@ 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]!;
+
+ const { children, as: asProp, ...rest } = props;
const slot = (
state.components?.[slotName] === undefined || typeof state.components[slotName] === 'string'
@@ -89,8 +93,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..4d564af2496234 100644
--- a/packages/react-components/react-utilities/src/compose/resolveShorthand.test.tsx
+++ b/packages/react-components/react-utilities/src/compose/resolveShorthand.test.tsx
@@ -15,21 +15,27 @@ describe('resolveShorthand', () => {
const props: TestProps = { slotA: 'hello' };
const resolvedProps = resolveShorthand(props.slotA);
- expect(resolvedProps).toEqual({ children: 'hello' });
+ expect(resolvedProps).toEqual({
+ children: 'hello',
+ });
});
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
,
+ });
});
it('resolves a number', () => {
const props: TestProps = { slotA: 42 };
const resolvedProps = resolveShorthand(props.slotA);
- expect(resolvedProps).toEqual({ children: 42 });
+ expect(resolvedProps).toEqual({
+ children: 42,
+ });
});
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..76b109240de9f9 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 type { SlotRenderFunction, SlotShorthandValue, UnknownSlotProps } from './types';
+import { SLOT_RENDER_FUNCTION_SYMBOL } from './constants';
export type ResolveShorthandOptions = Required extends true
? { required: true; defaultProps?: Props }
@@ -24,13 +25,26 @@ export const resolveShorthand: ResolveShorthandFunction = (value, options) => {
return undefined;
}
- let resolvedShorthand = {} as UnknownSlotProps;
+ let resolvedShorthand: UnknownSlotProps & {
+ [SLOT_RENDER_FUNCTION_SYMBOL]?: SlotRenderFunction;
+ } = {};
- 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 };
+ resolvedShorthand = value;
}
- return defaultProps ? { ...defaultProps, ...resolvedShorthand } : resolvedShorthand;
+ resolvedShorthand = {
+ ...defaultProps,
+ ...resolvedShorthand,
+ };
+
+ if (typeof resolvedShorthand.children === 'function') {
+ resolvedShorthand[SLOT_RENDER_FUNCTION_SYMBOL] = resolvedShorthand.children as SlotRenderFunction;
+ resolvedShorthand.children = defaultProps?.children;
+ }
+
+ return resolvedShorthand;
};
diff --git a/packages/react-components/react-utilities/src/index.ts b/packages/react-components/react-utilities/src/index.ts
index 743ee85e64d6d1..c2d0023253e0b3 100644
--- a/packages/react-components/react-utilities/src/index.ts
+++ b/packages/react-components/react-utilities/src/index.ts
@@ -1,4 +1,10 @@
-export { getSlots, resolveShorthand, isResolvedShorthand } from './compose/index';
+export {
+ getSlots,
+ getSlotsNext,
+ resolveShorthand,
+ isResolvedShorthand,
+ SLOT_RENDER_FUNCTION_SYMBOL,
+} from './compose/index';
export type {
ExtractSlotProps,
ComponentProps,
@@ -12,6 +18,7 @@ export type {
SlotPropsRecord,
SlotRenderFunction,
SlotShorthandValue,
+ UnknownSlotProps,
} from './compose/index';
export {