Skip to content
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

[core] Fix proptypes generation when multiple components per file #44058

Merged
merged 7 commits into from
Oct 10, 2024
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
Expand Up @@ -173,8 +173,8 @@ function createBabelPlugin({
let importName = '';
let needImport = false;
let alreadyImported = false;
let originalPropTypesPath: null | babel.NodePath = null;
const previousPropTypesSource = new Map<string, string>();
const originalPropTypesPaths = new Map<string, babel.NodePath>();
const previousPropTypesSources = new Map<string, Map<string, string>>();

function injectPropTypes(injectOptions: {
path: babel.NodePath;
Expand All @@ -184,6 +184,9 @@ function createBabelPlugin({
}) {
const { path, props, usedProps, nodeName } = injectOptions;

const previousPropTypesSource =
previousPropTypesSources.get(nodeName) || new Map<string, string>();

const source = generatePropTypes(props, {
...otherOptions,
importedName: importName,
Expand All @@ -201,8 +204,10 @@ function createBabelPlugin({

mapOfPropTypes.set(placeholder, source);

const originalPropTypesPath = originalPropTypesPaths.get(nodeName);

// `Component.propTypes` already exists
if (originalPropTypesPath !== null) {
if (originalPropTypesPath) {
originalPropTypesPath.replaceWith(babel.template.ast(placeholder) as babel.Node);
} else if (!emptyPropTypes && babelTypes.isExportNamedDeclaration(path.parent)) {
// in:
Expand Down Expand Up @@ -258,7 +263,12 @@ function createBabelPlugin({
babelTypes.isMemberExpression(node.expression.left) &&
babelTypes.isIdentifier(node.expression.left.property, { name: 'propTypes' })
) {
originalPropTypesPath = nodePath as babel.NodePath;
babelTypes.assertIdentifier(node.expression.left.object);
const componentName = node.expression.left.object.name;
originalPropTypesPaths.set(componentName, nodePath);

const previousPropTypesSource = new Map<string, string>();
previousPropTypesSources.set(componentName, previousPropTypesSource);

let maybeObjectExpression = node.expression.right;
// Component.propTypes = {} as any;
Expand Down
1 change: 1 addition & 0 deletions packages/mui-base/src/MenuItem/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const InnerMenuItem = React.memo(
*
* - [MenuItem API](https://mui.com/base-ui/react-menu/components-api/#menu-item)
*/

const MenuItem = React.forwardRef(function MenuItem(
props: MenuItemProps,
ref: React.ForwardedRef<Element>,
Expand Down
139 changes: 85 additions & 54 deletions packages/mui-base/src/Option/Option.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,60 +20,90 @@ function useUtilityClasses<OptionValue>(ownerState: OptionOwnerState<OptionValue
return composeClasses(slots, useClassNamesOverride(getOptionUtilityClass));
}

const InnerOption = React.memo(
React.forwardRef<Element, OptionProps<unknown>>(function Option<
OptionValue,
RootComponentType extends React.ElementType,
>(props: OptionProps<OptionValue, RootComponentType>, forwardedRef: React.ForwardedRef<Element>) {
const {
children,
disabled = false,
label,
slotProps = {},
slots = {},
value,
...other
} = props;

const Root = slots.root ?? 'li';

const optionRef = React.useRef<HTMLElement>(null);
const combinedRef = useForkRef(optionRef, forwardedRef);

// If `label` is not explicitly provided, the `children` are used for convenience.
// This is used to populate the select's trigger with the selected option's label.
const computedLabel =
label ?? (typeof children === 'string' ? children : optionRef.current?.textContent?.trim());

const { getRootProps, selected, highlighted, index } = useOption({
disabled,
label: computedLabel,
rootRef: combinedRef,
value,
});

const ownerState: OptionOwnerState<OptionValue> = {
...props,
disabled,
highlighted,
index,
selected,
};

const classes = useUtilityClasses(ownerState);

const rootProps: WithOptionalOwnerState<OptionRootSlotProps<OptionValue>> = useSlotProps({
getSlotProps: getRootProps,
elementType: Root,
externalSlotProps: slotProps.root,
externalForwardedProps: other,
className: classes.root,
ownerState,
});

return <Root {...rootProps}>{children}</Root>;
const InnerOption = React.forwardRef<Element, OptionProps<unknown>>(function InnerOption<
OptionValue,
RootComponentType extends React.ElementType,
>(props: OptionProps<OptionValue, RootComponentType>, forwardedRef: React.ForwardedRef<Element>) {
const { children, disabled = false, label, slotProps = {}, slots = {}, value, ...other } = props;

const Root = slots.root ?? 'li';

const optionRef = React.useRef<HTMLElement>(null);
const combinedRef = useForkRef(optionRef, forwardedRef);

// If `label` is not explicitly provided, the `children` are used for convenience.
// This is used to populate the select's trigger with the selected option's label.
const computedLabel =
label ?? (typeof children === 'string' ? children : optionRef.current?.textContent?.trim());

const { getRootProps, selected, highlighted, index } = useOption({
disabled,
label: computedLabel,
rootRef: combinedRef,
value,
});

const ownerState: OptionOwnerState<OptionValue> = {
...props,
disabled,
highlighted,
index,
selected,
};

const classes = useUtilityClasses(ownerState);

const rootProps: WithOptionalOwnerState<OptionRootSlotProps<OptionValue>> = useSlotProps({
getSlotProps: getRootProps,
elementType: Root,
externalSlotProps: slotProps.root,
externalForwardedProps: other,
className: classes.root,
ownerState,
});

return <Root {...rootProps}>{children}</Root>;
});

InnerOption.propTypes /* remove-proptypes */ = {
// ┌────────────────────────────── Warning ──────────────────────────────┐
// │ These PropTypes are generated from the TypeScript type definitions. │
// │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
// └─────────────────────────────────────────────────────────────────────┘
children: PropTypes.node,
className: PropTypes.string,
/**
* If `true`, the option will be disabled.
* @default false
*/
disabled: PropTypes.bool,
/**
* A text representation of the option's content.
* Used for keyboard text navigation matching.
*/
label: PropTypes.string,
/**
* The props used for each slot inside the Option.
* @default {}
*/
slotProps: PropTypes.shape({
root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
}),
);
/**
* The components used for each slot inside the Option.
* Either a string to use a HTML element or a component.
* @default {}
*/
slots: PropTypes.shape({
root: PropTypes.elementType,
}),
/**
* The value of the option.
*/
value: PropTypes.any.isRequired,
} as any;

const InnerOptionMemo = React.memo(InnerOption);

/**
* An unstyled option to be used within a Select.
Expand All @@ -86,6 +116,7 @@ const InnerOption = React.memo(
*
* - [Option API](https://mui.com/base-ui/react-select/components-api/#option)
*/

const Option = React.forwardRef(function Option<OptionValue>(
props: OptionProps<OptionValue>,
ref: React.ForwardedRef<Element>,
Expand All @@ -100,7 +131,7 @@ const Option = React.forwardRef(function Option<OptionValue>(

return (
<ListContext.Provider value={contextValue}>
<InnerOption {...props} ref={ref} />
<InnerOptionMemo {...props} ref={ref} />
</ListContext.Provider>
);
}) as OptionType;
Expand Down
Loading