diff --git a/packages/mui-material/test/typescript/moduleAugmentation/dataAttributesOverrides.spec.tsx b/packages/mui-material/test/typescript/moduleAugmentation/dataAttributesOverrides.spec.tsx new file mode 100644 index 00000000000000..672e36619551d3 --- /dev/null +++ b/packages/mui-material/test/typescript/moduleAugmentation/dataAttributesOverrides.spec.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import Backdrop from '@mui/material/Backdrop'; + +// Loose opt-in: accept any `data-*` key on every MUI slot prop, mirroring the +// primitive DOM elements. A single augmentation of the shared +// `DataAttributesOverrides` interface flows through `SlotComponentProps` / +// `SlotComponentPropsWithSlotState` in `@mui/utils/types` (and `SlotProps` in +// `@mui/material`) to every slot of every component wired through these helpers. +// +// See `dataAttributesOverridesAllowList.spec.tsx` for the narrow counterpart +// (a closed allow-list with autocomplete). The two variants live in separate +// files because module augmentation is global per compilation unit. +declare module '@mui/utils/types' { + interface DataAttributesOverrides { + [k: `data-${string}`]: string | number | boolean | undefined; + } +} + +// Object form: arbitrary `data-*` keys are assignable. +; + +// Callback form: the same widening applies to the function branch. + ({ + 'data-testid': 'backdrop-root', + 'data-custom': 'forwarded', + }), + }} +/>; diff --git a/packages/mui-material/test/typescript/moduleAugmentation/dataAttributesOverrides.tsconfig.json b/packages/mui-material/test/typescript/moduleAugmentation/dataAttributesOverrides.tsconfig.json new file mode 100644 index 00000000000000..03626e49c20291 --- /dev/null +++ b/packages/mui-material/test/typescript/moduleAugmentation/dataAttributesOverrides.tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../../../tsconfig.json", + "files": ["dataAttributesOverrides.spec.tsx"] +} diff --git a/packages/mui-material/test/typescript/moduleAugmentation/dataAttributesOverridesAllowList.spec.tsx b/packages/mui-material/test/typescript/moduleAugmentation/dataAttributesOverridesAllowList.spec.tsx new file mode 100644 index 00000000000000..a3a8bdb89f4fb8 --- /dev/null +++ b/packages/mui-material/test/typescript/moduleAugmentation/dataAttributesOverridesAllowList.spec.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import Backdrop from '@mui/material/Backdrop'; + +// Strict opt-in: only the explicitly declared keys become assignable, giving +// teams a closed allow-list with autocomplete and typo-checking for those keys. +// An undeclared `data-*` key still errors (asserted below). +// +// This is the narrow counterpart to the loose index-signature variant in +// `dataAttributesOverrides.spec.tsx`. The two must live in separate compilation +// units: module augmentation is global, so the loose `[k: `data-${string}`]` +// signature would otherwise merge in here and make every `data-*` key pass. +declare module '@mui/utils/types' { + interface DataAttributesOverrides { + 'data-testid'?: string; + } +} + +; diff --git a/packages/mui-material/test/typescript/moduleAugmentation/dataAttributesOverridesAllowList.tsconfig.json b/packages/mui-material/test/typescript/moduleAugmentation/dataAttributesOverridesAllowList.tsconfig.json new file mode 100644 index 00000000000000..2c54a45365898f --- /dev/null +++ b/packages/mui-material/test/typescript/moduleAugmentation/dataAttributesOverridesAllowList.tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../../../tsconfig.json", + "files": ["dataAttributesOverridesAllowList.spec.tsx"] +} diff --git a/packages/mui-utils/src/types/DataAttributes.ts b/packages/mui-utils/src/types/DataAttributes.ts new file mode 100644 index 00000000000000..0645c33c856073 --- /dev/null +++ b/packages/mui-utils/src/types/DataAttributes.ts @@ -0,0 +1,55 @@ +/** + * Opt-in interface for typing `data-*` attributes on MUI slot props. + * + * Slot prop types reject arbitrary `data-*` keys by design, so this interface is + * empty by default. Augment it to declare the keys you want, and they become + * assignable on every slot of every component that routes its slot props through + * `@mui/utils/types`. + * + * Choose the form that matches how strict you want to be: + * + * - Loose index signature: accepts any `data-*` key, like the underlying DOM + * element. Simplest to set up, but you get no autocomplete and typos slip through. + * - Explicit keys: lists each `data-*` key you allow. More to declare up front, + * in return for autocomplete and typo-checking on those keys. + * + * @example + * // Loose: accept any `data-*` key. + * declare module '@mui/utils/types' { + * interface DataAttributesOverrides { + * [k: `data-${string}`]: string | number | boolean | undefined; + * } + * } + * + * @example + * // Explicit: only the keys you declare, with autocomplete. + * declare module '@mui/utils/types' { + * interface DataAttributesOverrides { + * 'data-testid'?: string; + * } + * } + */ +export interface DataAttributesOverrides {} + +/** + * Surface contributed to slot prop types by the `DataAttributesOverrides` + * augmentation. Empty by default; populated only when a consumer declares + * `data-*` keys via module augmentation. This is what `WithDataAttributes` + * intersects into the widened branch of every slot prop union exposed by + * `@mui/utils/types`. + */ +export type DataAttributes = DataAttributesOverrides; + +/** + * Widens a slot-props type so that, when a consumer augments + * `DataAttributesOverrides`, the augmented keys become assignable to the + * widened branch. The default `DataAttributes` is empty, so this widening is + * a no-op until a consumer opts in. + * + * Implemented as a union between the original type and the intersected widened + * form — `T | (T & DataAttributes)` — so that pre-typed values remain + * assignable to the original branch without having to declare a `data-*` + * index signature themselves, while object literals can pick up the widened + * branch and include the augmented keys. + */ +export type WithDataAttributes = T | (T & DataAttributes); diff --git a/packages/mui-utils/src/types/index.ts b/packages/mui-utils/src/types/index.ts index 4d2a96ce8f99e1..d5c9327b1e80ab 100644 --- a/packages/mui-utils/src/types/index.ts +++ b/packages/mui-utils/src/types/index.ts @@ -1,4 +1,7 @@ import * as React from 'react'; +import { WithDataAttributes } from './DataAttributes'; + +export * from './DataAttributes'; export type EventHandlers = Record>; @@ -9,10 +12,10 @@ export type WithOptionalOwnerState = Omit Partial>; export type SlotComponentProps = - | (Partial> & TOverrides) + | WithDataAttributes> & TOverrides> | (( ownerState: TOwnerState, - ) => Partial> & TOverrides); + ) => WithDataAttributes> & TOverrides>); export type SlotComponentPropsWithSlotState< TSlotComponent extends React.ElementType, @@ -20,8 +23,8 @@ export type SlotComponentPropsWithSlotState< TOwnerState, TSlotState, > = - | (Partial> & TOverrides) + | WithDataAttributes> & TOverrides> | (( ownerState: TOwnerState, slotState: TSlotState, - ) => Partial> & TOverrides); + ) => WithDataAttributes> & TOverrides>);