diff --git a/apps/react-17-tests-v9/src/issues.tsx b/apps/react-17-tests-v9/src/issues.tsx index 38baf2a9f7e0c4..80e95162dab166 100644 --- a/apps/react-17-tests-v9/src/issues.tsx +++ b/apps/react-17-tests-v9/src/issues.tsx @@ -48,7 +48,6 @@ import type { type AppSplitButtonSlots = { root: NonNullable>; menuButton: NonNullable>; - // @ts-expect-error - Slot type mismatch menu: NonNullable>; }; type ValidAppSplitButtonSlots = { diff --git a/change/@fluentui-react-accordion-2de760f2-7636-41b8-8ab0-e6a55b5ff229.json b/change/@fluentui-react-accordion-2de760f2-7636-41b8-8ab0-e6a55b5ff229.json new file mode 100644 index 00000000000000..88a6922ee3710b --- /dev/null +++ b/change/@fluentui-react-accordion-2de760f2-7636-41b8-8ab0-e6a55b5ff229.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: adjust types to support react 18", + "packageName": "@fluentui/react-accordion", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-aria-d7e86c4f-8c88-472c-857b-eb965ca8c6f5.json b/change/@fluentui-react-aria-d7e86c4f-8c88-472c-857b-eb965ca8c6f5.json new file mode 100644 index 00000000000000..cd69ab489e1c15 --- /dev/null +++ b/change/@fluentui-react-aria-d7e86c4f-8c88-472c-857b-eb965ca8c6f5.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: adjust types to support react 18", + "packageName": "@fluentui/react-aria", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-dialog-497dfa26-d4be-4d88-929e-31341160b876.json b/change/@fluentui-react-dialog-497dfa26-d4be-4d88-929e-31341160b876.json new file mode 100644 index 00000000000000..433dc879a67dce --- /dev/null +++ b/change/@fluentui-react-dialog-497dfa26-d4be-4d88-929e-31341160b876.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: adjust types to support react 18", + "packageName": "@fluentui/react-dialog", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-drawer-41054add-b39c-4c7a-87cb-ae3ea4dfd636.json b/change/@fluentui-react-drawer-41054add-b39c-4c7a-87cb-ae3ea4dfd636.json new file mode 100644 index 00000000000000..b7011c439509a2 --- /dev/null +++ b/change/@fluentui-react-drawer-41054add-b39c-4c7a-87cb-ae3ea4dfd636.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: adjust types to support react 18", + "packageName": "@fluentui/react-drawer", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-menu-e78245da-217b-4e5a-8c8e-d31a93877289.json b/change/@fluentui-react-menu-e78245da-217b-4e5a-8c8e-d31a93877289.json new file mode 100644 index 00000000000000..f1705f1c68b77a --- /dev/null +++ b/change/@fluentui-react-menu-e78245da-217b-4e5a-8c8e-d31a93877289.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: adjust types to support react 18", + "packageName": "@fluentui/react-menu", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-migration-v0-v9-221b1a2f-4a9f-42e5-bec7-292bb66d0f97.json b/change/@fluentui-react-migration-v0-v9-221b1a2f-4a9f-42e5-bec7-292bb66d0f97.json new file mode 100644 index 00000000000000..c8667db62320c3 --- /dev/null +++ b/change/@fluentui-react-migration-v0-v9-221b1a2f-4a9f-42e5-bec7-292bb66d0f97.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: adjust types to support react 18", + "packageName": "@fluentui/react-migration-v0-v9", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-migration-v8-v9-f26c0071-2672-4ed3-aa8b-f08d261b31e3.json b/change/@fluentui-react-migration-v8-v9-f26c0071-2672-4ed3-aa8b-f08d261b31e3.json new file mode 100644 index 00000000000000..1ee070d1caff98 --- /dev/null +++ b/change/@fluentui-react-migration-v8-v9-f26c0071-2672-4ed3-aa8b-f08d261b31e3.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: adjust types to support react 18", + "packageName": "@fluentui/react-migration-v8-v9", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-search-b4c782b6-95bb-4f7a-9760-392ba37dd275.json b/change/@fluentui-react-search-b4c782b6-95bb-4f7a-9760-392ba37dd275.json new file mode 100644 index 00000000000000..1af0ca9a72cfc9 --- /dev/null +++ b/change/@fluentui-react-search-b4c782b6-95bb-4f7a-9760-392ba37dd275.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: adjust types to support react 18", + "packageName": "@fluentui/react-search", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-table-eb6a8a2f-feda-4c22-b9ae-3600c83d6c7a.json b/change/@fluentui-react-table-eb6a8a2f-feda-4c22-b9ae-3600c83d6c7a.json new file mode 100644 index 00000000000000..6eeea8fbc10e4b --- /dev/null +++ b/change/@fluentui-react-table-eb6a8a2f-feda-4c22-b9ae-3600c83d6c7a.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: adjust types to support react 18", + "packageName": "@fluentui/react-table", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-tag-picker-d8d7ef7d-3238-445f-bea9-28ff422edace.json b/change/@fluentui-react-tag-picker-d8d7ef7d-3238-445f-bea9-28ff422edace.json new file mode 100644 index 00000000000000..093ce2e5827d91 --- /dev/null +++ b/change/@fluentui-react-tag-picker-d8d7ef7d-3238-445f-bea9-28ff422edace.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: adjust types to support react 18", + "packageName": "@fluentui/react-tag-picker", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-toast-f7778328-a1b7-4a92-abdd-d734a8f34c0b.json b/change/@fluentui-react-toast-f7778328-a1b7-4a92-abdd-d734a8f34c0b.json new file mode 100644 index 00000000000000..f78d6162336a96 --- /dev/null +++ b/change/@fluentui-react-toast-f7778328-a1b7-4a92-abdd-d734a8f34c0b.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: adjust types to support react 18", + "packageName": "@fluentui/react-toast", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-tree-cbf08b3f-9f37-4cd7-8705-89d94929be2c.json b/change/@fluentui-react-tree-cbf08b3f-9f37-4cd7-8705-89d94929be2c.json new file mode 100644 index 00000000000000..05e5543ebbc7fa --- /dev/null +++ b/change/@fluentui-react-tree-cbf08b3f-9f37-4cd7-8705-89d94929be2c.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: adjust types to support react 18", + "packageName": "@fluentui/react-tree", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-utilities-5ab41c91-a073-432c-8f39-66f353eb3d81.json b/change/@fluentui-react-utilities-5ab41c91-a073-432c-8f39-66f353eb3d81.json new file mode 100644 index 00000000000000..ff4b18c381583e --- /dev/null +++ b/change/@fluentui-react-utilities-5ab41c91-a073-432c-8f39-66f353eb3d81.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: react 18 support for slots api", + "packageName": "@fluentui/react-utilities", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "minor" +} diff --git a/change/@fluentui-react-virtualizer-7ea733da-c8b9-4629-abd7-3b8a128a5063.json b/change/@fluentui-react-virtualizer-7ea733da-c8b9-4629-abd7-3b8a128a5063.json new file mode 100644 index 00000000000000..c40ae33bf3b73a --- /dev/null +++ b/change/@fluentui-react-virtualizer-7ea733da-c8b9-4629-abd7-3b8a128a5063.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: adjust types to support react 18", + "packageName": "@fluentui/react-virtualizer", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "none" +} diff --git a/packages/react-components/react-accordion/library/src/components/AccordionPanel/useAccordionPanel.ts b/packages/react-components/react-accordion/library/src/components/AccordionPanel/useAccordionPanel.ts index 808a7f62ec14f1..2e00c494ac3662 100644 --- a/packages/react-components/react-accordion/library/src/components/AccordionPanel/useAccordionPanel.ts +++ b/packages/react-components/react-accordion/library/src/components/AccordionPanel/useAccordionPanel.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; import { useTabsterAttributes } from '@fluentui/react-tabster'; -import { presenceMotionSlot, type PresenceMotionSlotProps } from '@fluentui/react-motion'; +import { presenceMotionSlot } from '@fluentui/react-motion'; import { Collapse } from '@fluentui/react-motion-components-preview'; import { useAccordionContext_unstable } from '../../contexts/accordion'; import type { AccordionPanelProps, AccordionPanelState } from './AccordionPanel.types'; @@ -24,11 +24,7 @@ export const useAccordionPanel_unstable = ( open, components: { root: 'div', - // TODO: remove once React v18 slot API is modified - // This is a problem at the moment due to UnknownSlotProps assumption - // that `children` property is `ReactNode`, which in this case is not valid - // as PresenceComponentProps['children'] is `ReactElement` - collapseMotion: Collapse as React.FC, + collapseMotion: Collapse, }, root: slot.always( getIntrinsicElementProps('div', { diff --git a/packages/react-components/react-aria/library/etc/react-aria.api.md b/packages/react-components/react-aria/library/etc/react-aria.api.md index 46aadb79afe483..9922330545c9c3 100644 --- a/packages/react-components/react-aria/library/etc/react-aria.api.md +++ b/packages/react-components/react-aria/library/etc/react-aria.api.md @@ -5,6 +5,7 @@ ```ts import type { AnnounceContextValue } from '@fluentui/react-shared-contexts'; +import type { DistributiveOmit } from '@fluentui/react-utilities'; import type { ExtractSlotProps } from '@fluentui/react-utilities'; import * as React_2 from 'react'; import type { ResolveShorthandFunction } from '@fluentui/react-utilities'; @@ -73,7 +74,7 @@ export type ARIAButtonElement = H export type ARIAButtonElementIntersection = UnionToIntersection>; // @public -export type ARIAButtonProps = React_2.PropsWithRef & { +export type ARIAButtonProps = DistributiveOmit, 'children'> & { disabled?: boolean; disabledFocusable?: boolean; }; diff --git a/packages/react-components/react-aria/library/src/button/types.ts b/packages/react-components/react-aria/library/src/button/types.ts index 478b894dadb0c1..8e0ae86460c03b 100644 --- a/packages/react-components/react-aria/library/src/button/types.ts +++ b/packages/react-components/react-aria/library/src/button/types.ts @@ -1,4 +1,4 @@ -import type { ExtractSlotProps, Slot, UnionToIntersection } from '@fluentui/react-utilities'; +import type { DistributiveOmit, ExtractSlotProps, Slot, UnionToIntersection } from '@fluentui/react-utilities'; import * as React from 'react'; export type ARIAButtonType = 'button' | 'a' | 'div'; @@ -18,8 +18,9 @@ export type ARIAButtonElementIntersection = React.PropsWithRef< - JSX.IntrinsicElements[Type] +export type ARIAButtonProps = DistributiveOmit< + React.PropsWithRef, + 'children' > & { disabled?: boolean; /** diff --git a/packages/react-components/react-aria/library/src/button/useARIAButtonShorthand.ts b/packages/react-components/react-aria/library/src/button/useARIAButtonShorthand.ts index 9d8b64067a863e..cf903b8093961b 100644 --- a/packages/react-components/react-aria/library/src/button/useARIAButtonShorthand.ts +++ b/packages/react-components/react-aria/library/src/button/useARIAButtonShorthand.ts @@ -14,10 +14,10 @@ import type { ARIAButtonProps, ARIAButtonSlotProps, ARIAButtonType } from './typ * for multiple scenarios of shorthand properties. Ensuring 1st rule of ARIA for cases * where no attribute addition is required. */ -// eslint-disable-next-line @typescript-eslint/no-deprecated -export const useARIAButtonShorthand: ResolveShorthandFunction = (value, options) => { +export const useARIAButtonShorthand = ((value, options) => { // eslint-disable-next-line @typescript-eslint/no-deprecated const shorthand = resolveShorthand(value, options); const shorthandARIAButton = useARIAButtonProps(shorthand?.as ?? 'button', shorthand); return shorthand && shorthandARIAButton; -}; + // eslint-disable-next-line @typescript-eslint/no-deprecated +}) as ResolveShorthandFunction; diff --git a/packages/react-components/react-dialog/library/src/components/Dialog/useDialog.ts b/packages/react-components/react-dialog/library/src/components/Dialog/useDialog.ts index 84d3930686a8a3..84d4d0e2622d7a 100644 --- a/packages/react-components/react-dialog/library/src/components/Dialog/useDialog.ts +++ b/packages/react-components/react-dialog/library/src/components/Dialog/useDialog.ts @@ -1,6 +1,6 @@ import { useHasParentContext } from '@fluentui/react-context-selector'; import { useModalAttributes } from '@fluentui/react-tabster'; -import { presenceMotionSlot, type PresenceMotionSlotProps } from '@fluentui/react-motion'; +import { presenceMotionSlot } from '@fluentui/react-motion'; import { useControllableState, useEventCallback, useId } from '@fluentui/react-utilities'; import * as React from 'react'; @@ -48,11 +48,7 @@ export const useDialog_unstable = (props: DialogProps): DialogState => { return { components: { - // TODO: remove once React v18 slot API is modified - // This is a problem at the moment due to UnknownSlotProps assumption - // that `children` property is `ReactNode`, which in this case is not valid - // as PresenceComponentProps['children'] is `ReactElement` - surfaceMotion: DialogSurfaceMotion as React.FC, + surfaceMotion: DialogSurfaceMotion, }, inertTrapFocus, open, diff --git a/packages/react-components/react-dialog/library/src/components/DialogSurface/useDialogSurface.ts b/packages/react-components/react-dialog/library/src/components/DialogSurface/useDialogSurface.ts index 51a6f5c59c7bc7..71fb7defa6347e 100644 --- a/packages/react-components/react-dialog/library/src/components/DialogSurface/useDialogSurface.ts +++ b/packages/react-components/react-dialog/library/src/components/DialogSurface/useDialogSurface.ts @@ -1,5 +1,5 @@ import { Escape } from '@fluentui/keyboard-keys'; -import { presenceMotionSlot, type PresenceMotionSlotProps } from '@fluentui/react-motion'; +import { presenceMotionSlot } from '@fluentui/react-motion'; import { useEventCallback, useMergedRefs, @@ -97,11 +97,7 @@ export const useDialogSurface_unstable = ( components: { backdrop: 'div', root: 'div', - // TODO: remove once React v18 slot API is modified - // This is a problem at the moment due to UnknownSlotProps assumption - // that `children` property is `ReactNode`, which in this case is not valid - // as PresenceComponentProps['children'] is `ReactElement` - backdropMotion: DialogBackdropMotion as React.FC, + backdropMotion: DialogBackdropMotion, }, open, backdrop, diff --git a/packages/react-components/react-drawer/library/src/components/DrawerHeaderTitle/useDrawerHeaderTitleStyles.styles.ts b/packages/react-components/react-drawer/library/src/components/DrawerHeaderTitle/useDrawerHeaderTitleStyles.styles.ts index 139d604726c523..8d937860c00d41 100644 --- a/packages/react-components/react-drawer/library/src/components/DrawerHeaderTitle/useDrawerHeaderTitleStyles.styles.ts +++ b/packages/react-components/react-drawer/library/src/components/DrawerHeaderTitle/useDrawerHeaderTitleStyles.styles.ts @@ -35,7 +35,16 @@ export const useDrawerHeaderTitleStyles_unstable = (state: DrawerHeaderTitleStat const styles = useStyles(); - const { heading: root = {}, action, components } = state; + const { + heading: root = {}, + action, + // We should not use components to pass along the base element type of a slot + // but there's no way to retrieve the element type of a slot from the slot definition + // right now without using SLOT_ELEMENT_TYPE_SYMBOL + // TODO: create a method to retrieve the element type of a slot + // eslint-disable-next-line @typescript-eslint/no-deprecated + components, + } = state; useDialogTitleStyles_unstable({ components: { diff --git a/packages/react-components/react-jsx-runtime/src/interop.test.tsx b/packages/react-components/react-jsx-runtime/src/interop.test.tsx index 24d2f754c91f99..76614a14bb15cb 100644 --- a/packages/react-components/react-jsx-runtime/src/interop.test.tsx +++ b/packages/react-components/react-jsx-runtime/src/interop.test.tsx @@ -171,6 +171,7 @@ describe('resolveShorthand with assertSlots', () => { const TestComponent = (props: TestComponentProps) => { const higherOrderState = useHigherOrderStateHook(props); const state: TestComponentState = { + // eslint-disable-next-line @typescript-eslint/no-deprecated components: { ...higherOrderState.components, slot: 'span' }, slot: { ...higherOrderState.slot, diff --git a/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItem.tsx b/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItem.tsx index 750a3deeda900c..134ff33a85c7a7 100644 --- a/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItem.tsx +++ b/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItem.tsx @@ -37,7 +37,14 @@ const ChevronLeftIcon = bundleIcon(ChevronLeftFilled, ChevronLeftRegular); export const useMenuItem_unstable = (props: MenuItemProps, ref: React.Ref>): MenuItemState => { const isSubmenuTrigger = useMenuTriggerContext_unstable(); const persistOnClickContext = useMenuContext_unstable(context => context.persistOnItemClick); - const { as = 'div', disabled = false, hasSubmenu = isSubmenuTrigger, persistOnClick = persistOnClickContext } = props; + const { + as = 'div', + disabled = false, + hasSubmenu = isSubmenuTrigger, + persistOnClick = persistOnClickContext, + content: _content, // `content` is a slot and it's type clashes with the HTMLElement `content` attribute + ...rest + } = props; const { hasIcons, hasCheckmarks } = useIconAndCheckmarkAlignment({ hasSubmenu }); const setOpen = useMenuContext_unstable(context => context.setOpen); useNotifySplitItemMultiline({ multiline: !!props.subText, hasSubmenu }); @@ -64,7 +71,7 @@ export const useMenuItem_unstable = (props: MenuItemProps, ref: React.Ref>(as, { role: 'menuitem', - ...props, + ...rest, disabled: false, disabledFocusable: disabled, ref: useMergedRefs(ref, innerRef) as React.Ref>, diff --git a/packages/react-components/react-menu/library/src/components/MenuItemLink/useMenuItemLink.ts b/packages/react-components/react-menu/library/src/components/MenuItemLink/useMenuItemLink.ts index d9aaa4f21d54ed..480635aea73987 100644 --- a/packages/react-components/react-menu/library/src/components/MenuItemLink/useMenuItemLink.ts +++ b/packages/react-components/react-menu/library/src/components/MenuItemLink/useMenuItemLink.ts @@ -27,6 +27,7 @@ export const useMenuItemLink_unstable = ( return { ...baseState, components: { + // eslint-disable-next-line @typescript-eslint/no-deprecated ...baseState.components, root: 'a', }, diff --git a/packages/react-components/react-menu/library/src/components/MenuItemSwitch/useMenuItemSwitch.tsx b/packages/react-components/react-menu/library/src/components/MenuItemSwitch/useMenuItemSwitch.tsx index 0099c7f05ebc9e..6ef0a923a359ee 100644 --- a/packages/react-components/react-menu/library/src/components/MenuItemSwitch/useMenuItemSwitch.tsx +++ b/packages/react-components/react-menu/library/src/components/MenuItemSwitch/useMenuItemSwitch.tsx @@ -29,6 +29,7 @@ export const useMenuItemSwitch_unstable = ( }, }), components: { + // eslint-disable-next-line @typescript-eslint/no-deprecated ...baseState.components, switchIndicator: 'span', }, diff --git a/packages/react-components/react-menu/library/src/components/MenuItemSwitch/useMenuItemSwitchStyles.styles.ts b/packages/react-components/react-menu/library/src/components/MenuItemSwitch/useMenuItemSwitchStyles.styles.ts index eca6a62d4704d6..76ab563a43efd7 100644 --- a/packages/react-components/react-menu/library/src/components/MenuItemSwitch/useMenuItemSwitchStyles.styles.ts +++ b/packages/react-components/react-menu/library/src/components/MenuItemSwitch/useMenuItemSwitchStyles.styles.ts @@ -135,6 +135,7 @@ export const useMenuItemSwitchStyles_unstable = (state: MenuItemSwitchState): Me useMenuItemStyles_unstable({ ...state, components: { + // eslint-disable-next-line @typescript-eslint/no-deprecated ...state.components, checkmark: 'span', submenuIndicator: 'span', diff --git a/packages/react-components/react-migration-v0-v9/library/etc/react-migration-v0-v9.api.md b/packages/react-components/react-migration-v0-v9/library/etc/react-migration-v0-v9.api.md index 5414b326c1d7a0..9c897364c7d313 100644 --- a/packages/react-components/react-migration-v0-v9/library/etc/react-migration-v0-v9.api.md +++ b/packages/react-components/react-migration-v0-v9/library/etc/react-migration-v0-v9.api.md @@ -157,9 +157,9 @@ export const input: { // @public (undocumented) export const ItemLayout: React_2.ForwardRefExoticComponent & Omit<{ as?: "div" | undefined; -} & Pick, HTMLDivElement>, "key" | keyof React_2.HTMLAttributes> & { +} & Omit, HTMLDivElement>, "key" | keyof React_2.HTMLAttributes> & { ref?: ((instance: HTMLDivElement | null) => void) | React_2.RefObject | null | undefined; -} & { +}, "children"> & { children?: React_2.ReactNode | SlotRenderFunction, HTMLDivElement>, "key" | keyof React_2.HTMLAttributes> & { ref?: ((instance: HTMLDivElement | null) => void) | React_2.RefObject | null | undefined; }>; diff --git a/packages/react-components/react-motion/library/src/slots/presenceMotionSlot.test.tsx b/packages/react-components/react-motion/library/src/slots/presenceMotionSlot.test.tsx index 56451080d3ff35..faa299c14467b8 100644 --- a/packages/react-components/react-motion/library/src/slots/presenceMotionSlot.test.tsx +++ b/packages/react-components/react-motion/library/src/slots/presenceMotionSlot.test.tsx @@ -21,11 +21,7 @@ const TestMotion = jest.fn( const TestComponent: React.FC = props => { const state: TestComponentState = { components: { - // TODO: remove once React v18 slot API is modified - // This is a problem at the moment due to UnknownSlotProps assumption - // that `children` property is `ReactNode`, which in this case is not valid - // as PresenceComponentProps['children'] is `ReactElement` - presenceMotion: TestMotion as React.FC, + presenceMotion: TestMotion, }, presenceMotion: presenceMotionSlot(props.presenceMotion, { elementType: TestMotion, diff --git a/packages/react-components/react-search/library/src/components/SearchBox/useSearchBox.tsx b/packages/react-components/react-search/library/src/components/SearchBox/useSearchBox.tsx index 266a2c08a10d2f..6418b53a982851 100644 --- a/packages/react-components/react-search/library/src/components/SearchBox/useSearchBox.tsx +++ b/packages/react-components/react-search/library/src/components/SearchBox/useSearchBox.tsx @@ -111,6 +111,7 @@ export const useSearchBox_unstable = (props: SearchBoxProps, ref: React.Ref & - ((props: DataGridBodyProps) => JSX.Element) = React.forwardRef((props, ref) => { - const state = useDataGridBody_unstable(props, ref); + ((props: DataGridBodyProps) => JSX.Element) = React.forwardRef( + (props, ref) => { + const state = useDataGridBody_unstable(props, ref); - useDataGridBodyStyles_unstable(state); + useDataGridBodyStyles_unstable(state); - useCustomStyleHook_unstable('useDataGridBodyStyles_unstable')(state); + useCustomStyleHook_unstable('useDataGridBodyStyles_unstable')(state); - return renderDataGridBody_unstable(state); -}) as ForwardRefComponent & ((props: DataGridBodyProps) => JSX.Element); + return renderDataGridBody_unstable(state); + }, +) as ForwardRefComponent & ((props: DataGridBodyProps) => JSX.Element); DataGridBody.displayName = 'DataGridBody'; diff --git a/packages/react-components/react-table/library/src/components/DataGridRow/DataGridRow.tsx b/packages/react-components/react-table/library/src/components/DataGridRow/DataGridRow.tsx index 36bfd9647550b5..a5e2d692fd742f 100644 --- a/packages/react-components/react-table/library/src/components/DataGridRow/DataGridRow.tsx +++ b/packages/react-components/react-table/library/src/components/DataGridRow/DataGridRow.tsx @@ -10,14 +10,16 @@ import { useCustomStyleHook_unstable } from '@fluentui/react-shared-contexts'; * DataGridRow component */ export const DataGridRow: ForwardRefComponent & - ((props: DataGridRowProps) => JSX.Element) = React.forwardRef((props, ref) => { - const state = useDataGridRow_unstable(props, ref); + ((props: DataGridRowProps) => JSX.Element) = React.forwardRef( + (props, ref) => { + const state = useDataGridRow_unstable(props, ref); - useDataGridRowStyles_unstable(state); + useDataGridRowStyles_unstable(state); - useCustomStyleHook_unstable('useDataGridRowStyles_unstable')(state); + useCustomStyleHook_unstable('useDataGridRowStyles_unstable')(state); - return renderDataGridRow_unstable(state); -}) as ForwardRefComponent & ((props: DataGridRowProps) => JSX.Element); + return renderDataGridRow_unstable(state); + }, +) as ForwardRefComponent & ((props: DataGridRowProps) => JSX.Element); DataGridRow.displayName = 'DataGridRow'; diff --git a/packages/react-components/react-table/library/src/components/DataGridRow/useDataGridRow.tsx b/packages/react-components/react-table/library/src/components/DataGridRow/useDataGridRow.tsx index 819e6a3652b36f..c02b1a04508d82 100644 --- a/packages/react-components/react-table/library/src/components/DataGridRow/useDataGridRow.tsx +++ b/packages/react-components/react-table/library/src/components/DataGridRow/useDataGridRow.tsx @@ -72,6 +72,7 @@ export const useDataGridRow_unstable = (props: DataGridRowProps, ref: React.Ref< return { ...baseState, components: { + // eslint-disable-next-line @typescript-eslint/no-deprecated ...baseState.components, selectionCell: DataGridSelectionCell, }, diff --git a/packages/react-components/react-table/library/src/components/TableCellLayout/useTableCellLayout.ts b/packages/react-components/react-table/library/src/components/TableCellLayout/useTableCellLayout.ts index bbf2e096734a5a..324533861161a9 100644 --- a/packages/react-components/react-table/library/src/components/TableCellLayout/useTableCellLayout.ts +++ b/packages/react-components/react-table/library/src/components/TableCellLayout/useTableCellLayout.ts @@ -33,13 +33,18 @@ export const useTableCellLayout_unstable = ( media: 'span', }, root: slot.always( - getIntrinsicElementProps('div', { - // FIXME: - // `ref` is wrongly assigned to be `HTMLElement` instead of `HTMLDivElement` - // but since it would be a breaking change to fix it, we are casting ref to it's proper type - ref: ref as React.Ref, - ...props, - }), + getIntrinsicElementProps( + 'div', + { + // FIXME: + // `ref` is wrongly assigned to be `HTMLElement` instead of `HTMLDivElement` + // but since it would be a breaking change to fix it, we are casting ref to it's proper type + ref: ref as React.Ref, + ...props, + }, + // `content` is a slot and it's type clashes with the HTMLElement `content` attribute + ['content'], + ), { elementType: 'div' }, ), appearance: props.appearance, diff --git a/packages/react-components/react-table/library/src/components/TableSelectionCell/useTableSelectionCell.tsx b/packages/react-components/react-table/library/src/components/TableSelectionCell/useTableSelectionCell.tsx index 3770b04f5b33f3..068e5a93359bad 100644 --- a/packages/react-components/react-table/library/src/components/TableSelectionCell/useTableSelectionCell.tsx +++ b/packages/react-components/react-table/library/src/components/TableSelectionCell/useTableSelectionCell.tsx @@ -34,6 +34,7 @@ export const useTableSelectionCell_unstable = ( return { ...tableCellState, components: { + // eslint-disable-next-line @typescript-eslint/no-deprecated ...tableCellState.components, checkboxIndicator: Checkbox, radioIndicator: Radio, diff --git a/packages/react-components/react-tag-picker/library/src/components/TagPickerOption/useTagPickerOption.ts b/packages/react-components/react-tag-picker/library/src/components/TagPickerOption/useTagPickerOption.ts index 775bbe458bea9f..0a7478beb45c9d 100644 --- a/packages/react-components/react-tag-picker/library/src/components/TagPickerOption/useTagPickerOption.ts +++ b/packages/react-components/react-tag-picker/library/src/components/TagPickerOption/useTagPickerOption.ts @@ -19,6 +19,7 @@ export const useTagPickerOption_unstable = ( const optionState = useOption_unstable(props, ref); const state: TagPickerOptionState = { components: { + // eslint-disable-next-line @typescript-eslint/no-deprecated ...optionState.components, media: 'div', secondaryContent: 'span', diff --git a/packages/react-components/react-toast/library/src/components/ToastContainer/useToastContainer.ts b/packages/react-components/react-toast/library/src/components/ToastContainer/useToastContainer.ts index 8923147185e43b..ab97acde4dee33 100644 --- a/packages/react-components/react-toast/library/src/components/ToastContainer/useToastContainer.ts +++ b/packages/react-components/react-toast/library/src/components/ToastContainer/useToastContainer.ts @@ -50,6 +50,7 @@ export const useToastContainer_unstable = ( pauseOnWindowBlur, imperativeRef, tryRestoreFocus, + content: _content, // `content` is a slot and it's type clashes with the HTMLElement `content` attribute ...rest } = props; const titleId = useId('toast-title'); diff --git a/packages/react-components/react-tree/library/src/hooks/useRootTree.ts b/packages/react-components/react-tree/library/src/hooks/useRootTree.ts index f7714274176ce1..0becfe3a82e1bb 100644 --- a/packages/react-components/react-tree/library/src/hooks/useRootTree.ts +++ b/packages/react-components/react-tree/library/src/hooks/useRootTree.ts @@ -2,7 +2,6 @@ import { getIntrinsicElementProps, useEventCallback, slot } from '@fluentui/reac import type { TreeCheckedChangeData, TreeProps, TreeState } from '../Tree'; import * as React from 'react'; import { Collapse } from '@fluentui/react-motion-components-preview'; -import { PresenceMotionSlotProps } from '@fluentui/react-motion'; import { TreeContextValue, TreeItemRequest } from '../contexts/treeContext'; import { createCheckedItems } from '../utils/createCheckedItems'; import { treeDataTypes } from '../utils/tokens'; @@ -80,11 +79,7 @@ export function useRootTree( return { components: { root: 'div', - // TODO: remove once React v18 slot API is modified - // This is a problem at the moment due to UnknownSlotProps assumption - // that `children` property is `ReactNode`, which in this case is not valid - // as PresenceComponentProps['children'] is `ReactElement` - collapseMotion: Collapse as React.FC, + collapseMotion: Collapse, }, contextType: 'root', selectionMode, diff --git a/packages/react-components/react-tree/library/src/hooks/useSubtree.ts b/packages/react-components/react-tree/library/src/hooks/useSubtree.ts index bb79652f6d094e..93d553600ea66d 100644 --- a/packages/react-components/react-tree/library/src/hooks/useSubtree.ts +++ b/packages/react-components/react-tree/library/src/hooks/useSubtree.ts @@ -3,7 +3,7 @@ import { TreeProps, TreeState } from '../Tree'; import { SubtreeContextValue, useSubtreeContext_unstable, useTreeItemContext_unstable } from '../contexts/index'; import { getIntrinsicElementProps, useMergedRefs, slot } from '@fluentui/react-utilities'; import { Collapse } from '@fluentui/react-motion-components-preview'; -import { presenceMotionSlot, PresenceMotionSlotProps } from '@fluentui/react-motion'; +import { presenceMotionSlot } from '@fluentui/react-motion'; /** * Create the state required to render a sub-level tree. @@ -26,11 +26,7 @@ export function useSubtree( open, components: { root: 'div', - // TODO: remove once React v18 slot API is modified - // This is a problem at the moment due to UnknownSlotProps assumption - // that `children` property is `ReactNode`, which in this case is not valid - // as PresenceComponentProps['children'] is `ReactElement` - collapseMotion: Collapse as React.FC, + collapseMotion: Collapse, }, level: parentLevel + 1, root: slot.always( 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 70291d8f451185..3b84c2bcd6e4d7 100644 --- a/packages/react-components/react-utilities/etc/react-utilities.api.md +++ b/packages/react-components/react-utilities/etc/react-utilities.api.md @@ -27,10 +27,10 @@ export type ComponentProps = { components: { - [Key in keyof Slots]-?: React_2.ComponentType> | (ExtractSlotProps extends AsIntrinsicElement ? As : keyof JSX.IntrinsicElements); + [Key in keyof Slots]-?: React_2.ElementType; }; } & { - [Key in keyof Slots]: ReplaceNullWithUndefined>; + [Key in keyof Slots]: ReplaceNullWithUndefined>>; }; // @internal (undocumented) @@ -63,7 +63,7 @@ export type FluentTriggerComponent = { }; // @public -export type ForwardRefComponent = React_2.ForwardRefExoticComponent>>; +export type ForwardRefComponent = NamedExoticComponent>>; // @public export function getEventClientCoords(event: TouchOrMouseEvent): { @@ -100,13 +100,13 @@ export const getRTLSafeKey: (key: string, dir: 'ltr' | 'rtl') => string; export const getSlotClassNameProp_unstable: (slot: UnknownSlotProps) => string | undefined; // @public @deprecated -export function getSlots(state: ComponentState): { +export function getSlots(state: unknown): { slots: Slots; slotProps: ObjectSlotProps; }; // @internal @deprecated -export function getSlotsNext(state: ComponentState): { +export function getSlotsNext(state: unknown): { slots: Slots; slotProps: ObjectSlotProps; }; @@ -210,8 +210,8 @@ function resolveShorthand_2(v // @public @deprecated (undocumented) export type ResolveShorthandFunction = { -

(value: P | SlotShorthandValue | undefined, options: ResolveShorthandOptions): P; -

(value: P | SlotShorthandValue | null | undefined, options?: ResolveShorthandOptions): P | undefined; +

(value: P | SlotShorthandValue | undefined, options: ResolveShorthandOptions): WithoutSlotRenderFunction

; +

(value: P | SlotShorthandValue | null | undefined, options?: ResolveShorthandOptions): WithoutSlotRenderFunction

| undefined; }; // @public @deprecated (undocumented) @@ -258,13 +258,11 @@ export { SelectionMode_2 as SelectionMode } export function setVirtualParent(child: Node, parent?: Node): void; // @public -export type Slot = IsSingleton> extends true ? WithSlotShorthandValue | UnknownSlotProps, AlternateAs extends keyof JSX.IntrinsicElements = never> = IsSingleton> extends true ? WithSlotShorthandValue> : Type extends React_2.ComponentType ? WithSlotRenderFunction : Type> | { - [As in AlternateAs]: { - as: As; - } & WithSlotRenderFunction>; -}[AlternateAs] | null : 'Error: First parameter to Slot must not be not a union of types. See documentation of Slot type.'; +} & WithSlotRenderFunction> : Type extends ComponentType ? Props extends UnknownSlotProps ? Props : WithSlotRenderFunction : Type> | (AlternateAs extends unknown ? { + as: AlternateAs; +} & WithSlotRenderFunction> : never) | null : 'Error: First parameter to Slot must not be not a union of types. See documentation of Slot type.'; declare namespace slot { export { @@ -291,10 +289,11 @@ export type SlotClassNames = { }; // @public -export type SlotComponentType = Props & { - (props: React_2.PropsWithChildren<{}>): React_2.ReactElement | null; +export type SlotComponentType = WithoutSlotRenderFunction & FunctionComponent<{ + children?: ReactNode; +}> & { [SLOT_RENDER_FUNCTION_SYMBOL]?: SlotRenderFunction; - [SLOT_ELEMENT_TYPE_SYMBOL]: React_2.ComponentType | (Props extends AsIntrinsicElement ? As : keyof JSX.IntrinsicElements); + [SLOT_ELEMENT_TYPE_SYMBOL]: ComponentType | (Props extends AsIntrinsicElement ? As : keyof JSX.IntrinsicElements); [SLOT_CLASS_NAME_PROP_SYMBOL]?: string; }; @@ -308,15 +307,15 @@ export type SlotOptions = { export type SlotPropsRecord = Record; // @public (undocumented) -export type SlotRenderFunction = (Component: React_2.ElementType, props: Omit) => React_2.ReactNode; +export type SlotRenderFunction = (Component: React_2.ElementType, props: Omit) => ReactNode; // @public @deprecated (undocumented) export type Slots = { - [K in keyof S]: ExtractSlotProps extends AsIntrinsicElement ? As : ExtractSlotProps extends React_2.ComponentType ? React_2.ElementType> : React_2.ElementType>; + [K in keyof S]: React_2.ElementType; }; // @public -export type SlotShorthandValue = React_2.ReactChild | React_2.ReactNode[] | React_2.ReactPortal; +export type SlotShorthandValue = React_2.ReactElement | string | number | Iterable | React_2.ReactPortal; // @public export const SSRProvider: React_2.FC<{ @@ -335,8 +334,9 @@ export type TriggerProps = { export type UnionToIntersection = (U extends unknown ? (x: U) => U : never) extends (x: infer I) => U ? I : never; // @public -export type UnknownSlotProps = Pick, 'children' | 'className' | 'style'> & { +export type UnknownSlotProps = Pick, 'className' | 'style'> & { as?: keyof JSX.IntrinsicElements; + children?: ReactNode; }; // @internal diff --git a/packages/react-components/react-utilities/src/compose/assertSlots.ts b/packages/react-components/react-utilities/src/compose/assertSlots.ts index 5b2d20d1ad38f4..bf93df9657d74c 100644 --- a/packages/react-components/react-utilities/src/compose/assertSlots.ts +++ b/packages/react-components/react-utilities/src/compose/assertSlots.ts @@ -34,6 +34,7 @@ export function assertSlots(state: unknown): asse */ if (process.env.NODE_ENV !== 'production') { const typedState = state as ComponentState; + // eslint-disable-next-line @typescript-eslint/no-deprecated for (const slotName of Object.keys(typedState.components)) { const slotElement = typedState[slotName]; if (slotElement === undefined) { @@ -44,6 +45,7 @@ export function assertSlots(state: unknown): asse // FIXME: this slot will still fail to support child render function scenario if (!isSlot(slotElement)) { typedState[slotName as keyof ComponentState] = slot.always(slotElement, { + // eslint-disable-next-line @typescript-eslint/no-deprecated elementType: typedState.components[slotName] as React.ComponentType<{}>, }) as ComponentState[keyof ComponentState]; // eslint-disable-next-line no-console @@ -56,13 +58,17 @@ export function assertSlots(state: unknown): asse // This means a slot is being declared by using resolveShorthand on the state hook, // but the render method is using the new `assertSlots` method. That scenario can be solved by simply updating the slot element with the proper element type const { [SLOT_ELEMENT_TYPE_SYMBOL]: elementType } = slotElement; + // eslint-disable-next-line @typescript-eslint/no-deprecated if (elementType !== typedState.components[slotName]) { + // eslint-disable-next-line @typescript-eslint/no-deprecated slotElement[SLOT_ELEMENT_TYPE_SYMBOL] = typedState.components[slotName] as React.ComponentType<{}>; // eslint-disable-next-line no-console console.warn(/** #__DE-INDENT__ */ ` @fluentui/react-utilities [${assertSlots.name}]: "state.${slotName}" element type differs from "state.components.${slotName}", - ${elementType} !== ${typedState.components[slotName]}. + ${elementType} !== ${ + typedState.components[slotName] /* eslint-disable-line @typescript-eslint/no-deprecated */ + }. Be sure to create slots properly by using "slot.always" or "slot.optional" with the correct elementType. `); } diff --git a/packages/react-components/react-utilities/src/compose/deprecated/getSlots.ts b/packages/react-components/react-utilities/src/compose/deprecated/getSlots.ts index c75947a2bc85b6..ea8e7f46ad13cb 100644 --- a/packages/react-components/react-utilities/src/compose/deprecated/getSlots.ts +++ b/packages/react-components/react-utilities/src/compose/deprecated/getSlots.ts @@ -1,40 +1,23 @@ import * as React from 'react'; import { omit } from '../../utils/omit'; -import type { - AsIntrinsicElement, - ComponentState, - ExtractSlotProps, - SlotPropsRecord, - SlotRenderFunction, - UnknownSlotProps, -} from '../types'; +import type { ComponentState, SlotPropsRecord, SlotRenderFunction, UnknownSlotProps } from '../types'; import { isSlot } from '../isSlot'; import { SLOT_RENDER_FUNCTION_SYMBOL } from '../constants'; -import { UnionToIntersection } from '../../utils/types'; /** * @deprecated - use slot.always or slot.optional combined with assertSlots instead */ export type Slots = { - [K in keyof S]: ExtractSlotProps extends AsIntrinsicElement - ? // for slots with an `as` prop, the slot will be any one of the possible values of `as` - As - : ExtractSlotProps extends React.ComponentType - ? React.ElementType> - : React.ElementType>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [K in keyof S]: React.ElementType; }; /** * @deprecated - use slot.always or slot.optional combined with assertSlots instead */ 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<> - UnionToIntersection // Slot<'div', 'span'> - : ExtractSlotProps extends React.ComponentType - ? P // Slot - : ExtractSlotProps; // Slot + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [K in keyof S]-?: any; }; /** @@ -57,20 +40,22 @@ export type ObjectSlotProps = { * @returns An object containing the `slots` map and `slotProps` map. */ export function getSlots( - state: ComponentState, + state: unknown, ): { // eslint-disable-next-line @typescript-eslint/no-deprecated slots: Slots; // eslint-disable-next-line @typescript-eslint/no-deprecated slotProps: ObjectSlotProps; } { + const typeState = state as ComponentState; // eslint-disable-next-line @typescript-eslint/no-deprecated const slots = {} as Slots; const slotProps = {} as R; - const slotNames: (keyof R)[] = Object.keys(state.components); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const slotNames: (keyof R)[] = Object.keys(typeState.components); for (const slotName of slotNames) { - const [slot, props] = getSlot(state, slotName); + const [slot, props] = getSlot(typeState, slotName); // eslint-disable-next-line @typescript-eslint/no-deprecated slots[slotName] = slot as Slots[typeof slotName]; slotProps[slotName] = props; @@ -96,15 +81,19 @@ function getSlot( const renderFunction = isSlot(props) ? props[SLOT_RENDER_FUNCTION_SYMBOL] : undefined; const slot = ( - state.components?.[slotName] === undefined || typeof state.components[slotName] === 'string' - ? asProp || state.components?.[slotName] || 'div' - : state.components[slotName] + state.components?.[slotName] === undefined || // eslint-disable-line @typescript-eslint/no-deprecated + // eslint-disable-next-line @typescript-eslint/no-deprecated + typeof state.components[slotName] === 'string' + ? // eslint-disable-next-line @typescript-eslint/no-deprecated + asProp || state.components?.[slotName] || 'div' + : // eslint-disable-next-line @typescript-eslint/no-deprecated + state.components[slotName] ) as React.ElementType; if (renderFunction || typeof children === 'function') { const render = (renderFunction || children) as SlotRenderFunction; return [ - React.Fragment, + React.Fragment as React.ElementType, { children: render(slot, rest as Omit), } as unknown as R[K], @@ -112,6 +101,7 @@ function getSlot( } const shouldOmitAsProp = typeof slot === 'string' && asProp; + // eslint-disable-next-line @typescript-eslint/no-deprecated 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/deprecated/getSlotsNext.ts b/packages/react-components/react-utilities/src/compose/deprecated/getSlotsNext.ts index 9889ea0654fdf7..f8dc0a1f58be6f 100644 --- a/packages/react-components/react-utilities/src/compose/deprecated/getSlotsNext.ts +++ b/packages/react-components/react-utilities/src/compose/deprecated/getSlotsNext.ts @@ -13,21 +13,23 @@ import { ObjectSlotProps, Slots } from './getSlots'; * @deprecated use slot.always or slot.optional combined with assertSlots instead */ export function getSlotsNext( - state: ComponentState, + state: unknown, ): { // eslint-disable-next-line @typescript-eslint/no-deprecated slots: Slots; // eslint-disable-next-line @typescript-eslint/no-deprecated slotProps: ObjectSlotProps; } { + const typedState = state as ComponentState; // eslint-disable-next-line @typescript-eslint/no-deprecated const slots = {} as Slots; const slotProps = {} as R; - const slotNames: (keyof R)[] = Object.keys(state.components); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const slotNames: (keyof R)[] = Object.keys(typedState.components); for (const slotName of slotNames) { // eslint-disable-next-line @typescript-eslint/no-deprecated - const [slot, props] = getSlotNext(state, slotName); + const [slot, props] = getSlotNext(typedState, slotName); // eslint-disable-next-line @typescript-eslint/no-deprecated slots[slotName] = slot as Slots[typeof slotName]; slotProps[slotName] = props; @@ -54,12 +56,17 @@ function getSlotNext( const { as: asProp, ...propsWithoutAs } = props as NonUndefined; const slot = ( - state.components?.[slotName] === undefined || typeof state.components[slotName] === 'string' - ? asProp || state.components?.[slotName] || 'div' - : state.components[slotName] + state.components?.[slotName] === undefined || // eslint-disable-line @typescript-eslint/no-deprecated + // eslint-disable-next-line @typescript-eslint/no-deprecated + typeof state.components[slotName] === 'string' + ? // eslint-disable-next-line @typescript-eslint/no-deprecated + asProp || state.components?.[slotName] || 'div' + : // eslint-disable-next-line @typescript-eslint/no-deprecated + state.components[slotName] ) as React.ElementType; const shouldOmitAsProp = typeof slot === 'string' && asProp; + // eslint-disable-next-line @typescript-eslint/no-deprecated const slotProps: UnknownSlotProps = shouldOmitAsProp ? propsWithoutAs : props; return [slot, slotProps as R[K]]; diff --git a/packages/react-components/react-utilities/src/compose/deprecated/resolveShorthand.ts b/packages/react-components/react-utilities/src/compose/deprecated/resolveShorthand.ts index 5ba28a48fe71c7..498f618ca739ea 100644 --- a/packages/react-components/react-utilities/src/compose/deprecated/resolveShorthand.ts +++ b/packages/react-components/react-utilities/src/compose/deprecated/resolveShorthand.ts @@ -1,5 +1,5 @@ import * as slot from '../slot'; -import type { SlotShorthandValue, UnknownSlotProps } from '../types'; +import type { UnknownSlotProps, SlotShorthandValue, WithoutSlotRenderFunction } from '../types'; /** * @deprecated - use slot.always or slot.optional combined with assertSlots instead @@ -12,11 +12,14 @@ export type ResolveShorthandOptions = R * @deprecated use slot.always or slot.optional combined with assertSlots instead */ export type ResolveShorthandFunction = { - // eslint-disable-next-line @typescript-eslint/no-deprecated -

(value: P | SlotShorthandValue | undefined, options: ResolveShorthandOptions): P; +

( + value: P | SlotShorthandValue | undefined, + // eslint-disable-next-line @typescript-eslint/no-deprecated + options: ResolveShorthandOptions, + ): WithoutSlotRenderFunction

; // eslint-disable-next-line @typescript-eslint/no-deprecated

(value: P | SlotShorthandValue | null | undefined, options?: ResolveShorthandOptions): - | P + | WithoutSlotRenderFunction

| undefined; }; @@ -27,7 +30,7 @@ export type ResolveShorthandFunction = (value, options) => @@ -37,4 +40,4 @@ export const resolveShorthand: ResolveShorthandFunction = (val // elementType as undefined is the way to identify between a slot and a resolveShorthand call // in the case elementType is undefined assertSlots will fail, ensuring it'll only work with slot method. elementType: undefined!, - }); + }) as WithoutSlotRenderFunction; diff --git a/packages/react-components/react-utilities/src/compose/index.ts b/packages/react-components/react-utilities/src/compose/index.ts index 756a54c8a9cf06..b4b474a3168c85 100644 --- a/packages/react-components/react-utilities/src/compose/index.ts +++ b/packages/react-components/react-utilities/src/compose/index.ts @@ -11,8 +11,6 @@ export type { RefAttributes, InferredElementRefType, IsSingleton, - PropsWithoutChildren, - PropsWithoutRef, Slot, SlotClassNames, SlotComponentType, @@ -21,6 +19,7 @@ export type { SlotShorthandValue, UnknownSlotProps, } from './types'; + export { isResolvedShorthand } from './isResolvedShorthand'; export { SLOT_CLASS_NAME_PROP_SYMBOL, SLOT_ELEMENT_TYPE_SYMBOL, SLOT_RENDER_FUNCTION_SYMBOL } from './constants'; export { isSlot } from './isSlot'; @@ -41,3 +40,4 @@ export { getSlotsNext } from './deprecated/getSlotsNext'; export { slot }; export type { SlotOptions } from './slot'; +export type { PropsWithoutChildren, PropsWithoutRef } from '../utils/types'; diff --git a/packages/react-components/react-utilities/src/compose/slot.ts b/packages/react-components/react-utilities/src/compose/slot.ts index f8e506ca11cc7f..ee716fb459964e 100644 --- a/packages/react-components/react-utilities/src/compose/slot.ts +++ b/packages/react-components/react-utilities/src/compose/slot.ts @@ -86,7 +86,7 @@ export function resolveShorthand(value) ) { @@ -105,3 +105,7 @@ export function resolveShorthand => + typeof value === 'object' && value !== null && Symbol.iterator in value; diff --git a/packages/react-components/react-utilities/src/compose/types.ts b/packages/react-components/react-utilities/src/compose/types.ts index c4194d28ad34a3..8941c8b4694e08 100644 --- a/packages/react-components/react-utilities/src/compose/types.ts +++ b/packages/react-components/react-utilities/src/compose/types.ts @@ -1,11 +1,17 @@ import * as React from 'react'; import { SLOT_CLASS_NAME_PROP_SYMBOL, SLOT_ELEMENT_TYPE_SYMBOL, SLOT_RENDER_FUNCTION_SYMBOL } from './constants'; -import type { DistributiveOmit, ReplaceNullWithUndefined } from '../utils/types'; +import type { + ComponentType, + FunctionComponent, + NamedExoticComponent, + PropsWithoutChildren, + PropsWithoutRef, + ReactNode, + ReactVersionDependent, + ReplaceNullWithUndefined, +} from '../utils/types'; -export type SlotRenderFunction = ( - Component: React.ElementType, - props: Omit, -) => React.ReactNode; +export type SlotRenderFunction = (Component: React.ElementType, props: Omit) => ReactNode; /** * Matches any component's Slots type (such as ButtonSlots). @@ -18,7 +24,7 @@ export type SlotPropsRecord = Record | React.ReactPortal; /** * Matches any slot props type. @@ -26,8 +32,9 @@ export type SlotShorthandValue = React.ReactChild | React.ReactNode[] | React.Re * This should ONLY be used in type templates as in `extends UnknownSlotProps`; * it shouldn't be used as the type of a slot. */ -export type UnknownSlotProps = Pick, 'children' | 'className' | 'style'> & { +export type UnknownSlotProps = Pick, 'className' | 'style'> & { as?: keyof JSX.IntrinsicElements; + children?: ReactNode; }; /** @@ -38,13 +45,38 @@ type WithSlotShorthandValue = | ('children' extends keyof Props ? Extract : never); /** + * @internal * Helper type for {@link Slot}. Takes the props we want to support for a slot and adds the ability for `children` * to be a render function that takes those props. + * + * Notes: For React 17 and earlier, `children` can be a render function that returns a ReactNode. + * For React 18 and later, `children` can be any value, as React.ReactNode is a more strict type and does not allow functions anymore. + * This means that the render functions need to be asserted as `SlotRenderFunction` for React 18 and later. + * + * @example + * ```tsx + * // For React 17 and earlier: + * }} /> + * + * // For React 18 and later: + * as SlotRenderFunction }} /> + * ``` */ -type WithSlotRenderFunction = Props & { - children?: ('children' extends keyof Props ? Props['children'] : never) | SlotRenderFunction; +export type WithSlotRenderFunction = PropsWithoutChildren & { + children?: 'children' extends keyof Props + ? ReactVersionDependent> + : never; }; +/** + * @internal + */ +export type WithoutSlotRenderFunction = Props extends unknown + ? 'children' extends keyof Props + ? Omit & { children?: Exclude } + : Props + : never; + /** * HTML element types that are not allowed to have children. * @@ -98,20 +130,23 @@ type IntrinsicElementProps = Type exte * ``` */ export type Slot< - Type extends keyof JSX.IntrinsicElements | React.ComponentType | React.VoidFunctionComponent | UnknownSlotProps, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Type extends keyof JSX.IntrinsicElements | ComponentType | UnknownSlotProps, AlternateAs extends keyof JSX.IntrinsicElements = never, > = IsSingleton> extends true ? | WithSlotShorthandValue< Type extends keyof JSX.IntrinsicElements // Intrinsic elements like `div` ? { as?: Type } & WithSlotRenderFunction> - : Type extends React.ComponentType // Component types like `typeof Button` - ? WithSlotRenderFunction + : Type extends ComponentType // Component types like `typeof Button` + ? Props extends UnknownSlotProps + ? Props + : WithSlotRenderFunction : Type // Props types like `ButtonProps` > - | { - [As in AlternateAs]: { as: As } & WithSlotRenderFunction>; - }[AlternateAs] + | (AlternateAs extends unknown + ? { as: AlternateAs } & WithSlotRenderFunction> + : never) | null : 'Error: First parameter to Slot must not be not a union of types. See documentation of Slot type.'; @@ -135,24 +170,6 @@ export type IsSingleton = { [K in T]: Exclude extends ne */ export type AsIntrinsicElement = { as?: As }; -/** - * Removes the 'ref' prop from the given Props type, leaving unions intact (such as the discriminated union created by - * IntrinsicSlotProps). This allows IntrinsicSlotProps to be used with React.forwardRef. - * - * The conditional "extends unknown" (always true) exploits a quirk in the way TypeScript handles conditional - * types, to prevent unions from being expanded. - */ -export type PropsWithoutRef

= 'ref' extends keyof P ? DistributiveOmit : P; - -/** - * Removes the 'ref' prop from the given Props type, leaving unions intact (such as the discriminated union created by - * IntrinsicSlotProps). This allows IntrinsicSlotProps to be used with React.forwardRef. - * - * The conditional "extends unknown" (always true) exploits a quirk in the way TypeScript handles conditional - * types, to prevent unions from being expanded. - */ -export type PropsWithoutChildren

= 'children' extends keyof P ? DistributiveOmit : P; - /** * Removes SlotShorthandValue and null from the slot type, extracting just the slot's Props object. */ @@ -164,30 +181,33 @@ export type ExtractSlotProps = Exclude = // Include a prop for each slot (see note below about the Omit) + // Note: the `Omit` here is a little tricky. Here's what it's doing: + // * If the Primary slot is 'root', then omit the `root` slot prop. + // * Otherwise, don't omit any props: include *both* the Primary and `root` props. + // We need both props to allow the user to specify native props for either slot because the `root` slot is + // special and always gets className and style props, per RFC https://github.com/microsoft/fluentui/pull/18983 Omit & // Include all of the props of the primary slot inline in the component's props PropsWithoutRef>; -// Note: the `Omit` above is a little tricky. Here's what it's doing: -// * If the Primary slot is 'root', then omit the `root` slot prop. -// * Otherwise, don't omit any props: include *both* the Primary and `root` props. -// We need both props to allow the user to specify native props for either slot because the `root` slot is -// special and always gets className and style props, per RFC https://github.com/microsoft/fluentui/pull/18983 - /** * Defines the State object of a component given its slots. */ export type ComponentState = { + /** + * @deprecated + * The base element type for each slot. + * This property is deprecated and will be removed in a future version. + * The slot base element type is declared through `slot.*(slotShorthand, {elementType: ElementType})` instead. + */ components: { - [Key in keyof Slots]-?: - | React.ComponentType> - | (ExtractSlotProps extends AsIntrinsicElement ? As : keyof JSX.IntrinsicElements); + [Key in keyof Slots]-?: React.ElementType; }; } & { // Include a prop for each slot, with the shorthand resolved to a props object // The root slot can never be null, so also exclude null from it [Key in keyof Slots]: ReplaceNullWithUndefined< - Exclude + WithoutSlotRenderFunction> >; }; @@ -223,14 +243,7 @@ export type InferredElementRefType = ObscureEventName extends keyof Props * - `forwardRef` component do not support string refs. * - uses custom `RefAttributes` which is compatible with all React versions enforcing no `string` allowance. */ -export type ForwardRefComponent = React.ForwardRefExoticComponent< - Props & RefAttributes> ->; -// A definition like this would also work, but typescript is more likely to unnecessarily expand -// the props type with this version (and it's likely much more expensive to evaluate) -// export type ForwardRefComponent = Props extends React.DOMAttributes -// ? React.ForwardRefExoticComponent & React.RefAttributes -// : never; +export type ForwardRefComponent = NamedExoticComponent>>; /** * Helper type to correctly define the slot class names object. @@ -243,27 +256,24 @@ export type SlotClassNames = { * A definition of a slot, as a component, very similar to how a React component is declared, * but with some additional metadata that is used to determine how to render the slot. */ -export type SlotComponentType = Props & { - /** - * **NOTE**: Slot components are not callable. - */ - (props: React.PropsWithChildren<{}>): React.ReactElement | null; - /** - * @internal - */ - [SLOT_RENDER_FUNCTION_SYMBOL]?: SlotRenderFunction; - /** - * @internal - */ - [SLOT_ELEMENT_TYPE_SYMBOL]: - | React.ComponentType - | (Props extends AsIntrinsicElement ? As : keyof JSX.IntrinsicElements); - /** - * @internal - * The original className prop for the slot, before being modified by the useStyles hook. - */ - [SLOT_CLASS_NAME_PROP_SYMBOL]?: string; -}; +export type SlotComponentType = WithoutSlotRenderFunction & + FunctionComponent<{ children?: ReactNode }> & { + /** + * @internal + */ + [SLOT_RENDER_FUNCTION_SYMBOL]?: SlotRenderFunction; + /** + * @internal + */ + [SLOT_ELEMENT_TYPE_SYMBOL]: + | ComponentType + | (Props extends AsIntrinsicElement ? As : keyof JSX.IntrinsicElements); + /** + * @internal + * The original className prop for the slot, before being modified by the useStyles hook. + */ + [SLOT_CLASS_NAME_PROP_SYMBOL]?: string; + }; /** * Data type for event handlers. It makes data a discriminated union, where each object requires `event` and `type` property. diff --git a/packages/react-components/react-utilities/src/hooks/useMergedRefs.ts b/packages/react-components/react-utilities/src/hooks/useMergedRefs.ts index f7c5e60d8757b7..91a6358d8fb678 100644 --- a/packages/react-components/react-utilities/src/hooks/useMergedRefs.ts +++ b/packages/react-components/react-utilities/src/hooks/useMergedRefs.ts @@ -6,21 +6,35 @@ import * as React from 'react'; */ export type RefObjectFunction = React.RefObject & ((value: T | null) => void); +/** @internal */ +type MutableRefObjectFunction = React.MutableRefObject & ((value: T | null) => void); + /** * React hook to merge multiple React refs (either MutableRefObjects or ref callbacks) into a single ref callback that * updates all provided refs * @param refs - Refs to collectively update with one ref value. * @returns A function with an attached "current" prop, so that it can be treated like a RefObject. */ +// LegacyRef is actually not supported, but in React v18 types this is leaking directly from forwardRef component declaration export function useMergedRefs(...refs: (React.Ref | undefined)[]): RefObjectFunction { 'use no memo'; - const mergedCallback: RefObjectFunction = React.useCallback( + const mergedCallback = React.useCallback( (value: T | null) => { // Update the "current" prop hanging on the function. - (mergedCallback as React.MutableRefObject).current = value; + mergedCallback.current = value; for (const ref of refs) { + if (typeof ref === 'string' && process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.error(/** #__DE-INDENT__ */ ` + @fluentui/react-utilities [useMergedRefs]: + This hook does not support the usage of string refs. Please use React.useRef instead. + + For more info on 'React.useRef', see https://react.dev/reference/react/useRef. + For more info on string refs, see https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-string-refs. + `); + } if (typeof ref === 'function') { ref(value); } else if (ref) { @@ -31,7 +45,7 @@ export function useMergedRefs(...refs: (React.Ref | undefined)[]): RefObje }, // eslint-disable-next-line react-hooks/exhaustive-deps -- already exhaustive [...refs], - ) as RefObjectFunction; + ) as MutableRefObjectFunction; return mergedCallback; } diff --git a/packages/react-components/react-utilities/src/trigger/applyTriggerPropsToChildren.ts b/packages/react-components/react-utilities/src/trigger/applyTriggerPropsToChildren.ts index d13a4d3b8225db..12f3b5dbaf0780 100644 --- a/packages/react-components/react-utilities/src/trigger/applyTriggerPropsToChildren.ts +++ b/packages/react-components/react-utilities/src/trigger/applyTriggerPropsToChildren.ts @@ -27,7 +27,7 @@ export function applyTriggerPropsToChildren( * a FluentTriggerComponent or React Fragment (the same element returned by {@link getTriggerChild}). */ function cloneTriggerTree( - child: React.ReactNode, + child: TriggerProps['children'], triggerProps: TriggerChildProps, ): React.ReactElement { if (!React.isValidElement(child) || child.type === React.Fragment) { diff --git a/packages/react-components/react-utilities/src/utils/types.ts b/packages/react-components/react-utilities/src/utils/types.ts index 3d0f96236b358e..2767316ac1a55c 100644 --- a/packages/react-components/react-utilities/src/utils/types.ts +++ b/packages/react-components/react-utilities/src/utils/types.ts @@ -1,3 +1,5 @@ +import * as React from 'react'; + /** * Helper type that works similar to Omit, * but when modifying an union type it will distribute the omission to all the union members. @@ -21,6 +23,16 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type DistributiveOmit = T extends unknown ? Omit : T; +/** + * @public + * + * Helper type that works similar to Pick, + * but when modifying an union type it will distribute the picking to all the union members. + * + * See {@link https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types} for more information + */ +export type DistributivePick = T extends unknown ? Pick : never; + /** * Converts a union type (`A | B | C`) to an intersection type (`A & B & C`) */ @@ -31,3 +43,94 @@ export type UnionToIntersection = (U extends unknown ? (x: U) => U : never) e * If type T includes `null`, remove it and add `undefined` instead. */ export type ReplaceNullWithUndefined = T extends null ? Exclude | undefined : T; + +/** + * @internal + * With react 18, our `children` type starts leaking everywhere and that causes conflicts on component declaration, specially in the `propTypes` property of + * both `ComponentClass` and `FunctionComponent`. + * + * This type substitutes `React.ComponentType` only keeping the function signature, it omits `propTypes`, `displayName` and other properties that are not + * required for the inference. + */ +export type ComponentType

= ComponentClass

| FunctionComponent

; + +/** + * @internal + * + * On types/react 18 there are two types being delivered, + * they rely on the typescript version to decide which will be consumed {@link https://github.com/DefinitelyTyped/DefinitelyTyped/blob/b59dc3ac1e2770fbd6cdbb90ba52abe04c168196/types/react/package.json#L10} + * + * If TS is higher than 5.0 then the `FunctionComponent` will be returning ReactNode (which we don't support) + * If TS is below or equal to 5.0 then the `FunctionComponent` will be returning ReactElement | null (which we support) + * + * Since it's not possible to have a single type that works for both cases + * (as ReactNode is more specific, and this will break while evaluating functions), + * we need to create our own `FunctionComponent` type + * that will work for both cases. + * + * **THIS TYPE IS INTERNAL AND SHOULD NEVER BE EXPOSED** + */ +export interface FunctionComponent

{ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (props: P): any; + displayName?: string; +} + +export type FC

= FunctionComponent

; + +export interface ExoticComponent

{ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (props: P): any; + $$typeof: symbol; +} + +export interface NamedExoticComponent

extends ExoticComponent

{ + displayName?: string; +} + +/** + * @internal + * **THIS TYPE IS INTERNAL AND SHOULD NEVER BE EXPOSED** + */ +export interface ComponentClass

extends React.StaticLifecycle { + new (props: P): React.Component; +} + +/** + * @internal + * + * on types/react 18 ReactNode becomes a more strict type, which is not compatible with our current implementation. to avoid any issues we are creating our own ReactNode type which allows anything. + * + * This type should only be used for inference purposes, and should never be exposed. + * + * **THIS TYPE IS INTERNAL AND SHOULD NEVER BE EXPOSED** + * + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ReactNode = any; + +/** + * Removes the 'ref' prop from the given Props type, leaving unions intact (such as the discriminated union created by + * IntrinsicSlotProps). This allows IntrinsicSlotProps to be used with React.forwardRef. + */ +export type PropsWithoutRef

= 'ref' extends keyof P ? DistributiveOmit : P; + +/** + * Removes the 'children' prop from the given Props type, leaving unions intact (such as the discriminated union created by + * IntrinsicSlotProps). This allows IntrinsicSlotProps to be used with React.forwardRef. + */ +export type PropsWithoutChildren

= 'children' extends keyof P ? DistributiveOmit : P; + +/** + * @internal + * + * This type is used to determine if the current version of React is 18+ or not. + * + * It checks if the `React.ReactNode` has `{}` it its type. + * If it is, then it means that the current version of React is lower than 18. + * If it is not, then it means that the current version of React is 18 or higher. + * This is useful for ensuring compatibility with different versions of React. + * + * **THIS TYPE IS INTERNAL AND SHOULD NEVER BE EXPOSED** + */ +export type ReactVersionDependent = {} extends React.ReactNode ? Legacy : Modern; diff --git a/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollView/useVirtualizerScrollView.ts b/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollView/useVirtualizerScrollView.ts index 27fa73b27d53e8..21b8ee0acd4996 100644 --- a/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollView/useVirtualizerScrollView.ts +++ b/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollView/useVirtualizerScrollView.ts @@ -68,6 +68,7 @@ export function useVirtualizerScrollView_unstable(props: VirtualizerScrollViewPr return { ...virtualizerState, components: { + // eslint-disable-next-line @typescript-eslint/no-deprecated ...virtualizerState.components, container: 'div', }, diff --git a/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.tsx b/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.tsx index 0e2de6647db6d5..891978344695c2 100644 --- a/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.tsx +++ b/packages/react-components/react-virtualizer/library/src/components/VirtualizerScrollViewDynamic/useVirtualizerScrollViewDynamic.tsx @@ -183,6 +183,7 @@ export function useVirtualizerScrollViewDynamic_unstable( return { ...virtualizerState, components: { + // eslint-disable-next-line @typescript-eslint/no-deprecated ...virtualizerState.components, container: 'div', },