Refactor Bg providers
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
+27
-19
@@ -249,7 +249,9 @@ export const BoxDefinition: {
|
||||
readonly classNames: {
|
||||
readonly root: 'bui-Box';
|
||||
};
|
||||
readonly bg: 'container';
|
||||
readonly bg: {
|
||||
readonly provider: true;
|
||||
};
|
||||
readonly propDefs: {
|
||||
readonly as: {
|
||||
readonly default: 'div';
|
||||
@@ -290,7 +292,7 @@ export const BoxDefinition: {
|
||||
// @public (undocumented)
|
||||
export type BoxOwnProps = {
|
||||
as?: keyof JSX.IntrinsicElements;
|
||||
bg?: Responsive<ContainerBg>;
|
||||
bg?: Responsive<ProviderBg>;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
@@ -335,7 +337,9 @@ export const ButtonDefinition: {
|
||||
readonly content: 'bui-ButtonContent';
|
||||
readonly spinner: 'bui-ButtonSpinner';
|
||||
};
|
||||
readonly bg: 'leaf';
|
||||
readonly bg: {
|
||||
readonly consumer: true;
|
||||
};
|
||||
readonly propDefs: {
|
||||
readonly size: {
|
||||
readonly dataAttribute: true;
|
||||
@@ -374,7 +378,9 @@ export const ButtonIconDefinition: {
|
||||
readonly content: 'bui-ButtonIconContent';
|
||||
readonly spinner: 'bui-ButtonIconSpinner';
|
||||
};
|
||||
readonly bg: 'leaf';
|
||||
readonly bg: {
|
||||
readonly consumer: true;
|
||||
};
|
||||
readonly propDefs: {
|
||||
readonly size: {
|
||||
readonly dataAttribute: true;
|
||||
@@ -422,7 +428,9 @@ export const ButtonLinkDefinition: {
|
||||
readonly root: 'bui-ButtonLink';
|
||||
readonly content: 'bui-ButtonLinkContent';
|
||||
};
|
||||
readonly bg: 'leaf';
|
||||
readonly bg: {
|
||||
readonly consumer: true;
|
||||
};
|
||||
readonly propDefs: {
|
||||
readonly size: {
|
||||
readonly dataAttribute: true;
|
||||
@@ -517,11 +525,12 @@ export const CardDefinition: {
|
||||
readonly classNames: {
|
||||
readonly root: 'bui-Card';
|
||||
};
|
||||
readonly bg: 'container';
|
||||
readonly bg: {
|
||||
readonly provider: true;
|
||||
readonly consumer: true;
|
||||
readonly defaultBg: 'neutral-auto';
|
||||
};
|
||||
readonly propDefs: {
|
||||
readonly bg: {
|
||||
readonly dataAttribute: true;
|
||||
};
|
||||
readonly children: {};
|
||||
readonly className: {};
|
||||
};
|
||||
@@ -589,7 +598,6 @@ export interface CardHeaderProps
|
||||
|
||||
// @public (undocumented)
|
||||
export type CardOwnProps = {
|
||||
bg?: Responsive<ContainerBg>;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
@@ -966,7 +974,7 @@ export interface FlexProps extends SpaceProps {
|
||||
// (undocumented)
|
||||
align?: Responsive<'start' | 'center' | 'end' | 'baseline' | 'stretch'>;
|
||||
// (undocumented)
|
||||
bg?: Responsive<ContainerBg>;
|
||||
bg?: Responsive<ProviderBg>;
|
||||
// (undocumented)
|
||||
children?: React.ReactNode;
|
||||
// (undocumented)
|
||||
@@ -1048,7 +1056,7 @@ export const GridItemDefinition: {
|
||||
// @public (undocumented)
|
||||
export interface GridItemProps {
|
||||
// (undocumented)
|
||||
bg?: Responsive<ContainerBg>;
|
||||
bg?: Responsive<ProviderBg>;
|
||||
// (undocumented)
|
||||
children?: React.ReactNode;
|
||||
// (undocumented)
|
||||
@@ -1068,7 +1076,7 @@ export interface GridItemProps {
|
||||
// @public (undocumented)
|
||||
export interface GridProps extends SpaceProps {
|
||||
// (undocumented)
|
||||
bg?: Responsive<ContainerBg>;
|
||||
bg?: Responsive<ProviderBg>;
|
||||
// (undocumented)
|
||||
children?: React.ReactNode;
|
||||
// (undocumented)
|
||||
@@ -1521,6 +1529,9 @@ export interface PopoverProps extends Omit<PopoverProps_2, 'children'> {
|
||||
hideArrow?: boolean;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type ProviderBg = ContainerBg | 'neutral-auto';
|
||||
|
||||
// @public (undocumented)
|
||||
export interface QueryOptions<TFilter> {
|
||||
// (undocumented)
|
||||
@@ -2174,13 +2185,10 @@ export const TooltipTrigger: (
|
||||
) => JSX_2.Element;
|
||||
|
||||
// @public
|
||||
export const useBg: (options?: UseBgOptions) => BgContextValue;
|
||||
export function useBgConsumer(): BgContextValue;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface UseBgOptions {
|
||||
bg?: Responsive<ContainerBg>;
|
||||
mode: 'container' | 'leaf';
|
||||
}
|
||||
// @public
|
||||
export function useBgProvider(bg?: Responsive<ProviderBg>): BgContextValue;
|
||||
|
||||
// @public (undocumented)
|
||||
export const useBreakpoint: () => {
|
||||
|
||||
@@ -379,31 +379,14 @@ export const BackgroundColors = meta.story({
|
||||
export const NestedNeutralColors = meta.story({
|
||||
args: { px: '6', py: '4' },
|
||||
render: args => (
|
||||
<Flex direction="column" gap="6">
|
||||
<Flex direction="column" gap="2">
|
||||
<div>With bg="neutral-1" — nested boxes and buttons auto-increment</div>
|
||||
<Box {...args} bg="neutral-1">
|
||||
<Button variant="secondary">Button (neutral-2)</Button>
|
||||
<Box {...args} mt="4">
|
||||
<Button variant="secondary">Button (neutral-3)</Button>
|
||||
<Box {...args} mt="4">
|
||||
<Button variant="secondary">Button (neutral-4)</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box {...args} bg="neutral-1">
|
||||
<Button variant="secondary">Button (on neutral-1)</Button>
|
||||
<Box {...args} bg="neutral-2" mt="4">
|
||||
<Button variant="secondary">Button (on neutral-2)</Button>
|
||||
<Box {...args} bg="neutral-3" mt="4">
|
||||
<Button variant="secondary">Button (on neutral-3)</Button>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="2">
|
||||
<div>Without bg — no context, no auto-increment</div>
|
||||
<Box {...args}>
|
||||
<Button variant="secondary">Button (default)</Button>
|
||||
<Box {...args} mt="4">
|
||||
<Button variant="secondary">Button (default)</Button>
|
||||
<Box {...args} mt="4">
|
||||
<Button variant="secondary">Button (default)</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ export const BoxDefinition = defineComponent<BoxOwnProps>()({
|
||||
classNames: {
|
||||
root: 'bui-Box',
|
||||
},
|
||||
bg: 'container',
|
||||
bg: { provider: true },
|
||||
propDefs: {
|
||||
as: { default: 'div' },
|
||||
bg: { dataAttribute: true },
|
||||
|
||||
@@ -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<ContainerBg>;
|
||||
bg?: Responsive<ProviderBg>;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
|
||||
@@ -29,7 +29,7 @@ export const ButtonDefinition = defineComponent<ButtonOwnProps>()({
|
||||
content: 'bui-ButtonContent',
|
||||
spinner: 'bui-ButtonSpinner',
|
||||
},
|
||||
bg: 'leaf',
|
||||
bg: { consumer: true },
|
||||
propDefs: {
|
||||
size: { dataAttribute: true, default: 'small' },
|
||||
variant: { dataAttribute: true, default: 'primary' },
|
||||
|
||||
@@ -29,7 +29,7 @@ export const ButtonIconDefinition = defineComponent<ButtonIconOwnProps>()({
|
||||
content: 'bui-ButtonIconContent',
|
||||
spinner: 'bui-ButtonIconSpinner',
|
||||
},
|
||||
bg: 'leaf',
|
||||
bg: { consumer: true },
|
||||
propDefs: {
|
||||
size: { dataAttribute: true, default: 'small' },
|
||||
variant: { dataAttribute: true, default: 'primary' },
|
||||
|
||||
@@ -28,7 +28,7 @@ export const ButtonLinkDefinition = defineComponent<ButtonLinkOwnProps>()({
|
||||
root: 'bui-ButtonLink',
|
||||
content: 'bui-ButtonLinkContent',
|
||||
},
|
||||
bg: 'leaf',
|
||||
bg: { consumer: true },
|
||||
propDefs: {
|
||||
size: { dataAttribute: true, default: 'small' },
|
||||
variant: { dataAttribute: true, default: 'primary' },
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -132,41 +132,27 @@ export const Backgrounds = meta.story({
|
||||
render: args => (
|
||||
<Flex align="start" style={{ flexWrap: 'wrap' }} gap="4">
|
||||
<Card {...args} style={{ width: '200px' }}>
|
||||
<CardHeader>Default</CardHeader>
|
||||
<CardBody>No bg prop</CardBody>
|
||||
</Card>
|
||||
<Card {...args} bg="neutral-1" style={{ width: '200px' }}>
|
||||
<CardHeader>Neutral 1</CardHeader>
|
||||
<CardBody>Explicit neutral-1</CardBody>
|
||||
</Card>
|
||||
<Card {...args} bg="neutral-2" style={{ width: '200px' }}>
|
||||
<CardHeader>Neutral 2</CardHeader>
|
||||
<CardBody>Explicit neutral-2</CardBody>
|
||||
</Card>
|
||||
<Card {...args} bg="neutral-3" style={{ width: '200px' }}>
|
||||
<CardHeader>Neutral 3</CardHeader>
|
||||
<CardBody>Explicit neutral-3</CardBody>
|
||||
</Card>
|
||||
<Card
|
||||
{...args}
|
||||
bg={{ initial: 'neutral-1', sm: 'neutral-2' }}
|
||||
style={{ width: '200px' }}
|
||||
>
|
||||
<CardHeader>Responsive</CardHeader>
|
||||
<CardBody>Neutral 1 → 2</CardBody>
|
||||
</Card>
|
||||
<Card {...args} bg="danger" style={{ width: '200px' }}>
|
||||
<CardHeader>Danger</CardHeader>
|
||||
<CardBody>Bg danger</CardBody>
|
||||
</Card>
|
||||
<Card {...args} bg="warning" style={{ width: '200px' }}>
|
||||
<CardHeader>Warning</CardHeader>
|
||||
<CardBody>Bg warning</CardBody>
|
||||
</Card>
|
||||
<Card {...args} bg="success" style={{ width: '200px' }}>
|
||||
<CardHeader>Success</CardHeader>
|
||||
<CardBody>Bg success</CardBody>
|
||||
<CardHeader>No parent</CardHeader>
|
||||
<CardBody>Defaults to neutral-1</CardBody>
|
||||
</Card>
|
||||
<Box bg="neutral-1" p="4" style={{ borderRadius: '8px' }}>
|
||||
<Card {...args} style={{ width: '200px' }}>
|
||||
<CardHeader>On neutral-1</CardHeader>
|
||||
<CardBody>Auto-increments to neutral-2</CardBody>
|
||||
</Card>
|
||||
</Box>
|
||||
<Box bg="neutral-2" p="4" style={{ borderRadius: '8px' }}>
|
||||
<Card {...args} style={{ width: '200px' }}>
|
||||
<CardHeader>On neutral-2</CardHeader>
|
||||
<CardBody>Auto-increments to neutral-3</CardBody>
|
||||
</Card>
|
||||
</Box>
|
||||
<Box bg="neutral-3" p="4" style={{ borderRadius: '8px' }}>
|
||||
<Card {...args} style={{ width: '200px' }}>
|
||||
<CardHeader>On neutral-3</CardHeader>
|
||||
<CardBody>Steps up to neutral-4</CardBody>
|
||||
</Card>
|
||||
</Box>
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
@@ -175,22 +161,25 @@ export const BgNested = meta.story({
|
||||
render: args => (
|
||||
<Flex direction="column">
|
||||
<Box style={{ maxWidth: '600px' }} mb="4">
|
||||
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.
|
||||
</Box>
|
||||
<Card {...args} bg="neutral-1" style={{ width: '500px' }}>
|
||||
<CardHeader>Neutral 1</CardHeader>
|
||||
<Card {...args} style={{ width: '500px' }}>
|
||||
<CardHeader>Card (visual: neutral-1, provides: neutral-1)</CardHeader>
|
||||
<CardBody>
|
||||
<Button variant="secondary">Button</Button>
|
||||
<Card {...args} bg="neutral-2" style={{ marginTop: '16px' }}>
|
||||
<CardHeader>Neutral 2</CardHeader>
|
||||
<Button variant="secondary">Button (on neutral-1)</Button>
|
||||
<Card {...args} style={{ marginTop: '16px' }}>
|
||||
<CardHeader>
|
||||
Card (visual: neutral-2, provides: neutral-2)
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Button variant="secondary">Button</Button>
|
||||
<Button variant="secondary">Button (on neutral-2)</Button>
|
||||
<Card {...args} style={{ marginTop: '16px' }}>
|
||||
<CardHeader>Auto-incremented</CardHeader>
|
||||
<CardHeader>
|
||||
Card (visual: neutral-4, provides: neutral-3)
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Button variant="secondary">Button</Button>
|
||||
<Button variant="secondary">Button (on neutral-3)</Button>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</CardBody>
|
||||
@@ -201,27 +190,56 @@ export const BgNested = meta.story({
|
||||
),
|
||||
});
|
||||
|
||||
export const BgAutoIncrement = meta.story({
|
||||
export const BgOnProviders = meta.story({
|
||||
render: args => (
|
||||
<Flex align="start" style={{ flexWrap: 'wrap' }} gap="4">
|
||||
<Card {...args} style={{ width: '200px' }}>
|
||||
<CardHeader>No provider</CardHeader>
|
||||
<CardBody>Card defaults to neutral-1</CardBody>
|
||||
</Card>
|
||||
<Box bg="neutral-1" p="4" style={{ borderRadius: '8px' }}>
|
||||
<Card {...args} style={{ width: '200px' }}>
|
||||
<CardHeader>On neutral-1</CardHeader>
|
||||
<CardBody>Card auto → neutral-2</CardBody>
|
||||
<CardBody>Card auto-increments to neutral-2</CardBody>
|
||||
</Card>
|
||||
</Box>
|
||||
<Box bg="neutral-2" p="4" style={{ borderRadius: '8px' }}>
|
||||
<Card {...args} style={{ width: '200px' }}>
|
||||
<CardHeader>On neutral-2</CardHeader>
|
||||
<CardBody>Card auto → neutral-3</CardBody>
|
||||
<CardBody>Card auto-increments to neutral-3</CardBody>
|
||||
</Card>
|
||||
</Box>
|
||||
<Box bg="neutral-3" p="4" style={{ borderRadius: '8px' }}>
|
||||
<Card {...args} style={{ width: '200px' }}>
|
||||
<CardHeader>On neutral-3</CardHeader>
|
||||
<CardBody>Card auto → neutral-4</CardBody>
|
||||
<CardBody>Card visually at neutral-4</CardBody>
|
||||
</Card>
|
||||
</Box>
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
|
||||
export const CustomCardWithBox = meta.story({
|
||||
render: () => (
|
||||
<Flex direction="column" gap="4">
|
||||
<Box style={{ maxWidth: '600px' }}>
|
||||
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.
|
||||
</Box>
|
||||
<Box
|
||||
bg="neutral-auto"
|
||||
p="4"
|
||||
style={{ borderRadius: '8px', width: '300px' }}
|
||||
>
|
||||
<Button variant="secondary" style={{ marginTop: '8px' }}>
|
||||
Button (on neutral-1)
|
||||
</Button>
|
||||
</Box>
|
||||
<Card style={{ width: '300px' }}>
|
||||
<CardHeader>Header</CardHeader>
|
||||
<CardBody>Body</CardBody>
|
||||
<CardFooter>Footer</CardFooter>
|
||||
</Card>
|
||||
</Flex>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -32,9 +32,8 @@ export const CardDefinition = defineComponent<CardOwnProps>()({
|
||||
classNames: {
|
||||
root: 'bui-Card',
|
||||
},
|
||||
bg: 'container',
|
||||
bg: { provider: true, consumer: true, defaultBg: 'neutral-auto' },
|
||||
propDefs: {
|
||||
bg: { dataAttribute: true },
|
||||
children: {},
|
||||
className: {},
|
||||
},
|
||||
|
||||
@@ -15,11 +15,9 @@
|
||||
*/
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import type { Responsive, ContainerBg } from '../../types';
|
||||
|
||||
/** @public */
|
||||
export type CardOwnProps = {
|
||||
bg?: Responsive<ContainerBg>;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
@@ -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 => (
|
||||
<Flex direction="column">
|
||||
|
||||
@@ -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<HTMLDivElement, FlexProps>((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, {
|
||||
|
||||
@@ -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<ContainerBg>;
|
||||
bg?: Responsive<ProviderBg>;
|
||||
}
|
||||
|
||||
@@ -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 => (
|
||||
<Flex direction="column">
|
||||
|
||||
@@ -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<HTMLDivElement, GridProps>((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<HTMLDivElement, GridProps>((props, ref) => {
|
||||
});
|
||||
|
||||
const GridItem = forwardRef<HTMLDivElement, GridItemProps>((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, {
|
||||
|
||||
@@ -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<Columns>;
|
||||
gap?: Responsive<Space>;
|
||||
style?: React.CSSProperties;
|
||||
bg?: Responsive<ContainerBg>;
|
||||
bg?: Responsive<ProviderBg>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
@@ -41,5 +41,5 @@ export interface GridItemProps {
|
||||
colStart?: Responsive<Columns>;
|
||||
rowSpan?: Responsive<Columns>;
|
||||
style?: React.CSSProperties;
|
||||
bg?: Responsive<ContainerBg>;
|
||||
bg?: Responsive<ProviderBg>;
|
||||
}
|
||||
|
||||
@@ -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<ContainerBg>;
|
||||
}
|
||||
|
||||
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<ProviderBg>): 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 };
|
||||
}
|
||||
|
||||
@@ -27,6 +27,21 @@ export interface PropDefConfig<T> {
|
||||
|
||||
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<string, any>,
|
||||
S extends Record<string, string>,
|
||||
@@ -36,19 +51,22 @@ export interface ComponentConfig<
|
||||
propDefs: { [K in keyof P]: PropDefConfig<P[K]> };
|
||||
// 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<P, BgMode> = BgMode extends 'container'
|
||||
? 'bg' extends keyof P
|
||||
export type BgPropsConstraint<P, Bg> = 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<P[K], PropDefs[K]>;
|
||||
};
|
||||
|
||||
type ChildrenProps<BgMode extends 'container' | 'leaf' | undefined> =
|
||||
BgMode extends 'container'
|
||||
? { bgChildren: ReactNode; children?: never }
|
||||
: { children: ReactNode; bgChildren?: never };
|
||||
type ChildrenProps<Bg extends BgConfig | undefined> = Bg extends {
|
||||
provider: true;
|
||||
}
|
||||
? { bgChildren: ReactNode; children?: never }
|
||||
: { children: ReactNode; bgChildren?: never };
|
||||
|
||||
type DataAttributeKeys<PropDefs> = {
|
||||
[K in keyof PropDefs]: PropDefs[K] extends { dataAttribute: true }
|
||||
|
||||
@@ -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<D, P> {
|
||||
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<UtilityKeys<D>>(
|
||||
@@ -101,9 +119,9 @@ export function useDefinition<
|
||||
let children: ReactNode | undefined;
|
||||
let bgChildren: ReactNode | undefined;
|
||||
|
||||
if (definition.bg === 'container') {
|
||||
bgChildren = resolvedBg ? (
|
||||
<BgProvider bg={resolvedBg}>{props.children}</BgProvider>
|
||||
if (definition.bg?.provider) {
|
||||
bgChildren = providerBg.bg ? (
|
||||
<BgProvider bg={providerBg.bg}>{props.children}</BgProvider>
|
||||
) : (
|
||||
props.children
|
||||
);
|
||||
@@ -115,7 +133,7 @@ export function useDefinition<
|
||||
ownProps: {
|
||||
classes,
|
||||
...ownPropsResolved,
|
||||
...(definition.bg === 'container' ? { bgChildren } : { children }),
|
||||
...(definition.bg?.provider ? { bgChildren } : { children }),
|
||||
},
|
||||
restProps,
|
||||
dataAttributes,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user