mui / material-ui

Material UI: Comprehensive React component library that implements Google's Material Design. Free forever.
https://mui.com/material-ui/
MIT License
93.92k stars 32.27k forks source link

Ability to override additional props in every MUI Components #38986

Closed ever-dev closed 1 year ago

ever-dev commented 1 year ago

Duplicates

Latest version

Summary 💡

I want to add new props to MUI components without getting TypeError.

Examples 🌈

No response

Motivation 🔦

I wanted to add a border to the InputAdornment component with the showBorder prop.

I tried to add

// mui-overrides.d.ts
declare module '@mui/material' {
  interface InputAdornmentProps {
    showBorder?: true;
  }
}

// component-overrides.ts
MuiInputAdornment: {
    variants: [
      {
        props: {
          showBorder: true,
        },
        style: ({ theme }) => ({
          '&.MuiInputAdornment-root.MuiInputAdornment-positionStart': {
            borderRight: '1px solid',
            borderRightColor: theme.palette.divider,
            height: '100%',
            '& > p': {
              paddingRight: 8,
            },
          },
          '&.MuiInputAdornment-root.MuiInputAdornment-positionEnd': {
            borderLeft: '1px solid',
            borderLeftColor: theme.palette.divider,
            height: '100%',
            '& > p': {
              paddingLeft: 8,
            },
          },
        }),
      },
    ],
  },

And used

<InputAdornment showBorder position="start">$</InputAdornment>

It shows the border in the application, but TypeScript compiler throws an TypeError that showBorder doesn't exist.

ever-dev commented 1 year ago

The workaround is to create a new component using styled.

import { InputAdornment } from '@mui/material';
import type { InputAdornmentProps } from '@mui/material';
import { styled } from '@mui/material/styles';

const BorderedInputAdornment = styled(InputAdornment)<InputAdornmentProps>(({ theme }) => ({
  '&.MuiInputAdornment-root.MuiInputAdornment-positionStart': {
    borderRight: '1px solid',
    borderRightColor: theme.palette.divider,
    height: '100%',
    '& > p': {
      paddingRight: 8,
    },
  },
  '&.MuiInputAdornment-root.MuiInputAdornment-positionEnd': {
    borderLeft: '1px solid',
    borderLeftColor: theme.palette.divider,
    height: '100%',
    '& > p': {
      paddingLeft: 8,
    },
  },
}));

export default BorderedInputAdornment;

But I don't want to have a separate component while it's possible to do by overriding component.

ever-dev commented 1 year ago

I looked at the source code and I was wondering how to effectively use AdditionalProps.

I changed node_modules/@mui/material/InputAdornment/InputAdornment.d.ts file to the following

import * as React from 'react';
import { SxProps } from '@mui/system';
import { OverridableComponent, OverrideProps } from '../OverridableComponent';
import { Theme } from '..';
import { InputAdornmentClasses } from './inputAdornmentClasses';

export interface InputAdornmentAdditonalPropsOverride {}

export interface InputAdornmentTypeMap<
  AdditionalProps = InputAdornmentAdditonalPropsOverride,
  DefaultComponent extends React.ElementType = 'div',
> {
  props: AdditionalProps & {
    /**
     * Override or extend the styles applied to the component.
     */
    classes?: Partial<InputAdornmentClasses>;
    /**
     * The content of the component, normally an `IconButton` or string.
     */
    children?: React.ReactNode;
    /**
     * Disable pointer events on the root.
     * This allows for the content of the adornment to focus the `input` on click.
     * @default false
     */
    disablePointerEvents?: boolean;
    /**
     * If children is a string then disable wrapping in a Typography component.
     * @default false
     */
    disableTypography?: boolean;
    /**
     * The position this adornment should appear relative to the `Input`.
     */
    position: 'start' | 'end';
    /**
     * The system prop that allows defining system overrides as well as additional CSS styles.
     */
    sx?: SxProps<Theme>;
    /**
     * The variant to use.
     * Note: If you are using the `TextField` component or the `FormControl` component
     * you do not have to set this manually.
     */
    variant?: 'standard' | 'outlined' | 'filled';
  };
  defaultComponent: DefaultComponent;
}
/**
 *
 * Demos:
 *
 * - [Text Field](https://mui.com/material-ui/react-text-field/)
 *
 * API:
 *
 * - [InputAdornment API](https://mui.com/material-ui/api/input-adornment/)
 */
declare const InputAdornment: OverridableComponent<InputAdornmentTypeMap>;

export type InputAdornmentProps<
  RootComponent extends React.ElementType = InputAdornmentTypeMap['defaultComponent'],
  AdditionalProps = InputAdornmentAdditonalPropsOverride,
> = OverrideProps<InputAdornmentTypeMap<AdditionalProps, RootComponent>, RootComponent> & {
  component?: React.ElementType;
};

export default InputAdornment;

Basically, I introduced InputAdornmentAdditonalPropsOverride and used it for AdditionalProps for both InputAdornmentTypeMap and InputAdornmentProps. Then I override that interface in my mui-override.d.ts

declare module '@mui/material' {
  interface InputAdornmentAdditonalPropsOverride {
    showBorder?: true;
  }
}

TypeScript was happy with it.

ever-dev commented 1 year ago

I'm not sure if there's a way already to use AdditionalProps effectively. If it doesn't, I think we should bring this kind of props override to every MUI component so that we can effectively add new props and customize styles.

ever-dev commented 1 year ago

I'm pretty sure it's not only myself who wants this.

I was doing some research and found this StackOverflow question without an answer. https://stackoverflow.com/questions/73281320/how-to-extend-mui-5-typography-with-custom-props-typescript

ZeeshanTamboli commented 1 year ago

@ever-dev You are applying the variant incorrectly which you showed in the description. Follow this documentation on how to create a new component variant. See - https://codesandbox.io/s/thirsty-mcnulty-r4tmlf?file=/src/App.tsx. There's a TypeScript error because we don't provide the interface InputAdornmentPropsVariantOverrides to override the inbuilt variants. That's a separate issue.

As for your question, if you want to have your own props you will have to create a new component wrapping the Material UI component.

ever-dev commented 1 year ago

@ZeeshanTamboli I think there's a confusion. I'm not asking to extend the variant, but I'm asking how to extend props. BTW, it's not a new functionality, it's already working without TypeScript. I just want the team to add TypeScript support for this functionality. Also, I proposed a solution here. just wondering if the MUI team is considering including that.

ZeeshanTamboli commented 1 year ago

I'm asking how to extend props.

You can create your own wrapper component and extend the Material UI's interface with your custom interface for types. Why not this?

Can you provide your use cases examples in the form of CodeSandboxes so that we are one the same page? The issue template is a good starting point.

mogrady88 commented 11 months ago

Im having a similar issue in Tab where I need to add a new prop ( dense ). Like Ever-dev said: I have no issues adding the prop itself but the type definitions are created in a way where I can't extend them and I have to redefine them which means that I will have to watch for any changes to these types moving forward. Can you tell me what AdditionalProps is meant to do? I had the same thought as Ever-dev where extending that AdditionalProps interface would allow me to tap into those types without stomping on the originals.

Ive also tried module augmentation on the TabOwnProps interface and it doesnt seem to be working:

declare module '@mui/material/Tab' {
  interface TabOwnProps {
    /**
       'Applies the dense prop to the Tab component.'
      */
    dense?: boolean;
  }
}

Is there any reason that the above wouldnt work? Its an exported interface so I assumed there wouldnt be any issues

ZeeshanTamboli commented 11 months ago

@mogrady88 Does this work for you - https://codesandbox.io/p/sandbox/white-wave-fxqrlq?file=%2Fsrc%2FApp.tsx%3A44%2C2? AdditionalProps is the second generic type parameter you can use for custom props without having to redefine or extending your custom interface with Material UI component props.

mogrady88 commented 11 months ago

That gets me a lot closer. I think I can come up with the rest based off of your example. Thank you!

mogrady88 commented 11 months ago

Actually @ZeeshanTamboli using TabProps isnt 1-1 with the original definition. Right now I'm creating a CustomTab in the way you've done in your codesandbox and it looks like the use of component with external isnt working because external doesnt seem to be included in TabProps. You can see this if you add external to your props destructuring like so:

const { dense, external, ...other } = props;
ZeeshanTamboli commented 11 months ago

Actually @ZeeshanTamboli using TabProps isnt 1-1 with the original definition. Right now I'm creating a CustomTab in the way you've done in your codesandbox and it looks like the use of component with external isnt working because external doesnt seem to be included in TabProps. You can see this if you add external to your props destructuring like so:

const { dense, external, ...other } = props;

@mogrady88 I am not quite understanding you. It would be better to provide a CodeSandbox. Have you added the external prop to the TabProps type like:

function CustomTab(
  props: TabProps<"div", { dense?: boolean; external?: boolean }>
...
...

CodeSandbox - https://codesandbox.io/p/sandbox/white-wave-fxqrlq?file=%2Fsrc%2FApp.tsx.

mogrady88 commented 11 months ago

I can do that but I shouldnt have to. Here is a codesandbox that shows the issue.

TabProps isnt the same as ExtendButtonBase<TabTypeMap>. The external prop is included in the original type but wrapping my component and using TabProps doesnt include it. So the actual Tab component can take different props than TabProps includes.

You only have to pay attention to CustomTab. Take a look at the props destructuring based off of the TabProps and then look at the actual <Tab ... /> component and see that it accepts props that TabProps doesnt. If Im using TabProps I would expect it to include everything the original type definition for Tab does.

mogrady88 commented 11 months ago

There are also other missing types. Im getting reports that the to prop isnt supported under this new TabProps type. There seems to be a major mismatch between TabProps and the const Tab: ExtendButtonBase<TabTypeMap>

I will be out of town for the next few weeks but Ill check this thread when Im back in office. Thank you for your help in this matter!

ZeeshanTamboli commented 11 months ago

@mogrady88 To enable the href prop, you need to use an anchor element because the href property is not applicable to divs. Use TabProps<"a", { dense?: boolean }> for this. Regarding the external prop, it's not an HTML attribute, so it causes an error.

CodeSandbox link: https://codesandbox.io/p/sandbox/shy-lake-rjnvdr?file=%2Fsrc%2FApp.tsx%3A49%2C7

JairTorres1003 commented 1 month ago

I’ve been working on a project where I encountered an issue while customizing Material UI's Typography component. Specifically, I added a custom property textStrokeWidth to control the stroke width of text. Here's an outline of the implementation and the issue I'm facing.

Current Implementation

I add a typography component and added a new property textStrokeWidth:

import { Typography } from '@mui/material'
import React from 'react'

export const Home = (): React.ReactNode => {
  return (
    <Typography variant='outlineText' fontSize={80} textStrokeWidth={3}>
      Home Component
    </Typography>
  )
}

In my theme configuration, I extended Material UI's theme to handle this property:

import { type Theme, type Components, type PaletteColor } from '@mui/material/styles'

export const dataDisplayCustomizations: Components<Theme> = {
  MuiTypography: {
    styleOverrides: {
      root: ({ ownerState: { textStrokeWidth } }) => ({
        WebkitTextStrokeWidth: isNaN(textStrokeWidth as number)
          ? textStrokeWidth
          : `${textStrokeWidth}px`,
      }),
      outlineText: ({ ownerState: { color = 'secondary' }, theme }) => {
        const textColor = {
          textPrimary: theme.palette.text.primary,
          textSecondary: theme.palette.text.secondary,
          textDisabled: theme.palette.text.disabled,
        }

        let muiColor = textColor[color as keyof typeof textColor]

        if (muiColor === undefined) {
          muiColor =
            (theme.palette[color as keyof typeof theme.palette] as PaletteColor)?.main ??
            theme.palette.text.primary
        }

        return { color: 'transparent', WebkitTextStrokeColor: muiColor }
      },
    },
    variants: [{ props: { noWrap: true }, style: { maxWidth: '100%' } }],
    defaultProps: {
      variantMapping: {
        superTitle: 'h1',
        outlineText: 'span',
      },
      classes: {
        superTitle: 'MuiTypography-superTitle',
        outlineText: 'MuiTypography-outlineText',
      },
    },
  },
}
'use client'
import { createTheme } from '@mui/material'
import { esES } from '@mui/material/locale'
import { esES as esESX } from '@mui/x-date-pickers/locales'
import type {} from '@mui/x-date-pickers/themeAugmentation'

import {
  dataDisplayCustomizations,
  inputsCustomizations,
  navigationCustomizations,
  surfacesCustomizations,
} from './Customizations'

export const theme = createTheme(
  {
    components: {
      ...dataDisplayCustomizations,
      ...inputsCustomizations,
      ...navigationCustomizations,
      ...surfacesCustomizations,
    },
    palette: {...},
    shape: { borderRadius: 10 },
    typography: {
      outlineText: {
        font: 'inherit',
        color: 'transparent',
        WebkitTextStrokeWidth: '1.5px',
        WebkitTextStrokeColor: '#2B2D2E',
      },
      // ... other typography
    },
  },
  esES,
  esESX
)

This setup works well, and it correctly applies the style -webkit-text-stroke-width: 3px;, as expected.

Screenshot

TypeScript Typing Issue

To handle the custom property textStrokeWidth in TypeScript, I extended the TypographyOwnProps interface to avoid typing errors:

// src/interfaces/@mui/material/Typography.d.ts
import {
  type TypographyClasses as MuiTypographyClasses,
  type TypographyOwnProps as MuiTypographyOwnProps,
} from '@mui/material/Typography'

export declare module '@mui/material/Typography' {
  interface TypographyPropsVariantOverrides {
    superTitle: true
    outlineText: true
  }

  interface TypographyPropsColorOverrides {
    gray: true
  }

  interface TypographyClasses extends MuiTypographyClasses {
    superTitle: string
    outlineText: string
  }

  interface TypographyOwnProps extends MuiTypographyOwnProps {
    textStrokeWidth?:
      | '-moz-initial'
      | 'inherit'
      | 'initial'
      | 'revert'
      | 'revert-layer'
      | 'unset'
      | (string & {})
      | number
  }
}

This solution resolves the TypeScript error, allowing me to use textStrokeWidth without issues. Everything works fine, and I get the desired visual result without having to create a fully custom typography component.

The Problem: Unwanted Attribute in the DOM

Despite everything working visually, I’ve noticed an issue in the console. React is throwing the following warning:

React does not recognize the `textStrokeWidth` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `textstrokewidth` instead. If you accidentally passed it from a parent component, remove it from the DOM element.

As a result, the DOM is rendering textstrokewidth="3", which shouldn’t happen. Since textStrokeWidth is not a valid HTML attribute, it should be omitted from the DOM. However, it's still being rendered, causing unnecessary clutter and a React warning.

Screenshot

What I’m Looking For

I’m wondering if there is an existing way in Material UI to prevent this attribute from appearing in the DOM, similar to how fontSize is handled. I attempted to trace how fontSize is managed internally by Material UI since it’s excluded from the DOM, but I couldn’t determine the exact mechanism.

Ideally, I’d like to exclude the custom textStrokeWidth property from being rendered in the DOM, without creating a custom typography component. A solution like a hypothetical mappingIgnoreProps attribute would be perfect, where we could list attributes to be ignored during rendering.