diff --git a/.changeset/funny-areas-rescue.md b/.changeset/funny-areas-rescue.md new file mode 100644 index 0000000000..0b13201f82 --- /dev/null +++ b/.changeset/funny-areas-rescue.md @@ -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 diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 4856387abf..b3cdc5ced4 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -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, '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, '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; + grow?: Responsive; + shrink?: Responsive; +} + // @public (undocumented) export type FlexOwnProps = { children: ReactNode; @@ -1332,6 +1351,7 @@ export type FlexOwnProps = { // @public (undocumented) export interface FlexProps extends SpaceProps, + FlexItemProps, FlexOwnProps, Omit, '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, 'children'> { // (undocumented) diff --git a/packages/ui/src/components/Box/Box.tsx b/packages/ui/src/components/Box/Box.tsx index 146292a344..1a2930dd2b 100644 --- a/packages/ui/src/components/Box/Box.tsx +++ b/packages/ui/src/components/Box/Box.tsx @@ -36,7 +36,7 @@ export const Box = forwardRef((props, ref) => { { ref, className: classes.root, - style: { ...ownProps.style, ...utilityStyle }, + style: { ...utilityStyle, ...ownProps.style }, ...dataAttributes, ...restProps, }, diff --git a/packages/ui/src/components/Box/definition.ts b/packages/ui/src/components/Box/definition.ts index 3837dc8ea4..c3a38326b2 100644 --- a/packages/ui/src/components/Box/definition.ts +++ b/packages/ui/src/components/Box/definition.ts @@ -52,6 +52,9 @@ export const BoxDefinition = defineComponent()({ 'py', 'position', 'display', + 'grow', + 'shrink', + 'basis', 'width', 'minWidth', 'maxWidth', diff --git a/packages/ui/src/components/Box/types.ts b/packages/ui/src/components/Box/types.ts index 1b4259099b..e333690e2e 100644 --- a/packages/ui/src/components/Box/types.ts +++ b/packages/ui/src/components/Box/types.ts @@ -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, 'children'> {} diff --git a/packages/ui/src/components/Card/Card.tsx b/packages/ui/src/components/Card/Card.tsx index aecca09512..498fd5b021 100644 --- a/packages/ui/src/components/Card/Card.tsx +++ b/packages/ui/src/components/Card/Card.tsx @@ -41,7 +41,7 @@ const INTERACTIVE_ELEMENT_SELECTOR = * @public */ export const Card = forwardRef((props, ref) => { - const { ownProps, restProps, dataAttributes } = useDefinition( + const { ownProps, restProps, dataAttributes, utilityStyle } = useDefinition( CardDefinition, props, ); @@ -98,6 +98,7 @@ export const Card = forwardRef((props, ref) => { {...dataAttributes} {...restProps} onClick={isInteractive ? handleClick : undefined} + style={{ ...utilityStyle, ...ownProps.style }} > {href && ( ()({ target: {}, rel: {}, download: {}, + style: {}, }, + utilityProps: ['grow', 'shrink', 'basis'], }); /** diff --git a/packages/ui/src/components/Card/types.ts b/packages/ui/src/components/Card/types.ts index 5809384c16..83daf2de08 100644 --- a/packages/ui/src/components/Card/types.ts +++ b/packages/ui/src/components/Card/types.ts @@ -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, 'onClick'> & (CardButtonVariant | CardLinkVariant | CardStaticVariant); @@ -81,6 +87,7 @@ export type CardOwnProps = Pick< | 'target' | 'rel' | 'download' + | 'style' >; /** @public */ diff --git a/packages/ui/src/components/Flex/Flex.stories.tsx b/packages/ui/src/components/Flex/Flex.stories.tsx index 91d4088004..b392b6e731 100644 --- a/packages/ui/src/components/Flex/Flex.stories.tsx +++ b/packages/ui/src/components/Flex/Flex.stories.tsx @@ -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) => { const diagonalStripePattern = (() => { const svg = ` @@ -58,9 +59,11 @@ const DecorativeBox = ({ return ( , + Card: props => ( + + + Header + + + + 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. + + + 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. + + + 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. + + + + Footer + + + ), + Grid: props => ( + + + + + + + + + + + + ), + Flex: props => ( + + + + + + ), + }, + }, + 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 ( + +
+ + + +
+ + ); + }, +}); + export const ResponsiveGap = meta.story({ args: { gap: { xs: '4', md: '8', lg: '12' }, diff --git a/packages/ui/src/components/Flex/definition.ts b/packages/ui/src/components/Flex/definition.ts index 802464c4e1..1bc9c54217 100644 --- a/packages/ui/src/components/Flex/definition.ts +++ b/packages/ui/src/components/Flex/definition.ts @@ -53,5 +53,8 @@ export const FlexDefinition = defineComponent()({ 'align', 'justify', 'direction', + 'grow', + 'shrink', + 'basis', ], }); diff --git a/packages/ui/src/components/Flex/types.ts b/packages/ui/src/components/Flex/types.ts index 1c7f887e62..5b295e1825 100644 --- a/packages/ui/src/components/Flex/types.ts +++ b/packages/ui/src/components/Flex/types.ts @@ -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, 'children'> { gap?: Responsive; diff --git a/packages/ui/src/components/Grid/definition.ts b/packages/ui/src/components/Grid/definition.ts index 606b286b3c..2d83e9db6a 100644 --- a/packages/ui/src/components/Grid/definition.ts +++ b/packages/ui/src/components/Grid/definition.ts @@ -51,6 +51,9 @@ export const GridDefinition = defineComponent()({ 'pt', 'px', 'py', + 'grow', + 'shrink', + 'basis', ], }); diff --git a/packages/ui/src/components/Grid/types.ts b/packages/ui/src/components/Grid/types.ts index 6ad5d564c6..90699c59d2 100644 --- a/packages/ui/src/components/Grid/types.ts +++ b/packages/ui/src/components/Grid/types.ts @@ -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, 'children'> { columns?: Responsive; diff --git a/packages/ui/src/css/utilities/flex.css b/packages/ui/src/css/utilities/flex.css index 2248addfee..bf5bf392cd 100644 --- a/packages/ui/src/css/utilities/flex.css +++ b/packages/ui/src/css/utilities/flex.css @@ -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); + } } } diff --git a/packages/ui/src/hooks/useDefinition/helpers.ts b/packages/ui/src/hooks/useDefinition/helpers.ts index 8a990b4b6a..2169a26238 100644 --- a/packages/ui/src/hooks/useDefinition/helpers.ts +++ b/packages/ui/src/hooks/useDefinition/helpers.ts @@ -102,7 +102,7 @@ export function processUtilityProps( const handleUtilityValue = ( key: string, - val: unknown, + inputVal: unknown, prefix: string = '', ) => { // Get utility class configuration for this key @@ -113,6 +113,11 @@ export function processUtilityProps( 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)) { diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index 113eb06a5c..3a6633bc37 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -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; + /** Controls the flex-shrink property. Values of `true` or `false` are converted to `1` or `0` respectively. */ + shrink?: Responsive; + /** Controls the flex-basis property. */ + basis?: Responsive; +} + /** @public */ export type TextVariants = | 'title-large' diff --git a/packages/ui/src/utils/utilityClassMap.ts b/packages/ui/src/utils/utilityClassMap.ts index f70052d588..2b8106eef2 100644 --- a/packages/ui/src/utils/utilityClassMap.ts +++ b/packages/ui/src/utils/utilityClassMap.ts @@ -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; + } >;