Merge pull request #33948 from backstage/flex-item-props
ui: add support for flex-item props to generic layout components
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/ui': minor
|
||||
---
|
||||
|
||||
Add support for flex item props (`grow`, `shrink`, and `basis`) to `Box`, `Card`, `Grid`, and `Flex` itself.
|
||||
|
||||
**Affected components:** Box, Card, Grid, Flex
|
||||
@@ -431,6 +431,9 @@ export const BoxDefinition: {
|
||||
'py',
|
||||
'position',
|
||||
'display',
|
||||
'grow',
|
||||
'shrink',
|
||||
'basis',
|
||||
'width',
|
||||
'minWidth',
|
||||
'maxWidth',
|
||||
@@ -452,6 +455,7 @@ export type BoxOwnProps = {
|
||||
// @public (undocumented)
|
||||
export interface BoxProps
|
||||
extends SpaceProps,
|
||||
FlexItemProps,
|
||||
BoxOwnProps,
|
||||
BoxUtilityProps,
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {}
|
||||
@@ -642,6 +646,7 @@ export const Card: ForwardRefExoticComponent<
|
||||
export type CardBaseProps = {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -702,7 +707,9 @@ export const CardDefinition: {
|
||||
readonly target: {};
|
||||
readonly rel: {};
|
||||
readonly download: {};
|
||||
readonly style: {};
|
||||
};
|
||||
readonly utilityProps: readonly ['grow', 'shrink', 'basis'];
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -786,10 +793,12 @@ export type CardOwnProps = Pick<
|
||||
| 'target'
|
||||
| 'rel'
|
||||
| 'download'
|
||||
| 'style'
|
||||
>;
|
||||
|
||||
// @public
|
||||
export type CardProps = CardBaseProps &
|
||||
FlexItemProps &
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'onClick'> &
|
||||
(CardButtonVariant | CardLinkVariant | CardStaticVariant);
|
||||
|
||||
@@ -1315,12 +1324,22 @@ export const FlexDefinition: {
|
||||
'align',
|
||||
'justify',
|
||||
'direction',
|
||||
'grow',
|
||||
'shrink',
|
||||
'basis',
|
||||
];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type FlexDirection = 'row' | 'column';
|
||||
|
||||
// @public
|
||||
export interface FlexItemProps {
|
||||
basis?: Responsive<CSSProperties['flexBasis']>;
|
||||
grow?: Responsive<number | boolean>;
|
||||
shrink?: Responsive<number | boolean>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type FlexOwnProps = {
|
||||
children: ReactNode;
|
||||
@@ -1332,6 +1351,7 @@ export type FlexOwnProps = {
|
||||
// @public (undocumented)
|
||||
export interface FlexProps
|
||||
extends SpaceProps,
|
||||
FlexItemProps,
|
||||
FlexOwnProps,
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
|
||||
// (undocumented)
|
||||
@@ -1422,6 +1442,9 @@ export const GridDefinition: {
|
||||
'pt',
|
||||
'px',
|
||||
'py',
|
||||
'grow',
|
||||
'shrink',
|
||||
'basis',
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1478,6 +1501,7 @@ export type GridOwnProps = {
|
||||
// @public (undocumented)
|
||||
export interface GridProps
|
||||
extends SpaceProps,
|
||||
FlexItemProps,
|
||||
GridOwnProps,
|
||||
Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
||||
// (undocumented)
|
||||
|
||||
@@ -36,7 +36,7 @@ export const Box = forwardRef<HTMLDivElement, BoxProps>((props, ref) => {
|
||||
{
|
||||
ref,
|
||||
className: classes.root,
|
||||
style: { ...ownProps.style, ...utilityStyle },
|
||||
style: { ...utilityStyle, ...ownProps.style },
|
||||
...dataAttributes,
|
||||
...restProps,
|
||||
},
|
||||
|
||||
@@ -52,6 +52,9 @@ export const BoxDefinition = defineComponent<BoxOwnProps>()({
|
||||
'py',
|
||||
'position',
|
||||
'display',
|
||||
'grow',
|
||||
'shrink',
|
||||
'basis',
|
||||
'width',
|
||||
'minWidth',
|
||||
'maxWidth',
|
||||
|
||||
@@ -15,7 +15,12 @@
|
||||
*/
|
||||
|
||||
import type { ReactNode, CSSProperties } from 'react';
|
||||
import type { Responsive, ProviderBg, SpaceProps } from '../../types';
|
||||
import type {
|
||||
Responsive,
|
||||
ProviderBg,
|
||||
SpaceProps,
|
||||
FlexItemProps,
|
||||
} from '../../types';
|
||||
|
||||
/** @public */
|
||||
export type BoxOwnProps = {
|
||||
@@ -43,6 +48,7 @@ export type BoxUtilityProps = {
|
||||
/** @public */
|
||||
export interface BoxProps
|
||||
extends SpaceProps,
|
||||
FlexItemProps,
|
||||
BoxOwnProps,
|
||||
BoxUtilityProps,
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {}
|
||||
|
||||
@@ -41,7 +41,7 @@ const INTERACTIVE_ELEMENT_SELECTOR =
|
||||
* @public
|
||||
*/
|
||||
export const Card = forwardRef<HTMLDivElement, CardProps>((props, ref) => {
|
||||
const { ownProps, restProps, dataAttributes } = useDefinition(
|
||||
const { ownProps, restProps, dataAttributes, utilityStyle } = useDefinition(
|
||||
CardDefinition,
|
||||
props,
|
||||
);
|
||||
@@ -98,6 +98,7 @@ export const Card = forwardRef<HTMLDivElement, CardProps>((props, ref) => {
|
||||
{...dataAttributes}
|
||||
{...restProps}
|
||||
onClick={isInteractive ? handleClick : undefined}
|
||||
style={{ ...utilityStyle, ...ownProps.style }}
|
||||
>
|
||||
{href && (
|
||||
<Link
|
||||
|
||||
@@ -42,7 +42,9 @@ export const CardDefinition = defineComponent<CardOwnProps>()({
|
||||
target: {},
|
||||
rel: {},
|
||||
download: {},
|
||||
style: {},
|
||||
},
|
||||
utilityProps: ['grow', 'shrink', 'basis'],
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,11 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ReactNode, CSSProperties } from 'react';
|
||||
import type { ButtonProps as RAButtonProps } from 'react-aria-components';
|
||||
import type { FlexItemProps } from '../../types';
|
||||
|
||||
/** @public */
|
||||
export type CardBaseProps = { children?: ReactNode; className?: string };
|
||||
export type CardBaseProps = {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type CardButtonVariant = {
|
||||
@@ -63,6 +68,7 @@ export type CardStaticVariant = {
|
||||
* @public
|
||||
*/
|
||||
export type CardProps = CardBaseProps &
|
||||
FlexItemProps &
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'onClick'> &
|
||||
(CardButtonVariant | CardLinkVariant | CardStaticVariant);
|
||||
|
||||
@@ -81,6 +87,7 @@ export type CardOwnProps = Pick<
|
||||
| 'target'
|
||||
| 'rel'
|
||||
| 'download'
|
||||
| 'style'
|
||||
>;
|
||||
|
||||
/** @public */
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
import preview from '../../../../../.storybook/preview';
|
||||
import { Flex } from './Flex';
|
||||
import { Text } from '../Text';
|
||||
import { Box } from '../Box';
|
||||
import { Box, BoxProps } from '../Box';
|
||||
import { Card, CardHeader, CardBody, CardFooter } from '../Card';
|
||||
import { Grid } from '../Grid';
|
||||
|
||||
const meta = preview.meta({
|
||||
title: 'Backstage UI/Flex',
|
||||
@@ -41,10 +43,9 @@ const meta = preview.meta({
|
||||
const DecorativeBox = ({
|
||||
width = '48px',
|
||||
height = '48px',
|
||||
}: {
|
||||
width?: string;
|
||||
height?: string;
|
||||
}) => {
|
||||
style,
|
||||
...props
|
||||
}: Omit<BoxProps, 'children'>) => {
|
||||
const diagonalStripePattern = (() => {
|
||||
const svg = `
|
||||
<svg width="6" height="6" viewBox="0 0 6 6" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -58,9 +59,11 @@ const DecorativeBox = ({
|
||||
|
||||
return (
|
||||
<Box
|
||||
{...props}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{
|
||||
...style,
|
||||
background: '#eaf2fd',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #2563eb',
|
||||
@@ -199,6 +202,122 @@ export const ResponsiveAlign = meta.story({
|
||||
),
|
||||
});
|
||||
|
||||
export const FlexItems = meta.story({
|
||||
args: {
|
||||
component: 'Box',
|
||||
grow: 1,
|
||||
shrink: 0,
|
||||
basis: 'auto',
|
||||
},
|
||||
argTypes: {
|
||||
component: {
|
||||
control: { type: 'select' },
|
||||
options: ['Box', 'Card', 'Grid', 'Flex'],
|
||||
mapping: {
|
||||
Box: props => <DecorativeBox height="100%" width="256px" {...props} />,
|
||||
Card: props => (
|
||||
<Card style={{ height: '100%', width: '256px' }} {...props}>
|
||||
<CardHeader>
|
||||
<Text>Header</Text>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Text>
|
||||
This is the first paragraph of a long body text that
|
||||
demonstrates how the Card component handles extensive content.
|
||||
The card should adjust accordingly to display all the text
|
||||
properly while maintaining its structure.
|
||||
</Text>
|
||||
<Text>
|
||||
Here's a second paragraph that adds more content to our card
|
||||
body. Having multiple paragraphs helps to visualize how spacing
|
||||
works within the card component.
|
||||
</Text>
|
||||
<Text>
|
||||
This third paragraph continues to add more text to ensure we
|
||||
have a proper demonstration of a card with significant content.
|
||||
This makes it easier to test scrolling behavior and overall
|
||||
layout when content exceeds the initial view.
|
||||
</Text>
|
||||
</CardBody>
|
||||
<CardFooter>
|
||||
<Text>Footer</Text>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
),
|
||||
Grid: props => (
|
||||
<Grid.Root
|
||||
{...props}
|
||||
height="128px"
|
||||
style={{ width: '256px' }}
|
||||
columns="3"
|
||||
>
|
||||
<Grid.Item colSpan="1" rowSpan="2">
|
||||
<DecorativeBox height="100%" width="100%" />
|
||||
</Grid.Item>
|
||||
<Grid.Item colSpan="2">
|
||||
<DecorativeBox height="100%" width="100%" />
|
||||
</Grid.Item>
|
||||
<Grid.Item colSpan="2">
|
||||
<DecorativeBox height="100%" width="100%" />
|
||||
</Grid.Item>
|
||||
</Grid.Root>
|
||||
),
|
||||
Flex: props => (
|
||||
<Flex
|
||||
{...props}
|
||||
height="128px"
|
||||
style={{ width: '256px' }}
|
||||
justify="between"
|
||||
>
|
||||
<DecorativeBox height="100%" />
|
||||
<DecorativeBox height="100%" />
|
||||
<DecorativeBox height="100%" />
|
||||
</Flex>
|
||||
),
|
||||
},
|
||||
},
|
||||
grow: {
|
||||
control: 'radio',
|
||||
options: [undefined, 0, 1, false, true],
|
||||
},
|
||||
shrink: {
|
||||
control: 'radio',
|
||||
options: [undefined, 0, 1, false, true],
|
||||
},
|
||||
basis: {
|
||||
control: 'radio',
|
||||
options: [undefined, '0%', '25%', '50%', '100%', 100, '250px', 'auto'],
|
||||
},
|
||||
},
|
||||
render: ({ component: Component, ...args }) => {
|
||||
return (
|
||||
<Flex style={{ width: '100%', height: '256px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '256px',
|
||||
flex: '1 1 auto',
|
||||
background:
|
||||
'repeating-linear-gradient(-45deg, transparent 0px, transparent 5px, #e8e8e8 5px, #e8e8e8 10px)',
|
||||
borderRadius: '12px',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Component {...args} />
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: '256px',
|
||||
flex: '1 1 auto',
|
||||
background:
|
||||
'repeating-linear-gradient(-45deg, transparent 0px, transparent 5px, #e8e8e8 5px, #e8e8e8 10px)',
|
||||
borderRadius: '12px',
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const ResponsiveGap = meta.story({
|
||||
args: {
|
||||
gap: { xs: '4', md: '8', lg: '12' },
|
||||
|
||||
@@ -53,5 +53,8 @@ export const FlexDefinition = defineComponent<FlexOwnProps>()({
|
||||
'align',
|
||||
'justify',
|
||||
'direction',
|
||||
'grow',
|
||||
'shrink',
|
||||
'basis',
|
||||
],
|
||||
});
|
||||
|
||||
@@ -15,7 +15,13 @@
|
||||
*/
|
||||
|
||||
import type { ReactNode, CSSProperties } from 'react';
|
||||
import type { Responsive, Space, SpaceProps, ProviderBg } from '../../types';
|
||||
import type {
|
||||
Responsive,
|
||||
Space,
|
||||
SpaceProps,
|
||||
ProviderBg,
|
||||
FlexItemProps,
|
||||
} from '../../types';
|
||||
|
||||
/** @public */
|
||||
export type FlexOwnProps = {
|
||||
@@ -28,6 +34,7 @@ export type FlexOwnProps = {
|
||||
/** @public */
|
||||
export interface FlexProps
|
||||
extends SpaceProps,
|
||||
FlexItemProps,
|
||||
FlexOwnProps,
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
|
||||
gap?: Responsive<Space>;
|
||||
|
||||
@@ -51,6 +51,9 @@ export const GridDefinition = defineComponent<GridOwnProps>()({
|
||||
'pt',
|
||||
'px',
|
||||
'py',
|
||||
'grow',
|
||||
'shrink',
|
||||
'basis',
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
Responsive,
|
||||
Columns,
|
||||
ProviderBg,
|
||||
FlexItemProps,
|
||||
} from '../../types';
|
||||
|
||||
/** @public */
|
||||
@@ -34,6 +35,7 @@ export type GridOwnProps = {
|
||||
/** @public */
|
||||
export interface GridProps
|
||||
extends SpaceProps,
|
||||
FlexItemProps,
|
||||
GridOwnProps,
|
||||
Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
||||
columns?: Responsive<Columns>;
|
||||
|
||||
@@ -67,6 +67,18 @@
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.bui-grow {
|
||||
flex-grow: var(--grow);
|
||||
}
|
||||
|
||||
.bui-shrink {
|
||||
flex-shrink: var(--shrink);
|
||||
}
|
||||
|
||||
.bui-basis {
|
||||
flex-basis: var(--basis);
|
||||
}
|
||||
|
||||
/* Breakpoint xs */
|
||||
@media (min-width: 640px) {
|
||||
.xs\:bui-align-start {
|
||||
@@ -116,6 +128,18 @@
|
||||
.xs\:bui-fd-column-reverse {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.xs\:bui-grow {
|
||||
flex-grow: var(--grow-xs);
|
||||
}
|
||||
|
||||
.xs\:bui-shrink {
|
||||
flex-shrink: var(--shrink-xs);
|
||||
}
|
||||
|
||||
.xs\:bui-basis {
|
||||
flex-basis: var(--basis-xs);
|
||||
}
|
||||
}
|
||||
|
||||
/* Breakpoint sm */
|
||||
@@ -167,6 +191,18 @@
|
||||
.sm\:bui-fd-column-reverse {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.sm\:bui-grow {
|
||||
flex-grow: var(--grow-sm);
|
||||
}
|
||||
|
||||
.sm\:bui-shrink {
|
||||
flex-shrink: var(--shrink-sm);
|
||||
}
|
||||
|
||||
.sm\:bui-basis {
|
||||
flex-basis: var(--basis-sm);
|
||||
}
|
||||
}
|
||||
|
||||
/* Breakpoint md */
|
||||
@@ -218,6 +254,18 @@
|
||||
.md\:bui-fd-column-reverse {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.md\:bui-grow {
|
||||
flex-grow: var(--grow-md);
|
||||
}
|
||||
|
||||
.md\:bui-shrink {
|
||||
flex-shrink: var(--shrink-md);
|
||||
}
|
||||
|
||||
.md\:bui-basis {
|
||||
flex-basis: var(--basis-md);
|
||||
}
|
||||
}
|
||||
|
||||
/* Breakpoint lg */
|
||||
@@ -269,6 +317,18 @@
|
||||
.lg\:bui-fd-column-reverse {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.lg\:bui-grow {
|
||||
flex-grow: var(--grow-lg);
|
||||
}
|
||||
|
||||
.lg\:bui-shrink {
|
||||
flex-shrink: var(--shrink-lg);
|
||||
}
|
||||
|
||||
.lg\:bui-basis {
|
||||
flex-basis: var(--basis-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Breakpoint xl */
|
||||
@@ -320,5 +380,17 @@
|
||||
.xl\:bui-fd-column-reverse {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.xl\:bui-grow {
|
||||
flex-grow: var(--grow-xl);
|
||||
}
|
||||
|
||||
.xl\:bui-shrink {
|
||||
flex-shrink: var(--shrink-xl);
|
||||
}
|
||||
|
||||
.xl\:bui-basis {
|
||||
flex-basis: var(--basis-xl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ export function processUtilityProps<Keys extends string>(
|
||||
|
||||
const handleUtilityValue = (
|
||||
key: string,
|
||||
val: unknown,
|
||||
inputVal: unknown,
|
||||
prefix: string = '',
|
||||
) => {
|
||||
// Get utility class configuration for this key
|
||||
@@ -113,6 +113,11 @@ export function processUtilityProps<Keys extends string>(
|
||||
return;
|
||||
}
|
||||
|
||||
const val =
|
||||
'transform' in utilityConfig
|
||||
? utilityConfig.transform(inputVal)
|
||||
: inputVal;
|
||||
|
||||
// Check if value is in the list of valid values for this utility
|
||||
const values = utilityConfig.values as readonly (string | number)[];
|
||||
if (values.length > 0 && values.includes(val as string | number)) {
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
/** @public */
|
||||
export type Breakpoint = 'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
|
||||
@@ -120,6 +122,20 @@ export interface PaddingProps {
|
||||
/** @public */
|
||||
export interface SpaceProps extends MarginProps, PaddingProps {}
|
||||
|
||||
/**
|
||||
* Flex item properties.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface FlexItemProps {
|
||||
/** Controls the flex-grow property. Values of `true` or `false` are converted to `1` or `0` respectively. */
|
||||
grow?: Responsive<number | boolean>;
|
||||
/** Controls the flex-shrink property. Values of `true` or `false` are converted to `1` or `0` respectively. */
|
||||
shrink?: Responsive<number | boolean>;
|
||||
/** Controls the flex-basis property. */
|
||||
basis?: Responsive<CSSProperties['flexBasis']>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type TextVariants =
|
||||
| 'title-large'
|
||||
|
||||
@@ -196,7 +196,30 @@ export const utilityClassMap = {
|
||||
class: 'bui-row-span',
|
||||
values: columnsValues,
|
||||
},
|
||||
grow: {
|
||||
class: 'bui-grow',
|
||||
cssVar: '--grow',
|
||||
values: [],
|
||||
transform: input => (typeof input === 'boolean' ? Number(input) : input),
|
||||
},
|
||||
shrink: {
|
||||
class: 'bui-shrink',
|
||||
cssVar: '--shrink',
|
||||
values: [],
|
||||
transform: input => (typeof input === 'boolean' ? Number(input) : input),
|
||||
},
|
||||
basis: {
|
||||
class: 'bui-basis',
|
||||
cssVar: '--basis',
|
||||
values: [],
|
||||
transform: input => (typeof input === 'number' ? `${input}px` : input),
|
||||
},
|
||||
} as const satisfies Record<
|
||||
string,
|
||||
{ class: string; cssVar?: string; values: readonly (string | number)[] }
|
||||
{
|
||||
class: string;
|
||||
cssVar?: string;
|
||||
values: readonly (string | number)[];
|
||||
transform?: (input: unknown) => unknown;
|
||||
}
|
||||
>;
|
||||
|
||||
Reference in New Issue
Block a user