From 0f462f85fe39cfb6c052b63ba7996f12f92ab03c Mon Sep 17 00:00:00 2001 From: Johan Persson Date: Mon, 23 Feb 2026 17:06:32 +0100 Subject: [PATCH] refactor(ui): enforce required children on bg provider component types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strengthens BgPropsConstraint to also check that children is present and non-optional in the OwnProps type of bg provider components. This ensures children can only flow through childrenWithBgProvider, preventing silent bypasses of the bg context system. Box and Accordion OwnProps updated to comply — children is now required. BoxProps updated to Omit children from React.HTMLAttributes to avoid an incompatible duplicate property declaration. Storybook stories updated accordingly. Signed-off-by: Johan Persson --- .changeset/rude-groups-shout.md | 5 +++++ packages/ui/report.api.md | 6 +++--- packages/ui/src/components/Accordion/types.ts | 2 +- packages/ui/src/components/Box/Box.stories.tsx | 6 ++++-- packages/ui/src/components/Box/types.ts | 4 ++-- .../src/components/Container/Container.stories.tsx | 1 + packages/ui/src/components/Flex/Flex.stories.tsx | 1 + packages/ui/src/components/Grid/Grid.stories.tsx | 2 ++ packages/ui/src/hooks/useDefinition/types.ts | 14 +++++++++++--- 9 files changed, 30 insertions(+), 11 deletions(-) create mode 100644 .changeset/rude-groups-shout.md diff --git a/.changeset/rude-groups-shout.md b/.changeset/rude-groups-shout.md new file mode 100644 index 0000000000..59e4a22aa9 --- /dev/null +++ b/.changeset/rude-groups-shout.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Improved type safety in `useDefinition` by centralizing prop resolution and strengthening the `BgPropsConstraint` to require that `bg` provider components declare `children` as a required prop in their OwnProps type. diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 87cea3d387..5922155e93 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -118,7 +118,7 @@ export interface AccordionGroupProps // @public export type AccordionOwnProps = { bg?: ProviderBg; - children?: ReactNode; + children: ReactNode; className?: string; }; @@ -360,7 +360,7 @@ export const BoxDefinition: { export type BoxOwnProps = { as?: keyof JSX.IntrinsicElements; bg?: Responsive; - children?: ReactNode; + children: ReactNode; className?: string; style?: CSSProperties; }; @@ -370,7 +370,7 @@ export interface BoxProps extends SpaceProps, BoxOwnProps, BoxUtilityProps, - React.HTMLAttributes {} + Omit, 'children'> {} // @public (undocumented) export type BoxUtilityProps = { diff --git a/packages/ui/src/components/Accordion/types.ts b/packages/ui/src/components/Accordion/types.ts index 8895b79086..39ce1ab5be 100644 --- a/packages/ui/src/components/Accordion/types.ts +++ b/packages/ui/src/components/Accordion/types.ts @@ -29,7 +29,7 @@ import type { ProviderBg } from '../../types'; */ export type AccordionOwnProps = { bg?: ProviderBg; - children?: ReactNode; + children: ReactNode; className?: string; }; diff --git a/packages/ui/src/components/Box/Box.stories.tsx b/packages/ui/src/components/Box/Box.stories.tsx index f5bc8e5b23..5d60d99872 100644 --- a/packages/ui/src/components/Box/Box.stories.tsx +++ b/packages/ui/src/components/Box/Box.stories.tsx @@ -60,6 +60,7 @@ export const Default = meta.story({ fontWeight: 'bold', color: '#2563eb', }, + children: null, }, }); @@ -326,6 +327,7 @@ const CardDisplay = ({ children }: { children?: ReactNode }) => { }; export const Display = meta.story({ + args: { children: null }, render: args => ( @@ -347,7 +349,7 @@ export const Display = meta.story({ }); export const BackgroundColors = meta.story({ - args: { px: '6', py: '4' }, + args: { px: '6', py: '4', children: null }, render: args => ( Default @@ -377,7 +379,7 @@ export const BackgroundColors = meta.story({ }); export const NestedNeutralColors = meta.story({ - args: { px: '6', py: '4' }, + args: { px: '6', py: '4', children: null }, render: args => ( diff --git a/packages/ui/src/components/Box/types.ts b/packages/ui/src/components/Box/types.ts index f7641d2f27..1b4259099b 100644 --- a/packages/ui/src/components/Box/types.ts +++ b/packages/ui/src/components/Box/types.ts @@ -21,7 +21,7 @@ import type { Responsive, ProviderBg, SpaceProps } from '../../types'; export type BoxOwnProps = { as?: keyof JSX.IntrinsicElements; bg?: Responsive; - children?: ReactNode; + children: ReactNode; className?: string; style?: CSSProperties; }; @@ -45,4 +45,4 @@ export interface BoxProps extends SpaceProps, BoxOwnProps, BoxUtilityProps, - React.HTMLAttributes {} + Omit, 'children'> {} diff --git a/packages/ui/src/components/Container/Container.stories.tsx b/packages/ui/src/components/Container/Container.stories.tsx index 44ff6f2d17..e6fbee091b 100644 --- a/packages/ui/src/components/Container/Container.stories.tsx +++ b/packages/ui/src/components/Container/Container.stories.tsx @@ -43,6 +43,7 @@ const DecorativeBox = () => ( backgroundImage: 'url("data:image/svg+xml,%3Csvg%20width%3D%226%22%20height%3D%226%22%20viewBox%3D%220%200%206%206%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cg%20fill%3D%22%232563eb%22%20fill-opacity%3D%220.3%22%20fill-rule%3D%22evenodd%22%3E%3Cpath%20d%3D%22M5%200h1L0%206V5zM6%205v1H5z%22/%3E%3C/g%3E%3C/svg%3E")', }} + children={null} /> ); diff --git a/packages/ui/src/components/Flex/Flex.stories.tsx b/packages/ui/src/components/Flex/Flex.stories.tsx index 8b3b7af220..ff2aa442ea 100644 --- a/packages/ui/src/components/Flex/Flex.stories.tsx +++ b/packages/ui/src/components/Flex/Flex.stories.tsx @@ -70,6 +70,7 @@ const DecorativeBox = ({ fontWeight: 'bold', color: '#2563eb', }} + children={null} /> ); }; diff --git a/packages/ui/src/components/Grid/Grid.stories.tsx b/packages/ui/src/components/Grid/Grid.stories.tsx index d5b83d257d..b3b6957359 100644 --- a/packages/ui/src/components/Grid/Grid.stories.tsx +++ b/packages/ui/src/components/Grid/Grid.stories.tsx @@ -36,6 +36,7 @@ const FakeBox = () => ( backgroundImage: 'url("data:image/svg+xml,%3Csvg%20width%3D%226%22%20height%3D%226%22%20viewBox%3D%220%200%206%206%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cg%20fill%3D%22%232563eb%22%20fill-opacity%3D%220.3%22%20fill-rule%3D%22evenodd%22%3E%3Cpath%20d%3D%22M5%200h1L0%206V5zM6%205v1H5z%22/%3E%3C/g%3E%3C/svg%3E")', }} + children={null} /> ); @@ -94,6 +95,7 @@ export const RowAndColumns = meta.story({ backgroundImage: 'url("data:image/svg+xml,%3Csvg%20width%3D%226%22%20height%3D%226%22%20viewBox%3D%220%200%206%206%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Cg%20fill%3D%22%232563eb%22%20fill-opacity%3D%220.3%22%20fill-rule%3D%22evenodd%22%3E%3Cpath%20d%3D%22M5%200h1L0%206V5zM6%205v1H5z%22/%3E%3C/g%3E%3C/svg%3E")', }} + children={null} /> diff --git a/packages/ui/src/hooks/useDefinition/types.ts b/packages/ui/src/hooks/useDefinition/types.ts index 45ce5e3dc8..2ddc0c88c5 100644 --- a/packages/ui/src/hooks/useDefinition/types.ts +++ b/packages/ui/src/hooks/useDefinition/types.ts @@ -47,14 +47,22 @@ export interface ComponentConfig< /** * Type constraint that validates bg props are present in the props type. - * - Provider components must include 'bg' in their props + * - Provider components must include 'bg' in their props and 'children' in propDefs * - Consumer components don't need a bg prop */ export type BgPropsConstraint = Bg extends 'provider' ? 'bg' extends keyof P - ? {} + ? 'children' extends keyof P + ? {} extends Pick + ? { + __error: 'Bg provider components cannot have children as optional.'; + } + : {} + : { + __error: 'Bg provider components must include children in own props type.'; + } : { - __error: 'Bg provider components must include bg in props type.'; + __error: 'Bg provider components must include bg in own props type.'; } : {};