From b113fb9359c4ac82700137807db99a1a078bff50 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Tue, 10 Feb 2026 16:55:04 +0000 Subject: [PATCH] Refactor Bg providers Signed-off-by: Charles de Dreuille --- .changeset/clean-bags-occur.md | 37 ++++-- packages/ui/report.api.md | 46 ++++--- .../ui/src/components/Box/Box.stories.tsx | 33 ++--- packages/ui/src/components/Box/definition.ts | 2 +- packages/ui/src/components/Box/types.ts | 4 +- .../ui/src/components/Button/definition.ts | 2 +- .../src/components/ButtonIcon/definition.ts | 2 +- .../src/components/ButtonLink/definition.ts | 2 +- .../ui/src/components/Card/Card.module.css | 20 +-- .../ui/src/components/Card/Card.stories.tsx | 116 ++++++++++-------- packages/ui/src/components/Card/definition.ts | 3 +- packages/ui/src/components/Card/types.ts | 2 - .../ui/src/components/Flex/Flex.stories.tsx | 2 +- packages/ui/src/components/Flex/Flex.tsx | 8 +- packages/ui/src/components/Flex/types.ts | 4 +- .../ui/src/components/Grid/Grid.stories.tsx | 2 +- packages/ui/src/components/Grid/Grid.tsx | 14 +-- packages/ui/src/components/Grid/types.ts | 6 +- packages/ui/src/hooks/useBg.tsx | 112 ++++++----------- packages/ui/src/hooks/useDefinition/types.ts | 39 ++++-- .../src/hooks/useDefinition/useDefinition.tsx | 44 +++++-- packages/ui/src/index.ts | 8 +- packages/ui/src/types.ts | 10 ++ 23 files changed, 260 insertions(+), 258 deletions(-) diff --git a/.changeset/clean-bags-occur.md b/.changeset/clean-bags-occur.md index 6bc5339675..d0b675a25a 100644 --- a/.changeset/clean-bags-occur.md +++ b/.changeset/clean-bags-occur.md @@ -2,15 +2,32 @@ '@backstage/ui': minor --- -**BREAKING**: Replaced `Surface` / `onSurface` system with new `ContainerBg` background system +**BREAKING**: Replaced `Surface` / `onSurface` system with new provider/consumer background system -The old `Surface` type (`'0'`–`'3'`, `'auto'`) and its associated props (`surface`, `onSurface`) have been replaced by `ContainerBg` — a union of `'neutral-1'` | `'neutral-2'` | `'neutral-3'` | `'danger'` | `'warning'` | `'success'`. There is no `neutral-4` value; containers are capped at `neutral-3`. Leaf components like Button no longer accept a `bg` prop — they inherit the parent container's `bg` via a `data-on-bg` attribute, and CSS handles the visual step-up to the next neutral level. +The old `Surface` type (`'0'`–`'3'`, `'auto'`) and its associated props (`surface`, `onSurface`) have been replaced by a provider/consumer bg architecture. -New `useBg` hook and `BgProvider` replace the deleted `useSurface` hook and `SurfaceProvider`. +**Types:** + +- `ContainerBg` — `'neutral-1'` | `'neutral-2'` | `'neutral-3'` | `'danger'` | `'warning'` | `'success'` +- `ProviderBg` — `ContainerBg | 'neutral-auto'` + +There is no `neutral-4` prop value; containers are capped at `neutral-3`. Consumer components (e.g. Button) inherit the parent's bg via `data-on-bg`, and CSS handles the visual step-up. + +**Hooks:** + +- `useBgProvider(bg?)` — for provider components. Returns `{ bg: undefined }` when no bg is given (transparent). Supports `'neutral-auto'` to auto-increment from the parent context. +- `useBgConsumer()` — for consumer components. Returns the parent container's bg unchanged. +- The old `useBg` hook and `UseBgOptions` interface have been removed. + +**Component roles:** + +- **Provider-only** (Box, Flex, Grid): set `data-bg`, wrap children in `BgProvider`. Transparent by default. +- **Consumer-only** (Button, ButtonIcon, ButtonLink): set `data-on-bg`, inherit from parent. +- **Provider + Consumer** (Card): sets both `data-bg` and `data-on-bg`, wraps children. Defaults to `neutral-auto`. **Migration:** -Rename the `surface` prop to `bg` on container components and update values: +Rename the `surface` prop to `bg` on provider components and update values: ```diff - @@ -26,7 +43,7 @@ Rename the `surface` prop to `bg` on container components and update values: + ``` -Remove `onSurface` from leaf components — they now always inherit from the parent container and can no longer override the value: +Remove `onSurface` from consumer components — they now always inherit from the parent container: ```diff - - - - - - - + + + + + + - - -
Without bg — no context, no auto-increment
- - - - - - - - - -
- +
+
), }); diff --git a/packages/ui/src/components/Box/definition.ts b/packages/ui/src/components/Box/definition.ts index 9d9c3b9417..7c1150c2dc 100644 --- a/packages/ui/src/components/Box/definition.ts +++ b/packages/ui/src/components/Box/definition.ts @@ -27,7 +27,7 @@ export const BoxDefinition = defineComponent()({ classNames: { root: 'bui-Box', }, - bg: 'container', + bg: { provider: true }, propDefs: { as: { default: 'div' }, bg: { dataAttribute: true }, diff --git a/packages/ui/src/components/Box/types.ts b/packages/ui/src/components/Box/types.ts index dabd56fc48..f7641d2f27 100644 --- a/packages/ui/src/components/Box/types.ts +++ b/packages/ui/src/components/Box/types.ts @@ -15,12 +15,12 @@ */ import type { ReactNode, CSSProperties } from 'react'; -import type { Responsive, ContainerBg, SpaceProps } from '../../types'; +import type { Responsive, ProviderBg, SpaceProps } from '../../types'; /** @public */ export type BoxOwnProps = { as?: keyof JSX.IntrinsicElements; - bg?: Responsive; + bg?: Responsive; children?: ReactNode; className?: string; style?: CSSProperties; diff --git a/packages/ui/src/components/Button/definition.ts b/packages/ui/src/components/Button/definition.ts index 6b6b49ab53..3007064a48 100644 --- a/packages/ui/src/components/Button/definition.ts +++ b/packages/ui/src/components/Button/definition.ts @@ -29,7 +29,7 @@ export const ButtonDefinition = defineComponent()({ content: 'bui-ButtonContent', spinner: 'bui-ButtonSpinner', }, - bg: 'leaf', + bg: { consumer: true }, propDefs: { size: { dataAttribute: true, default: 'small' }, variant: { dataAttribute: true, default: 'primary' }, diff --git a/packages/ui/src/components/ButtonIcon/definition.ts b/packages/ui/src/components/ButtonIcon/definition.ts index 03c2bd9387..3750a7f3d9 100644 --- a/packages/ui/src/components/ButtonIcon/definition.ts +++ b/packages/ui/src/components/ButtonIcon/definition.ts @@ -29,7 +29,7 @@ export const ButtonIconDefinition = defineComponent()({ content: 'bui-ButtonIconContent', spinner: 'bui-ButtonIconSpinner', }, - bg: 'leaf', + bg: { consumer: true }, propDefs: { size: { dataAttribute: true, default: 'small' }, variant: { dataAttribute: true, default: 'primary' }, diff --git a/packages/ui/src/components/ButtonLink/definition.ts b/packages/ui/src/components/ButtonLink/definition.ts index 1e3727c60e..d528b90917 100644 --- a/packages/ui/src/components/ButtonLink/definition.ts +++ b/packages/ui/src/components/ButtonLink/definition.ts @@ -28,7 +28,7 @@ export const ButtonLinkDefinition = defineComponent()({ root: 'bui-ButtonLink', content: 'bui-ButtonLinkContent', }, - bg: 'leaf', + bg: { consumer: true }, propDefs: { size: { dataAttribute: true, default: 'small' }, variant: { dataAttribute: true, default: 'primary' }, diff --git a/packages/ui/src/components/Card/Card.module.css b/packages/ui/src/components/Card/Card.module.css index 1ca0b7a1b4..5450c1feb4 100644 --- a/packages/ui/src/components/Card/Card.module.css +++ b/packages/ui/src/components/Card/Card.module.css @@ -30,28 +30,16 @@ width: 100%; } - .bui-Card[data-bg='neutral-1'] { - background-color: var(--bui-bg-neutral-1); - } - - .bui-Card[data-bg='neutral-2'] { + .bui-Card[data-on-bg='neutral-1'] { background-color: var(--bui-bg-neutral-2); } - .bui-Card[data-bg='neutral-3'] { + .bui-Card[data-on-bg='neutral-2'] { background-color: var(--bui-bg-neutral-3); } - .bui-Card[data-bg='danger'] { - background-color: var(--bui-bg-danger); - } - - .bui-Card[data-bg='warning'] { - background-color: var(--bui-bg-warning); - } - - .bui-Card[data-bg='success'] { - background-color: var(--bui-bg-success); + .bui-Card[data-on-bg='neutral-3'] { + background-color: var(--bui-bg-neutral-4); } .bui-CardBody { diff --git a/packages/ui/src/components/Card/Card.stories.tsx b/packages/ui/src/components/Card/Card.stories.tsx index 80dd0ad8d6..3957b7beff 100644 --- a/packages/ui/src/components/Card/Card.stories.tsx +++ b/packages/ui/src/components/Card/Card.stories.tsx @@ -132,41 +132,27 @@ export const Backgrounds = meta.story({ render: args => ( - Default - No bg prop - - - Neutral 1 - Explicit neutral-1 - - - Neutral 2 - Explicit neutral-2 - - - Neutral 3 - Explicit neutral-3 - - - Responsive - Neutral 1 → 2 - - - Danger - Bg danger - - - Warning - Bg warning - - - Success - Bg success + No parent + Defaults to neutral-1 + + + On neutral-1 + Auto-increments to neutral-2 + + + + + On neutral-2 + Auto-increments to neutral-3 + + + + + On neutral-3 + Steps up to neutral-4 + + ), }); @@ -175,22 +161,25 @@ export const BgNested = meta.story({ render: args => ( - In this test, we are nesting cards to ensure that the correct background - is applied to each element. Buttons automatically inherit the bg context - and increment their neutral level. + Nested cards auto-increment their neutral level. Buttons inherit the + parent card's bg via data-on-bg. - - Neutral 1 + + Card (visual: neutral-1, provides: neutral-1) - - - Neutral 2 + + + + Card (visual: neutral-2, provides: neutral-2) + - + - Auto-incremented + + Card (visual: neutral-4, provides: neutral-3) + - + @@ -201,27 +190,56 @@ export const BgNested = meta.story({ ), }); -export const BgAutoIncrement = meta.story({ +export const BgOnProviders = meta.story({ render: args => ( + + No provider + Card defaults to neutral-1 + On neutral-1 - Card auto → neutral-2 + Card auto-increments to neutral-2 On neutral-2 - Card auto → neutral-3 + Card auto-increments to neutral-3 On neutral-3 - Card auto → neutral-4 + Card visually at neutral-4 ), }); + +export const CustomCardWithBox = meta.story({ + render: () => ( + + + A custom card built with Box. Use Box with an explicit bg prop to create + a card-like container that participates in the bg system as a provider. + + + + + + Header + Body + Footer + + + ), +}); diff --git a/packages/ui/src/components/Card/definition.ts b/packages/ui/src/components/Card/definition.ts index 73dc1b02af..0fc56b482b 100644 --- a/packages/ui/src/components/Card/definition.ts +++ b/packages/ui/src/components/Card/definition.ts @@ -32,9 +32,8 @@ export const CardDefinition = defineComponent()({ classNames: { root: 'bui-Card', }, - bg: 'container', + bg: { provider: true, consumer: true, defaultBg: 'neutral-auto' }, propDefs: { - bg: { dataAttribute: true }, children: {}, className: {}, }, diff --git a/packages/ui/src/components/Card/types.ts b/packages/ui/src/components/Card/types.ts index 2b32bbf50a..e31b2cfda2 100644 --- a/packages/ui/src/components/Card/types.ts +++ b/packages/ui/src/components/Card/types.ts @@ -15,11 +15,9 @@ */ import type { ReactNode } from 'react'; -import type { Responsive, ContainerBg } from '../../types'; /** @public */ export type CardOwnProps = { - bg?: Responsive; children?: ReactNode; className?: string; }; diff --git a/packages/ui/src/components/Flex/Flex.stories.tsx b/packages/ui/src/components/Flex/Flex.stories.tsx index 3ea311da0e..434655880c 100644 --- a/packages/ui/src/components/Flex/Flex.stories.tsx +++ b/packages/ui/src/components/Flex/Flex.stories.tsx @@ -277,7 +277,7 @@ export const Backgrounds = meta.story({ ), }); -export const BgAutoIncrement = meta.story({ +export const BgNeutralAuto = meta.story({ args: { px: '6', py: '4', gap: '4' }, render: args => ( diff --git a/packages/ui/src/components/Flex/Flex.tsx b/packages/ui/src/components/Flex/Flex.tsx index c4b8f9744c..a798a88012 100644 --- a/packages/ui/src/components/Flex/Flex.tsx +++ b/packages/ui/src/components/Flex/Flex.tsx @@ -20,15 +20,11 @@ import clsx from 'clsx'; import { useStyles } from '../../hooks/useStyles'; import { FlexDefinition } from './definition'; import styles from './Flex.module.css'; -import { BgProvider, useBg } from '../../hooks/useBg'; +import { BgProvider, useBgProvider } from '../../hooks/useBg'; /** @public */ export const Flex = forwardRef((props, ref) => { - // Only establish bg context when an explicit bg prop is provided. - // Flex is a layout primitive — it should be transparent to the bg system by default. - const { bg: resolvedBg } = useBg( - props.bg !== undefined ? { mode: 'container', bg: props.bg } : undefined, - ); + const { bg: resolvedBg } = useBgProvider(props.bg); const { classNames, dataAttributes, utilityClasses, style, cleanedProps } = useStyles(FlexDefinition, { diff --git a/packages/ui/src/components/Flex/types.ts b/packages/ui/src/components/Flex/types.ts index ad5404e41d..1f04a4818d 100644 --- a/packages/ui/src/components/Flex/types.ts +++ b/packages/ui/src/components/Flex/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Responsive, Space, SpaceProps, ContainerBg } from '../../types'; +import type { Responsive, Space, SpaceProps, ProviderBg } from '../../types'; /** @public */ export interface FlexProps extends SpaceProps { @@ -25,5 +25,5 @@ export interface FlexProps extends SpaceProps { direction?: Responsive<'row' | 'column' | 'row-reverse' | 'column-reverse'>; className?: string; style?: React.CSSProperties; - bg?: Responsive; + bg?: Responsive; } diff --git a/packages/ui/src/components/Grid/Grid.stories.tsx b/packages/ui/src/components/Grid/Grid.stories.tsx index 187a97dbbd..d5b83d257d 100644 --- a/packages/ui/src/components/Grid/Grid.stories.tsx +++ b/packages/ui/src/components/Grid/Grid.stories.tsx @@ -177,7 +177,7 @@ export const Backgrounds = meta.story({ ), }); -export const BgAutoIncrement = meta.story({ +export const BgNeutralAuto = meta.story({ args: { px: '6', py: '4', columns: '2', gap: '4' }, render: args => ( diff --git a/packages/ui/src/components/Grid/Grid.tsx b/packages/ui/src/components/Grid/Grid.tsx index 3e3ad7f300..d6e4cc2437 100644 --- a/packages/ui/src/components/Grid/Grid.tsx +++ b/packages/ui/src/components/Grid/Grid.tsx @@ -20,14 +20,10 @@ import type { GridItemProps, GridProps } from './types'; import { useStyles } from '../../hooks/useStyles'; import { GridDefinition, GridItemDefinition } from './definition'; import styles from './Grid.module.css'; -import { BgProvider, useBg } from '../../hooks/useBg'; +import { BgProvider, useBgProvider } from '../../hooks/useBg'; const GridRoot = forwardRef((props, ref) => { - // Only establish bg context when an explicit bg prop is provided. - // Grid is a layout primitive — it should be transparent to the bg system by default. - const { bg: resolvedBg } = useBg( - props.bg !== undefined ? { mode: 'container', bg: props.bg } : undefined, - ); + const { bg: resolvedBg } = useBgProvider(props.bg); const { classNames, dataAttributes, utilityClasses, style, cleanedProps } = useStyles(GridDefinition, { @@ -62,11 +58,7 @@ const GridRoot = forwardRef((props, ref) => { }); const GridItem = forwardRef((props, ref) => { - // Only establish bg context when an explicit bg prop is provided. - // GridItem is a layout slot — it should be transparent to the bg system by default. - const { bg: resolvedBg } = useBg( - props.bg !== undefined ? { mode: 'container', bg: props.bg } : undefined, - ); + const { bg: resolvedBg } = useBgProvider(props.bg); const { classNames, dataAttributes, utilityClasses, style, cleanedProps } = useStyles(GridItemDefinition, { diff --git a/packages/ui/src/components/Grid/types.ts b/packages/ui/src/components/Grid/types.ts index 4f279f24cb..4b33c657f3 100644 --- a/packages/ui/src/components/Grid/types.ts +++ b/packages/ui/src/components/Grid/types.ts @@ -19,7 +19,7 @@ import type { SpaceProps, Responsive, Columns, - ContainerBg, + ProviderBg, } from '../../types'; /** @public */ @@ -29,7 +29,7 @@ export interface GridProps extends SpaceProps { columns?: Responsive; gap?: Responsive; style?: React.CSSProperties; - bg?: Responsive; + bg?: Responsive; } /** @public */ @@ -41,5 +41,5 @@ export interface GridItemProps { colStart?: Responsive; rowSpan?: Responsive; style?: React.CSSProperties; - bg?: Responsive; + bg?: Responsive; } diff --git a/packages/ui/src/hooks/useBg.tsx b/packages/ui/src/hooks/useBg.tsx index 3cec51302b..0b999ee097 100644 --- a/packages/ui/src/hooks/useBg.tsx +++ b/packages/ui/src/hooks/useBg.tsx @@ -19,7 +19,7 @@ import { createVersionedContext, createVersionedValueMap, } from '@backstage/version-bridge'; -import { ContainerBg, Responsive } from '../types'; +import { ContainerBg, ProviderBg, Responsive } from '../types'; import { useBreakpoint } from './useBreakpoint'; import { resolveResponsiveValue } from './useDefinition/helpers'; @@ -34,26 +34,6 @@ export interface BgProviderProps { children: ReactNode; } -/** @public */ -export interface UseBgOptions { - /** - * The bg mode of the component. - * - * - `'container'` — for components like Box, Card, Flex that establish bg context. - * If `bg` prop is provided, uses that value. Otherwise auto-increments from parent, - * capping at `neutral-3`. - * - `'leaf'` — for components like Button that consume bg context. - * Returns the parent context bg unchanged (no increment). The leaf component's CSS - * handles the visual step-up. The `bg` prop is ignored. - */ - mode: 'container' | 'leaf'; - /** - * The explicit bg value from the component's prop. - * Only used in container mode — leaf mode ignores this. - */ - bg?: Responsive; -} - const BgContext = createVersionedContext<{ 1: BgContextValue; }>('bg-context'); @@ -62,15 +42,13 @@ const BgContext = createVersionedContext<{ * Increments a neutral bg level by one, capping at 'neutral-3'. * Intent backgrounds (danger, warning, success) pass through unchanged. * - * The 'neutral-4' level is reserved for leaf component CSS and is never - * set on containers. + * The 'neutral-4' level is reserved for consumer component CSS and is + * never set on providers. * * @internal */ -function incrementNeutralBg( - bg: ContainerBg | undefined, -): ContainerBg | undefined { - if (!bg) return undefined; +function incrementNeutralBg(bg: ContainerBg | undefined): ContainerBg { + if (!bg) return 'neutral-1'; if (bg === 'neutral-1') return 'neutral-2'; if (bg === 'neutral-2') return 'neutral-3'; if (bg === 'neutral-3') return 'neutral-3'; // capped at neutral-3 @@ -78,32 +56,6 @@ function incrementNeutralBg( return bg; } -/** - * Resolves the bg for a container component. - * - * Uses the explicit `bg` prop if provided. Otherwise auto-increments from - * the parent context, capping at `neutral-3`. Returns undefined when there - * is no prop and no parent context. - * - * @internal - */ -function resolveContainerBg( - context: BgContextValue, - propBg: ContainerBg | undefined, -): BgContextValue { - // Explicit bg prop takes priority - if (propBg !== undefined) { - return { bg: propBg }; - } - - // No explicit bg: auto-increment from context if available - if (context.bg === undefined) { - return { bg: undefined }; - } - - return { bg: incrementNeutralBg(context.bg) }; -} - /** * Provider component that establishes the bg context for child components. * @@ -118,36 +70,44 @@ export const BgProvider = ({ bg, children }: BgProviderProps) => { }; /** - * Hook to access and resolve the current bg context. + * Hook for consumer components (e.g. Button) to read the parent bg context. * - * - **Container mode** — uses explicit `bg` if provided, otherwise auto-increments - * from parent context. Caps at `neutral-3`. - * - **Leaf mode** — returns the parent context bg unchanged. No prop needed. - * - **No options** — returns the raw context value without resolution. + * Returns the parent container's bg unchanged. The consumer component's CSS + * handles the visual step-up (e.g. on a neutral-1 surface, the consumer + * uses neutral-2 tokens via `data-on-bg`). * - * @param options - Configuration for bg resolution * @public */ -export const useBg = (options?: UseBgOptions): BgContextValue => { - const { breakpoint } = useBreakpoint(); +export function useBgConsumer(): BgContextValue { const value = useContext(BgContext)?.atVersion(1); - const context = value ?? { bg: undefined }; + return value ?? { bg: undefined }; +} - if (!options) { - return context; +/** + * Hook for provider components (e.g. Box, Card) to resolve and provide bg context. + * + * - `bg` is `undefined` -- transparent, no context change, returns `{ bg: undefined }` + * - `bg` is a `ContainerBg` value -- uses that value directly + * - `bg` is `'neutral-auto'` -- increments from the parent context, capping at `neutral-3` + * + * The caller is responsible for wrapping children with `BgProvider` when the + * resolved bg is defined. + * + * @public + */ +export function useBgProvider(bg?: Responsive): BgContextValue { + const { breakpoint } = useBreakpoint(); + const context = useBgConsumer(); + + if (bg === undefined) { + return { bg: undefined }; } - // Leaf mode: return the parent context bg unchanged. - // The leaf component's CSS handles the visual step-up. - if (options.mode === 'leaf') { - return context; + const resolved = resolveResponsiveValue(bg, breakpoint); + + if (resolved === 'neutral-auto') { + return { bg: incrementNeutralBg(context.bg) }; } - // Resolve responsive prop value to a scalar for the current breakpoint - const propBg = - options.bg !== undefined - ? resolveResponsiveValue(options.bg, breakpoint) - : undefined; - - return resolveContainerBg(context, propBg); -}; + return { bg: resolved }; +} diff --git a/packages/ui/src/hooks/useDefinition/types.ts b/packages/ui/src/hooks/useDefinition/types.ts index d28b7e8552..925172d160 100644 --- a/packages/ui/src/hooks/useDefinition/types.ts +++ b/packages/ui/src/hooks/useDefinition/types.ts @@ -27,6 +27,21 @@ export interface PropDefConfig { export type UtilityPropKey = keyof typeof utilityClassMap; +/** + * Configuration for how a component participates in the bg system. + * + * - `provider` — calls `useBgProvider`, sets `data-bg`, wraps children in `BgProvider` + * - `consumer` — calls `useBgConsumer`, sets `data-on-bg` + * - `defaultBg` — default bg value when no `bg` prop is provided (e.g. `'neutral-auto'`) + * + * A component can be provider-only, consumer-only, or both. + */ +export interface BgConfig { + provider?: boolean; + consumer?: boolean; + defaultBg?: string; +} + export interface ComponentConfig< P extends Record, S extends Record, @@ -36,19 +51,22 @@ export interface ComponentConfig< propDefs: { [K in keyof P]: PropDefConfig }; // readonly for compatibility with const inference from factory utilityProps?: readonly UtilityPropKey[]; - bg?: 'container' | 'leaf'; + bg?: BgConfig; } /** * Type constraint that validates bg props are present in the props type. - * - If bg is 'container', P must include 'bg' - * - If bg is 'leaf', no prop constraint (fully automatic) + * - Provider-only components must include 'bg' in their props + * - Provider+consumer components (e.g. Card) don't need a bg prop (they auto-increment) + * - Consumer-only components don't need a bg prop */ -export type BgPropsConstraint = BgMode extends 'container' - ? 'bg' extends keyof P +export type BgPropsConstraint = Bg extends { provider: true } + ? Bg extends { consumer: true } + ? {} // provider+consumer: bg prop is optional (auto-increment via defaultBg) + : 'bg' extends keyof P ? {} : { - __error: 'Bg container components must include bg in props type.'; + __error: 'Bg provider components must include bg in props type.'; } : {}; @@ -75,10 +93,11 @@ type ResolvedOwnProps< [K in keyof PropDefs & keyof P]: ResolvePropType; }; -type ChildrenProps = - BgMode extends 'container' - ? { bgChildren: ReactNode; children?: never } - : { children: ReactNode; bgChildren?: never }; +type ChildrenProps = Bg extends { + provider: true; +} + ? { bgChildren: ReactNode; children?: never } + : { children: ReactNode; bgChildren?: never }; type DataAttributeKeys = { [K in keyof PropDefs]: PropDefs[K] extends { dataAttribute: true } diff --git a/packages/ui/src/hooks/useDefinition/useDefinition.tsx b/packages/ui/src/hooks/useDefinition/useDefinition.tsx index e7a4caca97..15817a754c 100644 --- a/packages/ui/src/hooks/useDefinition/useDefinition.tsx +++ b/packages/ui/src/hooks/useDefinition/useDefinition.tsx @@ -17,7 +17,7 @@ import { ReactNode } from 'react'; import clsx from 'clsx'; import { useBreakpoint } from '../useBreakpoint'; -import { useBg, BgProvider } from '../useBg'; +import { useBgProvider, useBgConsumer, BgProvider } from '../useBg'; import { resolveResponsiveValue, processUtilityProps } from './helpers'; import type { ComponentConfig, @@ -36,9 +36,17 @@ export function useDefinition< ): UseDefinitionResult { const { breakpoint } = useBreakpoint(); - const { bg: resolvedBg } = useBg( - definition.bg ? { mode: definition.bg, bg: props.bg } : undefined, - ); + // Resolve the effective bg value: use the bg prop if provided, + // otherwise fall back to the defaultBg from the bg config + const effectiveBg = definition.bg?.provider + ? props.bg ?? definition.bg.defaultBg + : undefined; + + // Provider: resolve bg and provide context for children + const providerBg = useBgProvider(effectiveBg); + + // Consumer: read parent context bg + const consumerBg = useBgConsumer(); const ownPropKeys = new Set(Object.keys(definition.propDefs)); const utilityPropKeys = new Set(definition.utilityProps ?? []); @@ -65,6 +73,9 @@ export function useDefinition< if (finalValue !== undefined) { ownPropsResolved[key] = finalValue; + // Skip data-bg for bg prop when the provider path handles it + if (key === 'bg' && definition.bg?.provider) continue; + if ((config as any).dataAttribute) { // eslint-disable-next-line no-restricted-syntax dataAttributes[`data-${key.toLowerCase()}`] = String(finalValue); @@ -72,11 +83,18 @@ export function useDefinition< } } - // Set the bg data attribute from the resolved bg value - // Containers use data-bg, leaf components use data-on-bg - if (definition.bg && resolvedBg !== undefined) { - const attrName = definition.bg === 'leaf' ? 'data-on-bg' : 'data-bg'; - dataAttributes[attrName] = String(resolvedBg); + // Provider-only: set data-bg (provider+consumer components use data-on-bg instead) + if ( + definition.bg?.provider && + !definition.bg?.consumer && + providerBg.bg !== undefined + ) { + dataAttributes['data-bg'] = String(providerBg.bg); + } + + // Consumer: set data-on-bg from the parent context + if (definition.bg?.consumer && consumerBg.bg !== undefined) { + dataAttributes['data-on-bg'] = String(consumerBg.bg); } const { utilityClasses, utilityStyle } = processUtilityProps>( @@ -101,9 +119,9 @@ export function useDefinition< let children: ReactNode | undefined; let bgChildren: ReactNode | undefined; - if (definition.bg === 'container') { - bgChildren = resolvedBg ? ( - {props.children} + if (definition.bg?.provider) { + bgChildren = providerBg.bg ? ( + {props.children} ) : ( props.children ); @@ -115,7 +133,7 @@ export function useDefinition< ownProps: { classes, ...ownPropsResolved, - ...(definition.bg === 'container' ? { bgChildren } : { children }), + ...(definition.bg?.provider ? { bgChildren } : { children }), }, restProps, dataAttributes, diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index dc920900f3..c9f2a3763c 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -64,9 +64,5 @@ export * from './types'; // Hooks export { useBreakpoint } from './hooks/useBreakpoint'; -export { useBg, BgProvider } from './hooks/useBg'; -export type { - BgContextValue, - BgProviderProps, - UseBgOptions, -} from './hooks/useBg'; +export { useBgProvider, useBgConsumer, BgProvider } from './hooks/useBg'; +export type { BgContextValue, BgProviderProps } from './hooks/useBg'; diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index 4b0b783656..d3dfd996aa 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -199,3 +199,13 @@ export type ContainerBg = | 'danger' | 'warning' | 'success'; + +/** + * Background values accepted by provider components. + * + * Includes all `ContainerBg` values plus `'neutral-auto'` which + * automatically increments the neutral level from the parent context. + * + * @public + */ +export type ProviderBg = ContainerBg | 'neutral-auto';