refactor(ui): enforce required children on bg provider component types

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 <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2026-02-23 17:06:32 +01:00
parent f2f6e3d47a
commit 0f462f85fe
9 changed files with 30 additions and 11 deletions
+5
View File
@@ -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.
+3 -3
View File
@@ -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<ProviderBg>;
children?: ReactNode;
children: ReactNode;
className?: string;
style?: CSSProperties;
};
@@ -370,7 +370,7 @@ export interface BoxProps
extends SpaceProps,
BoxOwnProps,
BoxUtilityProps,
React.HTMLAttributes<HTMLDivElement> {}
Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {}
// @public (undocumented)
export type BoxUtilityProps = {
@@ -29,7 +29,7 @@ import type { ProviderBg } from '../../types';
*/
export type AccordionOwnProps = {
bg?: ProviderBg;
children?: ReactNode;
children: ReactNode;
className?: string;
};
@@ -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 => (
<Flex direction="column" align="center">
<Flex>
@@ -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 => (
<Flex align="center" style={{ flexWrap: 'wrap' }}>
<Box {...args}>Default</Box>
@@ -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 => (
<Box {...args} bg="neutral-1">
<Button variant="secondary">Button (on neutral-1)</Button>
+2 -2
View File
@@ -21,7 +21,7 @@ import type { Responsive, ProviderBg, SpaceProps } from '../../types';
export type BoxOwnProps = {
as?: keyof JSX.IntrinsicElements;
bg?: Responsive<ProviderBg>;
children?: ReactNode;
children: ReactNode;
className?: string;
style?: CSSProperties;
};
@@ -45,4 +45,4 @@ export interface BoxProps
extends SpaceProps,
BoxOwnProps,
BoxUtilityProps,
React.HTMLAttributes<HTMLDivElement> {}
Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {}
@@ -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}
/>
);
@@ -70,6 +70,7 @@ const DecorativeBox = ({
fontWeight: 'bold',
color: '#2563eb',
}}
children={null}
/>
);
};
@@ -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}
/>
</Grid.Item>
<Grid.Item colSpan="2">
+11 -3
View File
@@ -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<P, Bg> = Bg extends 'provider'
? 'bg' extends keyof P
? {}
? 'children' extends keyof P
? {} extends Pick<P, 'children'>
? {
__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.';
}
: {};