Skip to content

fix(react-components): use custom RefAttributes instead of React.RefAttributes #34590

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although api.md changed the type is exactly the same -> thus no bump

"comment": "fix: use custom RefAttributes instead of React.RefAttributes",
"packageName": "@fluentui/react-migration-v8-v9",
"email": "[email protected]",
"dependentChangeType": "none"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "chore: use custom RefAttributes instead of React.RefAttributes",
"packageName": "@fluentui/react-nav-preview",
"email": "[email protected]",
"dependentChangeType": "none"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "chore: disable lint error on React.RefAttibute within getIntrinsicElementProps",
"packageName": "@fluentui/react-utilities",
"email": "[email protected]",
"dependentChangeType": "none"
}
12 changes: 12 additions & 0 deletions packages/eslint-plugin/src/configs/react.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ module.exports = {
},
],
'react-compiler/react-compiler': ['error'],
'@typescript-eslint/no-restricted-types': [
'error',
{
types: {
'React.RefAttributes': {
message:
'`React.RefAttributes` is leaking string starting @types/[email protected] creating invalid type contracts. Use `RefAttributes` from @fluentui/react-utilities instead',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tbh this is not the most robust rule as it doesn't work on interface extensions, and also doesn't distinguishes about the imports. the autofixer is also naive as it won't provide proper import.

for now it's good enough, but in future we will introduce custom rule

fixWith: 'RefAttributes',
},
},
},
],
},
overrides: [
// Enable rules requiring type info only for appropriate files/circumstances
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const brandWeb: BrandVariants;
export const ButtonShim: React_2.ForwardRefExoticComponent<IBaseButtonProps & React_2.RefAttributes<HTMLButtonElement>>;

// @public (undocumented)
export const CheckboxShim: React_2.ForwardRefExoticComponent<Pick<ICheckboxProps, "label" | "title" | "className" | "key" | "disabled" | "name" | "defaultChecked" | "id" | "onChange" | "componentRef" | "styles" | "theme" | "checked" | "ariaLabel" | "required" | "ariaDescribedBy" | "ariaLabelledBy" | "ariaPositionInSet" | "ariaSetSize" | "boxSide" | "checkmarkIconProps" | "defaultIndeterminate" | "indeterminate" | "inputProps" | "onRenderLabel"> & React_2.RefAttributes<HTMLInputElement>>;
export const CheckboxShim: React_2.ForwardRefExoticComponent<ICheckboxProps & React_2.RefAttributes<HTMLInputElement>>;
Copy link
Contributor Author

@Hotell Hotell Jun 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the same API without unwrapping -> result of normalizing patterns and using explicit function types


// @public
export type ColorVariants = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@ import * as React from 'react';
import type { IButtonProps } from '@fluentui/react';

import { Button } from '@fluentui/react-components';
import type { RefAttributes } from '@fluentui/react-utilities';

import { shimButtonProps } from './shimButtonProps';

/**
* Shims a v8 ActionButton to render a v9 Button
*/
export const ActionButtonShim: React.ForwardRefExoticComponent<IButtonProps & React.RefAttributes<HTMLButtonElement>> =
React.forwardRef((props, _ref) => {
const variantProps = {
...props,
variantClassName: 'ms-Button--action ms-Button--command',
};
export const ActionButtonShim: React.ForwardRefExoticComponent<
IButtonProps &
// eslint-disable-next-line @typescript-eslint/no-restricted-types -- this is expected in order to be compatible with v8, as every v8 interface contains `React.RefAttributes` to accept ref as string
React.RefAttributes<HTMLButtonElement>
> = React.forwardRef((props, _ref) => {
const variantProps = {
...props,
variantClassName: 'ms-Button--action ms-Button--command',
};

const shimProps = shimButtonProps(variantProps);
const shimProps = shimButtonProps(variantProps);

return <Button {...(props as React.RefAttributes<HTMLButtonElement>)} {...shimProps} appearance="transparent" />;
});
return <Button {...(props as RefAttributes<HTMLButtonElement>)} {...shimProps} appearance="transparent" />;
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,25 @@ import * as React from 'react';
import type { IBaseButtonProps } from '@fluentui/react';

import { Button } from '@fluentui/react-components';
import type { RefAttributes } from '@fluentui/react-utilities';

import { shimButtonProps } from './shimButtonProps';
import { ToggleButtonShim } from './ToggleButtonShim';
import { CompoundButtonShim } from './CompoundButtonShim';

export const ButtonShim: React.ForwardRefExoticComponent<IBaseButtonProps & React.RefAttributes<HTMLButtonElement>> =
React.forwardRef((props, _ref) => {
const shimProps = shimButtonProps(props);
export const ButtonShim: React.ForwardRefExoticComponent<
IBaseButtonProps &
// eslint-disable-next-line @typescript-eslint/no-restricted-types -- this is expected in order to be compatible with v8, as every v8 interface contains `React.RefAttributes` to accept ref as string
React.RefAttributes<HTMLButtonElement>
> = React.forwardRef((props, _ref) => {
const shimProps = shimButtonProps(props);

if (props.toggle) {
return <ToggleButtonShim {...props}>{props.children}</ToggleButtonShim>;
}
if (props.secondaryText || props.onRenderDescription?.(props)) {
return <CompoundButtonShim {...props} />;
}
if (props.toggle) {
return <ToggleButtonShim {...props}>{props.children}</ToggleButtonShim>;
}
if (props.secondaryText || props.onRenderDescription?.(props)) {
return <CompoundButtonShim {...props} />;
}

return <Button {...(props as React.RefAttributes<HTMLButtonElement>)} {...shimProps} />;
});
return <Button {...(props as RefAttributes<HTMLButtonElement>)} {...shimProps} />;
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import type { IButtonProps } from '@fluentui/react';

import { CompoundButton } from '@fluentui/react-components';
import type { CompoundButtonProps } from '@fluentui/react-components';
import type { RefAttributes } from '@fluentui/react-utilities';

import { shimButtonProps } from './shimButtonProps';

/**
* Shims v8 CompoundButton to render a v9 CompoundButton
*/
export const CompoundButtonShim: React.ForwardRefExoticComponent<
IButtonProps & React.RefAttributes<HTMLButtonElement>
IButtonProps &
// eslint-disable-next-line @typescript-eslint/no-restricted-types -- this is expected in order to be compatible with v8, as every v8 interface contains `React.RefAttributes` to accept ref as string
React.RefAttributes<HTMLButtonElement>
> = React.forwardRef((props, _ref) => {
const variantProps = {
...props,
Expand All @@ -23,5 +26,5 @@ export const CompoundButtonShim: React.ForwardRefExoticComponent<
secondaryContent: props.secondaryText || props.onRenderDescription?.(props),
};

return <CompoundButton {...(props as React.RefAttributes<HTMLButtonElement>)} {...shimProps} />;
return <CompoundButton {...(props as RefAttributes<HTMLButtonElement>)} {...shimProps} />;
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { ButtonShim } from './ButtonShim';
/**
* Shims a v8 DefaultButton to render a v9 Button
*/
export const DefaultButtonShim: React.ForwardRefExoticComponent<IButtonProps & React.RefAttributes<HTMLButtonElement>> =
React.forwardRef((props, _ref) => {
return <ButtonShim {...props} variantClassName={props.primary ? 'ms-Button--primary' : 'ms-Button--default'} />;
});
export const DefaultButtonShim: React.ForwardRefExoticComponent<
IButtonProps &
// eslint-disable-next-line @typescript-eslint/no-restricted-types -- this is expected in order to be compatible with v8, as every v8 interface contains `React.RefAttributes` to accept ref as string
React.RefAttributes<HTMLButtonElement>
> = React.forwardRef((props, _ref) => {
return <ButtonShim {...props} variantClassName={props.primary ? 'ms-Button--primary' : 'ms-Button--default'} />;
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,36 @@ import { MenuItemShim, shimMenuProps } from '../Menu/index';

import { shimButtonProps } from './shimButtonProps';

export const MenuButtonShim: React.ForwardRefExoticComponent<IButtonProps & React.RefAttributes<HTMLButtonElement>> =
React.forwardRef((props, _ref) => {
const variantProps = {
...props,
variantClassName: props.primary ? 'ms-Button--primary' : 'ms-Button--default',
};
export const MenuButtonShim: React.ForwardRefExoticComponent<
IButtonProps &
// eslint-disable-next-line @typescript-eslint/no-restricted-types -- this is expected in order to be compatible with v8, as every v8 interface contains `React.RefAttributes` to accept ref as string
React.RefAttributes<HTMLButtonElement>
> = React.forwardRef((props, _ref) => {
const variantProps = {
...props,
variantClassName: props.primary ? 'ms-Button--primary' : 'ms-Button--default',
};

const shimProps: MenuButtonProps = {
...shimButtonProps(variantProps),
};
const shimProps: MenuButtonProps = {
...shimButtonProps(variantProps),
};

const shimmedMenuProps = props.menuProps ? shimMenuProps(props.menuProps) : {};
const shimmedMenuProps = props.menuProps ? shimMenuProps(props.menuProps) : {};

return (
<Menu {...shimmedMenuProps}>
<MenuTrigger>
<MenuButton {...shimProps} />
</MenuTrigger>
<MenuPopover>
<MenuList>
{props.menuProps?.items.map(item => (
// key is added through item spread
// eslint-disable-next-line react/jsx-key
<MenuItemShim {...item} />
))}
</MenuList>
</MenuPopover>
</Menu>
);
});
return (
<Menu {...shimmedMenuProps}>
<MenuTrigger>
<MenuButton {...shimProps} />
</MenuTrigger>
<MenuPopover>
<MenuList>
{props.menuProps?.items.map(item => (
// key is added through item spread
// eslint-disable-next-line react/jsx-key
<MenuItemShim {...item} />
))}
</MenuList>
</MenuPopover>
</Menu>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { ButtonShim } from './ButtonShim';
/**
* Shims v8 PrimaryButton to render a v9 Button
*/
export const PrimaryButtonShim: React.ForwardRefExoticComponent<IButtonProps & React.RefAttributes<HTMLButtonElement>> =
React.forwardRef((props, _ref) => {
return <ButtonShim {...props} primary variantClassName="ms-Button--primary" />;
});
export const PrimaryButtonShim: React.ForwardRefExoticComponent<
IButtonProps &
// eslint-disable-next-line @typescript-eslint/no-restricted-types -- this is expected in order to be compatible with v8, as every v8 interface contains `React.RefAttributes` to accept ref as string
React.RefAttributes<HTMLButtonElement>
> = React.forwardRef((props, _ref) => {
return <ButtonShim {...props} primary variantClassName="ms-Button--primary" />;
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,28 @@ import type { IButtonProps } from '@fluentui/react';

import { ToggleButton } from '@fluentui/react-components';
import type { ToggleButtonProps } from '@fluentui/react-components';
import type { RefAttributes } from '@fluentui/react-utilities';

import { shimButtonProps } from './shimButtonProps';

/**
* Shims v8 ToggleButton to render a v9 ToggleButton
*/
export const ToggleButtonShim: React.ForwardRefExoticComponent<IButtonProps & React.RefAttributes<HTMLButtonElement>> =
React.forwardRef((props, _ref) => {
const variantProps = {
...props,
variantClassName: props.primary ? 'ms-Button--compoundPrimary' : 'ms-Button--compound',
};
export const ToggleButtonShim: React.ForwardRefExoticComponent<
IButtonProps &
// eslint-disable-next-line @typescript-eslint/no-restricted-types -- this is expected in order to be compatible with v8, as every v8 interface contains `React.RefAttributes` to accept ref as string
React.RefAttributes<HTMLButtonElement>
> = React.forwardRef((props, _ref) => {
const variantProps = {
...props,
variantClassName: props.primary ? 'ms-Button--compoundPrimary' : 'ms-Button--compound',
};

const shimProps: ToggleButtonProps = {
...shimButtonProps(variantProps),
checked: props.checked,
defaultChecked: props.defaultChecked,
};
const shimProps: ToggleButtonProps = {
...shimButtonProps(variantProps),
checked: props.checked,
defaultChecked: props.defaultChecked,
};

return <ToggleButton {...(props as React.RefAttributes<HTMLButtonElement>)} {...shimProps} />;
});
return <ToggleButton {...(props as RefAttributes<HTMLButtonElement>)} {...shimProps} />;
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import type { IBaseButtonProps } from '@fluentui/react';

import type { ButtonProps } from '@fluentui/react-components';

export const shimButtonProps = (props: IBaseButtonProps & React.RefAttributes<HTMLButtonElement>): ButtonProps => {
export const shimButtonProps = (
props: IBaseButtonProps &
// eslint-disable-next-line @typescript-eslint/no-restricted-types -- this is expected in order to be compatible with v8, as every v8 interface contains `React.RefAttributes` to accept ref as string
React.RefAttributes<HTMLButtonElement>,
): ButtonProps => {
//TODO: Icon shim. This still renders the v8 icon.
const icon = props.onRenderIcon ? (
props.onRenderIcon(props)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const getClassNames = classNamesFunction<ICheckboxStyleProps, ICheckboxStyles>({
useStaticStyles: false,
});

export const CheckboxShim = React.forwardRef((props: ICheckboxProps, _ref: React.ForwardedRef<HTMLInputElement>) => {
export const CheckboxShim = React.forwardRef((props, _ref) => {
'use no memo';

const { className, styles: stylesV8, onRenderLabel, label, componentRef } = props;
Expand Down Expand Up @@ -51,6 +51,11 @@ export const CheckboxShim = React.forwardRef((props: ICheckboxProps, _ref: React
indicator={{ className: mergeClasses('ms-Checkbox-checkbox', styles.checkbox) }}
/>
);
});
// NOTE: cast is necessary as `ICheckboxProps` extends React.Ref<HTMLDivElement> which is not compatible with our defined React.Ref<HTMLInputElement>
}) as React.ForwardRefExoticComponent<
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unified pattern with the rest of migration code - in this case simple advance declaration wouldn't match because the ref generic mismatch

ICheckboxProps &
// eslint-disable-next-line @typescript-eslint/no-restricted-types -- this is expected in order to be compatible with v8, as every v8 interface contains `React.RefAttributes` to accept ref as string
React.RefAttributes<HTMLInputElement>
>;

CheckboxShim.displayName = 'CheckboxShim';
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import { Drawer, DrawerProps } from '@fluentui/react-drawer';
import { useArrowNavigationGroup } from '@fluentui/react-tabster';
import { slot } from '@fluentui/react-utilities';
import { RefAttributes, slot } from '@fluentui/react-utilities';

import { useNav_unstable } from '../Nav/useNav';
import type { NavDrawerProps, NavDrawerState } from './NavDrawer.types';
Expand Down Expand Up @@ -49,7 +49,7 @@ export const useNavDrawer_unstable = (props: NavDrawerProps, ref: React.Ref<HTML
// this is a problem with the lack of support for union types on React v18
// ComponentState is using React.ComponentType which will try to infer propType
// propTypes WeakValidator signature will break distributive unions making this type invalid
elementType: Drawer as React.FC<DrawerProps & React.RefAttributes<HTMLDivElement>>,
elementType: Drawer as React.FC<DrawerProps & RefAttributes<HTMLDivElement>>,
},
),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,18 @@ type HTMLAttributes = React.HTMLAttributes<any>;
* element type.
*
* Equivalent to {@link getNativeElementProps}, but more type-safe.
*
* @param tagName - The slot's default element type (e.g. 'div')
* @param props - The component's props object
* @param excludedPropNames - List of native props to exclude from the returned value
*/
export const getIntrinsicElementProps = <
Props extends UnknownSlotProps,
ExcludedPropKeys extends Extract<keyof Props, string> = never,
>(
/** The slot's default element type (e.g. 'div') */
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is invalid "JSDOC" - moved to proper JSDoc format

tagName: NonNullable<Props['as']>,
/** The component's props object */
// eslint-disable-next-line @typescript-eslint/no-restricted-types -- in order to not introduce Type Restriction CHANGe which is kinda "breaking change from Types POV", we don't enforce our custom `RefAttributes` in this API, to be compatible with scenarios where non v9 interfaces might be used. This may/will change with React 19
props: Props & React.RefAttributes<InferredElementRefType<Props>>,
/** List of native props to exclude from the returned value */
excludedPropNames?: ExcludedPropKeys[],
) => {
// eslint-disable-next-line @typescript-eslint/no-deprecated
Expand Down
Loading