From 58fb55548ba10de1482073d120d3fb258fd25b70 Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 20 May 2026 15:39:06 +0300 Subject: [PATCH] [utils] Add opt-in DataAttributesOverrides augmentation for slot props MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Today, passing `data-testid` (or any `data-*` attribute) through `slotProps` on a MUI component is a TypeScript error even though the attribute is forwarded to the DOM at runtime. This adds a single, opt-in switch that lets consumers declare exactly which `data-*` keys they want typed, at whichever level of strictness they choose. - `DataAttributesOverrides`: module-augmentable empty interface in `@mui/utils/types`. The single sanctioned switch. - `DataAttributes = DataAttributesOverrides`: dormant by default; populated only when a consumer augments. - `WithDataAttributes = T | (T & DataAttributes)`: union form so the original `T` stays assignable as-is (preserves backwards compatibility with `x as CustomProps` style casts on slot values), while augmented keys flow through the widened branch when consumers opt in. - `SlotComponentProps` and `SlotComponentPropsWithSlotState` now wrap their object and callback branches with `WithDataAttributes`. With an empty default the wrapping is a no-op until a consumer augments — once they do, every Material component that reaches slot props through these helpers (or through `SlotProps` in `@mui/material`) picks up the new keys automatically. Consumers opt in with a single `declare module '@mui/utils/types' { ... }` block. Examples are documented in the new `DataAttributes.ts` file and exercised by a module-augmentation test using Backdrop's root slot. This is the canonical place for the helper because every MUI slot prop type ultimately flows through `@mui/utils/types`; downstream packages (`@mui/x-*` and friends) get the augmentation transitively without having to mirror the helper themselves. --- .../dataAttributesOverrides.spec.tsx | 23 +++++++++ .../dataAttributesOverrides.tsconfig.json | 4 ++ .../mui-utils/src/types/DataAttributes.ts | 47 +++++++++++++++++++ packages/mui-utils/src/types/index.ts | 11 +++-- 4 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 packages/mui-material/test/typescript/moduleAugmentation/dataAttributesOverrides.spec.tsx create mode 100644 packages/mui-material/test/typescript/moduleAugmentation/dataAttributesOverrides.tsconfig.json create mode 100644 packages/mui-utils/src/types/DataAttributes.ts 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..20e53ab6dd4354 --- /dev/null +++ b/packages/mui-material/test/typescript/moduleAugmentation/dataAttributesOverrides.spec.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import Backdrop from '@mui/material/Backdrop'; + +// Augment the shared `DataAttributesOverrides` interface to opt in to typed +// support for `data-testid` on every MUI slot prop. The augmentation flows +// through `SlotComponentProps` / `SlotComponentPropsWithSlotState` in +// `@mui/utils/types`, and via `SlotProps` in `@mui/material`, to every slot +// of every Material component that wires slot props through these helpers. +declare module '@mui/utils/types' { + interface DataAttributesOverrides { + 'data-testid'?: string; + } +} + +// After augmentation: `data-testid` is assignable on any Material slot prop. +; 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-utils/src/types/DataAttributes.ts b/packages/mui-utils/src/types/DataAttributes.ts new file mode 100644 index 00000000000000..16d247f3b6b2da --- /dev/null +++ b/packages/mui-utils/src/types/DataAttributes.ts @@ -0,0 +1,47 @@ +/** + * Module-augmentable interface that lets consumers opt in to typed support for + * `data-*` (and any other) attributes on MUI slot props. Empty by default — + * by design, MUI slot prop types do not include arbitrary `data-*` keys; the + * augmentation is the single switch consumers can flip to choose their level + * of strictness. + * + * Examples: + * + * // Strongly-typed: only `data-testid` becomes assignable on slots. + * declare module '@mui/utils/types' { + * interface DataAttributesOverrides { + * 'data-testid'?: string; + * } + * } + * + * // Loose: accept any `data-*` key on slots. + * declare module '@mui/utils/types' { + * interface DataAttributesOverrides { + * [k: `data-${string}`]: string | number | boolean | undefined; + * } + * } + */ +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>);