grafana/ui: Rename Flex component to Stack (#77453)

* grafana/ui: Remove Stack and rename FLex to Stack

* Update types

* Update grafana/ui imports

* Update Grafana imports

* Update docs
This commit is contained in:
Alex Khomenko 2023-11-01 08:48:02 +01:00 committed by GitHub
parent d511925fc9
commit f5cbd4f9d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 319 additions and 751 deletions

View File

@ -16,7 +16,7 @@ Use it whenever you would use custom CSS.
#### When not to use
If you need layout styles, use the Stack, Flex or Grid components instead.
If you need layout styles, use the Stack or Grid components instead.
### How to add a prop to Box

View File

@ -3,7 +3,7 @@ import React from 'react';
import { SpacingTokenControl } from '../../../utils/storybook/themeStorybookControls';
import { Text } from '../../Text/Text';
import { Flex } from '../Flex/Flex';
import { Stack } from '../Stack/Stack';
import { Box, BackgroundColor, BorderColor, BorderStyle, BorderRadius, BoxShadow } from './Box';
import mdx from './Box.mdx';
@ -48,11 +48,11 @@ const Item = ({ background }: { background?: string }) => {
export const Basic: StoryFn<typeof Box> = (args) => {
return (
<Flex>
<Stack>
<Box borderColor="medium" {...args}>
Box
</Box>
</Flex>
</Stack>
);
};
@ -88,64 +88,64 @@ Basic.args = {
export const Background: StoryFn<typeof Box> = () => {
return (
<Flex gap={4}>
<Stack gap={4}>
{backgroundOptions.map((background) => (
<Flex key={background} direction="column" alignItems="flex-start">
<Stack key={background} direction="column" alignItems="flex-start">
{background}
<Box backgroundColor={background} borderColor="strong" borderStyle="solid">
<Item />
</Box>
</Flex>
</Stack>
))}
</Flex>
</Stack>
);
};
export const Border: StoryFn<typeof Box> = () => {
return (
<Flex direction="column" gap={4}>
<Stack direction="column" gap={4}>
<div>
<Text variant="h4">Border Color</Text>
<Flex gap={4} wrap="wrap">
<Stack gap={4} wrap="wrap">
{borderColorOptions.map((border) => (
<Flex key={border} direction="column" alignItems="flex-start">
<Stack key={border} direction="column" alignItems="flex-start">
{border}
<Box borderColor={border} borderStyle="solid">
<Item />
</Box>
</Flex>
</Stack>
))}
</Flex>
</Stack>
</div>
<div>
<Text variant="h4">Border Style</Text>
<Flex gap={4} wrap="wrap">
<Stack gap={4} wrap="wrap">
{borderStyleOptions.map((border) => (
<Flex key={border} direction="column" alignItems="flex-start">
<Stack key={border} direction="column" alignItems="flex-start">
{border}
<Box borderColor="info" borderStyle={border}>
<Item />
</Box>
</Flex>
</Stack>
))}
</Flex>
</Stack>
</div>
</Flex>
</Stack>
);
};
export const Shadow: StoryFn<typeof Box> = () => {
return (
<Flex gap={4}>
<Stack gap={4}>
{boxShadowOptions.map((shadow) => (
<Flex key={shadow} direction="column" alignItems="flex-start">
<Stack key={shadow} direction="column" alignItems="flex-start">
{shadow}
<Box boxShadow={shadow} borderColor="strong" borderStyle="solid">
<Item />
</Box>
</Flex>
</Stack>
))}
</Flex>
</Stack>
);
};

View File

@ -4,7 +4,7 @@ import React, { ElementType, forwardRef, PropsWithChildren } from 'react';
import { GrafanaTheme2, ThemeSpacingTokens, ThemeShape, ThemeShadows } from '@grafana/data';
import { useStyles2 } from '../../../themes';
import { AlignItems, JustifyContent } from '../Flex/Flex';
import { AlignItems, JustifyContent } from '../Stack/Stack';
import { ResponsiveProp, getResponsiveStyle } from '../utils/responsiveness';
type Display = 'flex' | 'block' | 'inline' | 'none';

View File

@ -1,202 +0,0 @@
import { Meta, StoryFn } from '@storybook/react';
import React from 'react';
import { ThemeSpacingTokens } from '@grafana/data';
import { useTheme2 } from '../../../themes';
import { SpacingTokenControl } from '../../../utils/storybook/themeStorybookControls';
import { Flex, JustifyContent, Wrap, Direction } from './Flex';
import mdx from './Flex.mdx';
const Item = ({ color, text, height }: { color: string; text?: string | number; height?: string }) => {
return (
<div
style={{
width: '5em',
height: height,
backgroundColor: color,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{text && <h3 style={{ color: 'black' }}>{text}</h3>}
</div>
);
};
const meta: Meta<typeof Flex> = {
title: 'General/Layout/Flex',
component: Flex,
parameters: {
docs: {
page: mdx,
},
},
};
export const Basic: StoryFn<typeof Flex> = ({ direction, wrap, alignItems, justifyContent, gap }) => {
const theme = useTheme2();
return (
<div style={{ width: '600px', height: '600px', border: '1px solid grey' }}>
<Flex direction={direction} wrap={wrap} alignItems={alignItems} justifyContent={justifyContent} gap={gap}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.warning.main} text={i + 1} />
))}
</Flex>
</div>
);
};
Basic.argTypes = {
gap: SpacingTokenControl,
direction: { control: 'select', options: ['row', 'row-reverse', 'column', 'column-reverse'] },
wrap: { control: 'select', options: ['nowrap', 'wrap', 'wrap-reverse'] },
alignItems: {
control: 'select',
options: ['stretch', 'flex-start', 'flex-end', 'center', 'baseline', 'start', 'end', 'self-start', 'self-end'],
},
justifyContent: {
control: 'select',
options: [
'flex-start',
'flex-end',
'center',
'space-between',
'space-around',
'space-evenly',
'start',
'end',
'left',
'right',
],
},
};
export const AlignItemsExamples: StoryFn<typeof Flex> = () => {
const theme = useTheme2();
return (
<div style={{ width: '600px' }}>
<p>Align items flex-start</p>
<Flex direction="row" wrap="wrap" alignItems="flex-start" justifyContent="start" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Flex>
<p>Align items flex-end</p>
<Flex direction="row" wrap="wrap" alignItems="flex-end" justifyContent="end" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Flex>
<p>Align items baseline</p>
<Flex direction="row" wrap="nowrap" alignItems="baseline" justifyContent="center" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Flex>
<p>Align items center</p>
<Flex direction="row" wrap="wrap" alignItems="center" justifyContent="center" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Flex>
<p>Align items stretch</p>
<Flex direction="row" wrap="wrap" alignItems="stretch" justifyContent="center" gap={2}>
<Item color={theme.colors.error.main} height="10em" />
<Item color={theme.colors.error.main} />
<Item color={theme.colors.error.main} height="3em" />
<Item color={theme.colors.error.main} />
<Item color={theme.colors.error.main} />
</Flex>
</div>
);
};
export const JustifyContentExamples: StoryFn<typeof Flex> = () => {
const theme = useTheme2();
const justifyContentOptions: JustifyContent[] = [
'space-between',
'space-around',
'space-evenly',
'flex-start',
'flex-end',
'center',
];
return (
<div style={{ width: '600px' }}>
{justifyContentOptions.map((justifyContent) => (
<>
<p>Justify Content {justifyContent}</p>
<Flex direction="row" wrap="wrap" alignItems="center" justifyContent={justifyContent} gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.warning.main} text={i + 1} />
))}
</Flex>
</>
))}
</div>
);
};
export const GapExamples: StoryFn<typeof Flex> = () => {
const theme = useTheme2();
const gapOptions: ThemeSpacingTokens[] = [2, 8, 10];
return (
<div style={{ width: '800px' }}>
{gapOptions.map((gap) => (
<>
<p>Gap with spacingToken set to {gap}</p>
<Flex direction="row" wrap="wrap" alignItems="flex-start" justifyContent="flex-start" gap={gap}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Flex>
</>
))}
</div>
);
};
export const WrapExamples: StoryFn<typeof Flex> = () => {
const theme = useTheme2();
const wrapOptions: Wrap[] = ['nowrap', 'wrap', 'wrap-reverse'];
return (
<div style={{ width: '600px' }}>
{wrapOptions.map((wrap) => (
<>
<p>Wrap examples with {wrap} and gap set to spacingToken 2 (16px)</p>
<Flex direction="row" wrap={wrap} alignItems="center" justifyContent="center" gap={2}>
{Array.from({ length: 10 }).map((_, i) => (
<Item key={i} color={theme.colors.warning.main} text={i + 1} />
))}
</Flex>
</>
))}
</div>
);
};
export const DirectionExamples: StoryFn<typeof Flex> = () => {
const theme = useTheme2();
const directionOptions: Direction[] = ['row', 'row-reverse', 'column', 'column-reverse'];
return (
<div style={{ width: '600px' }}>
{directionOptions.map((direction) => (
<>
<p>Direction {direction}</p>
<Flex direction={direction} wrap="wrap" alignItems="center" justifyContent="center" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.warning.main} text={i + 1} />
))}
</Flex>
</>
))}
</div>
);
};
export default meta;

View File

@ -1,138 +0,0 @@
import { Meta, ArgTypes } from '@storybook/blocks';
import { Flex } from './Flex';
<Meta title="MDX|Flex" component={Flex} />
# Flex
The Flex Component aims at providing a more efficient way to lay out, align and distribute space among items in a container and
the decision to create it is to ensure consistency in design across Grafana.
### Usage
#### When to use
Use when in need to align components and small parts of the application. Use as parent container to wrap elements that you wish to align in a certain way.
Also:
* when working with one dimension layout
* to display the direction of the elements
* to set the elements to wrap
* to align items (vertically or horizontally)
#### When not to use
When you need to lay out bigger parts of the application or when you want to create page lay out.
Also:
* for complex grid layouts with various rows and columns
* bidirectional layouts
* complex nesting
* equal height columns
### Variants
Flex component has few variants that can be used based on the desired alignment you need for your case.
Some examples of how to use the Flex component can be seen below:
- AlignItems stretch
```ts
import { Flex } from '@grafana/ui'
import { useTheme2 } from '../../themes';
const theme = useTheme2();
<header>
<h1>Using Flex component to align-items stretch and justify-content to be center</h1>
</header>
<Flex direction="row" wrap="wrap" alignItems="stretch" justifyContent="center" gap={2}>
<Item color={theme.colors.warning.main} height="10em" />
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} height="3em" />
<Item color={theme.colors.warning.main} />
</Flex>
```
- Wrap items wrap-reverse
```ts
import { Flex } from '@grafana/ui'
import { useTheme2 } from '../../themes';
const theme = useTheme2();
<header>
<h1>Using Flex component to align-items with wrap-reverse property</h1>
</header>
<Flex direction="row" wrap="wrap-reverse" alignItems="center" justifyContent="center" gap={4}>
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
</Flex>
```
- JustifyContent flex-start
```ts
import { Flex } from '@grafana/ui'
import { useTheme2 } from '../../themes';
const theme = useTheme2();
<header>
<h1>Using Flex component to align-items with justify-content property</h1>
</header>
<Flex direction="row" wrap="wrap" alignItems="center" justifyContent="flex-start" gap={2}>
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
</Flex>
```
- Gap of 16px using the ThemeSpacingTokens
```ts
import { Flex } from '@grafana/ui'
import { useTheme2 } from '../../themes';
const theme = useTheme2();
<header>
<h1>Using Flex component to align-items with gap of 16px</h1>
</header>
<Flex direction="row" wrap="wrap" alignItems="center" justifyContent="center" gap={2}>
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
</Flex>
```
- Direction column
```ts
import { Flex } from '@grafana/ui'
import { useTheme2 } from '../../themes';
const theme = useTheme2();
<header>
<h1>Using Flex component to align-items with direction column</h1>
</header>
<Flex direction="column" wrap="wrap" alignItems="center" justifyContent="center" gap={2}>
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
<Item color={theme.colors.warning.main} />
</Flex>
```
### Props
<ArgTypes of={Flex} />

View File

@ -1,89 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2, ThemeSpacingTokens } from '@grafana/data';
import { useStyles2 } from '../../../themes';
import { ResponsiveProp, getResponsiveStyle } from '../utils/responsiveness';
export type AlignItems =
| 'stretch'
| 'flex-start'
| 'flex-end'
| 'center'
| 'baseline'
| 'start'
| 'end'
| 'self-start'
| 'self-end';
export type JustifyContent =
| 'flex-start'
| 'flex-end'
| 'center'
| 'space-between'
| 'space-around'
| 'space-evenly'
| 'start'
| 'end'
| 'left'
| 'right';
export type Direction = 'row' | 'row-reverse' | 'column' | 'column-reverse';
export type Wrap = 'nowrap' | 'wrap' | 'wrap-reverse';
interface FlexProps extends Omit<React.HTMLAttributes<HTMLElement>, 'className' | 'style'> {
gap?: ResponsiveProp<ThemeSpacingTokens>;
alignItems?: ResponsiveProp<AlignItems>;
justifyContent?: ResponsiveProp<JustifyContent>;
direction?: ResponsiveProp<Direction>;
wrap?: ResponsiveProp<Wrap>;
children?: React.ReactNode;
}
export const Flex = React.forwardRef<HTMLDivElement, FlexProps>(
({ gap = 1, alignItems, justifyContent, direction, wrap, children, ...rest }, ref) => {
const styles = useStyles2(getStyles, gap, alignItems, justifyContent, direction, wrap);
return (
<div ref={ref} className={styles.flex} {...rest}>
{children}
</div>
);
}
);
Flex.displayName = 'Flex';
const getStyles = (
theme: GrafanaTheme2,
gap: FlexProps['gap'],
alignItems: FlexProps['alignItems'],
justifyContent: FlexProps['justifyContent'],
direction: FlexProps['direction'],
wrap: FlexProps['wrap']
) => {
return {
flex: css([
{
display: 'flex',
},
getResponsiveStyle<Direction>(theme, direction, (val) => ({
flexDirection: val,
})),
getResponsiveStyle<Wrap>(theme, wrap, (val) => ({
flexWrap: val,
})),
getResponsiveStyle<AlignItems>(theme, alignItems, (val) => ({
alignItems: val,
})),
getResponsiveStyle<JustifyContent>(theme, justifyContent, (val) => ({
justifyContent: val,
})),
getResponsiveStyle<ThemeSpacingTokens>(theme, gap, (val) => ({
gap: theme.spacing(val),
})),
]),
};
};

View File

@ -17,11 +17,6 @@ Use the Grid component when you want to create structured and organized layouts
Use the `Stack` component instead for these use cases:
- **Simple layouts:** When you need to arrange elements in a linear format, either vertically or horizontally.
- **Regular flow:** When you want a "regular" site flow but with standardized spacing between the elements.
Use the `Flex` component instead for these use cases:
- **Alignment:** More options for item alignment.
- **Flex items:** Custom flex basis or configure how items stretch and wrap.

View File

@ -1,20 +0,0 @@
import React from 'react';
import { ThemeSpacingTokens } from '@grafana/data';
import { ResponsiveProp } from '../utils/responsiveness';
import { Stack } from './Stack';
interface HorizontalStackProps extends Omit<React.HTMLAttributes<HTMLElement>, 'className' | 'style'> {
gap?: ResponsiveProp<ThemeSpacingTokens>;
}
export const HorizontalStack = React.forwardRef<HTMLDivElement, React.PropsWithChildren<HorizontalStackProps>>(
({ children, gap = 1, ...rest }, ref) => (
<Stack ref={ref} direction="row" gap={gap} {...rest}>
{children}
</Stack>
)
);
HorizontalStack.displayName = 'HorizontalStack';

View File

@ -1,16 +1,31 @@
import { Meta, StoryFn } from '@storybook/react';
import React, { ReactNode } from 'react';
import React from 'react';
import { ThemeSpacingTokens } from '@grafana/data';
import { useTheme2 } from '../../../themes';
import { SpacingTokenControl } from '../../../utils/storybook/themeStorybookControls';
import { Alert } from '../../Alert/Alert';
import { Button } from '../../Button';
import { Card } from '../../Card/Card';
import { Text } from '../../Text/Text';
import { HorizontalStack } from './HorizontalStack';
import { Stack } from './Stack';
import { Stack, JustifyContent, Wrap, Direction } from './Stack';
import mdx from './Stack.mdx';
const Item = ({ color, text, height }: { color: string; text?: string | number; height?: string }) => {
return (
<div
style={{
width: '5em',
height: height,
backgroundColor: color,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{text && <h3 style={{ color: 'black' }}>{text}</h3>}
</div>
);
};
const meta: Meta<typeof Stack> = {
title: 'General/Layout/Stack',
component: Stack,
@ -19,204 +34,169 @@ const meta: Meta<typeof Stack> = {
page: mdx,
},
},
argTypes: {
gap: SpacingTokenControl,
direction: { control: 'select', options: ['row', 'column'] },
},
};
const Item = ({ children }: { children: ReactNode }) => (
<div style={{ backgroundColor: 'lightgrey', width: '100px', height: '50px' }}>{children}</div>
);
export const Basic: StoryFn<typeof Stack> = ({ direction = 'column', gap = 2 }) => {
export const Basic: StoryFn<typeof Stack> = ({ direction, wrap, alignItems, justifyContent, gap }) => {
const theme = useTheme2();
return (
<Stack direction={direction} gap={gap}>
<Item>Item 1</Item>
<Item>Item 2</Item>
<Item>Item 3</Item>
</Stack>
);
};
export const TestCases: StoryFn<typeof Stack> = () => {
return (
<div style={{ width: '100%' }}>
<Stack gap={4}>
<h2>Comparisons Stack vs No stack</h2>
<HorizontalStack>
<Example title="No stack">
<Button>A button</Button>
<Button>Longer button button</Button>
</Example>
<Example title="Horizontal stack">
<HorizontalStack>
<Button>A button</Button>
<Button>Longer button button</Button>
</HorizontalStack>
</Example>
<Example title="Vertical stack">
<Stack>
<Button>A button</Button>
<Button>Longer button button</Button>
</Stack>
</Example>
</HorizontalStack>
<HorizontalStack>
<Example title="No stack, mismatched heights">
<Card>
<Card.Heading>I am a card heading</Card.Heading>
</Card>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
<Card.Description>Ohhhhh - and now a description and some actions</Card.Description>
<Card.Actions>
<Button variant="secondary">Settings</Button>
<Button variant="secondary">Explore</Button>
</Card.Actions>
</Card>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
<Card.Description>Ohhhhh - and now a description!</Card.Description>
</Card>
<Button>Please press me!</Button>
</Example>
<Example title="Vertical stack, mismatched heights">
<Stack>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
</Card>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
<Card.Description>Ohhhhh - and now a description and some actions</Card.Description>
<Card.Actions>
<Button variant="secondary">Settings</Button>
<Button variant="secondary">Explore</Button>
</Card.Actions>
</Card>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
<Card.Description>Ohhhhh - and now a description!</Card.Description>
</Card>
<Button>Please press me!</Button>
</Stack>
</Example>
</HorizontalStack>
<div style={{ width: 500 }}>
<Example title="No stack, too many items">
<Button>A button</Button>
<Button>Longer button button</Button>
<Button>Another button</Button>
<Button>And another</Button>
<Button>Why not - one last button!</Button>
</Example>
<Example title="Horizontal stack, too many items">
<HorizontalStack>
<Button>A button</Button>
<Button>Longer button button</Button>
<Button>Another button</Button>
<Button>And another</Button>
<Button>Why not - one last button!</Button>
</HorizontalStack>
</Example>
</div>
<h2>Child alignment</h2>
<div style={{ width: 500 }}>
<Example title="Row, mismatched heights">
<HorizontalStack>
<MyComponent>
<div style={{ height: 50, width: 100, background: 'blue' }} />
</MyComponent>
<MyComponent>
<div style={{ height: 150, width: 100, background: 'orange' }} />
</MyComponent>
</HorizontalStack>
</Example>
</div>
<Example title="Horizontal stack, mismatched heights">
<HorizontalStack>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
</Card>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
<Card.Description>Ohhhhh - and now a description and some actions</Card.Description>
<Card.Actions>
<Button variant="secondary">Settings</Button>
<Button variant="secondary">Explore</Button>
</Card.Actions>
</Card>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
<Card.Description>Ohhhhh - and now a description!</Card.Description>
</Card>
</HorizontalStack>
</Example>
<Example title="Horizontal stack, mismatched heights with different components">
<HorizontalStack>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
</Card>
<Card>
<Card.Heading>I am a card heading</Card.Heading>
<Card.Description>Ohhhhh - and now a description!</Card.Description>
</Card>
<Alert severity="info" title="Plus an alert!" />
</HorizontalStack>
</Example>
<Example title="Horizontal stack, alerts with even heights">
<HorizontalStack>
<Alert severity="info" title="Plus an alert!" />
<Alert severity="success" title="Plus an alert!" />
<Alert severity="warning" title="Plus an alert!" />
<Alert severity="error" title="Plus an alert!" />
</HorizontalStack>
</Example>
<Example title="Horizontal stack, alerts with mismatched heights">
<HorizontalStack>
<Alert severity="info" title="Plus an alert!" />
<Alert severity="success" title="Plus an alert!" />
<Alert severity="warning" title="Plus an alert!">
Surprise - a description! What will happen to the height of all the other alerts?
</Alert>
<Alert severity="error" title="Plus an alert!" />
</HorizontalStack>
</Example>
<div style={{ width: '600px', height: '600px', border: '1px solid grey' }}>
<Stack direction={direction} wrap={wrap} alignItems={alignItems} justifyContent={justifyContent} gap={gap}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.warning.main} text={i + 1} />
))}
</Stack>
</div>
);
};
function Example({ title, children }: { title: string; children: React.ReactNode }) {
Basic.argTypes = {
gap: SpacingTokenControl,
direction: { control: 'select', options: ['row', 'row-reverse', 'column', 'column-reverse'] },
wrap: { control: 'select', options: ['nowrap', 'wrap', 'wrap-reverse'] },
alignItems: {
control: 'select',
options: ['stretch', 'flex-start', 'flex-end', 'center', 'baseline', 'start', 'end', 'self-start', 'self-end'],
},
justifyContent: {
control: 'select',
options: [
'flex-start',
'flex-end',
'center',
'space-between',
'space-around',
'space-evenly',
'start',
'end',
'left',
'right',
],
},
};
export const AlignItemsExamples: StoryFn<typeof Stack> = () => {
const theme = useTheme2();
return (
<div>
<Text variant="h3">{title}</Text>
<div style={{ background: 'rgba(255,255,255,0.1)', border: '1px dashed green' }}>{children}</div>
<div style={{ width: '600px' }}>
<p>Align items flex-start</p>
<Stack direction="row" wrap="wrap" alignItems="flex-start" justifyContent="start" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Stack>
<p>Align items flex-end</p>
<Stack direction="row" wrap="wrap" alignItems="flex-end" justifyContent="end" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Stack>
<p>Align items baseline</p>
<Stack direction="row" wrap="nowrap" alignItems="baseline" justifyContent="center" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Stack>
<p>Align items center</p>
<Stack direction="row" wrap="wrap" alignItems="center" justifyContent="center" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Stack>
<p>Align items stretch</p>
<Stack direction="row" wrap="wrap" alignItems="stretch" justifyContent="center" gap={2}>
<Item color={theme.colors.error.main} height="10em" />
<Item color={theme.colors.error.main} />
<Item color={theme.colors.error.main} height="3em" />
<Item color={theme.colors.error.main} />
<Item color={theme.colors.error.main} />
</Stack>
</div>
);
}
};
function MyComponent({ children }: { children: React.ReactNode }) {
return <div style={{ background: 'rgba(0,255,255, 0.2)', padding: 16 }}>{children}</div>;
}
export const JustifyContentExamples: StoryFn<typeof Stack> = () => {
const theme = useTheme2();
const justifyContentOptions: JustifyContent[] = [
'space-between',
'space-around',
'space-evenly',
'flex-start',
'flex-end',
'center',
];
return (
<div style={{ width: '600px' }}>
{justifyContentOptions.map((justifyContent) => (
<>
<p>Justify Content {justifyContent}</p>
<Stack direction="row" wrap="wrap" alignItems="center" justifyContent={justifyContent} gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.warning.main} text={i + 1} />
))}
</Stack>
</>
))}
</div>
);
};
export const GapExamples: StoryFn<typeof Stack> = () => {
const theme = useTheme2();
const gapOptions: ThemeSpacingTokens[] = [2, 8, 10];
return (
<div style={{ width: '800px' }}>
{gapOptions.map((gap) => (
<>
<p>Gap with spacingToken set to {gap}</p>
<Stack direction="row" wrap="wrap" alignItems="flex-start" justifyContent="flex-start" gap={gap}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.error.main} text={i + 1} />
))}
</Stack>
</>
))}
</div>
);
};
export const WrapExamples: StoryFn<typeof Stack> = () => {
const theme = useTheme2();
const wrapOptions: Wrap[] = ['nowrap', 'wrap', 'wrap-reverse'];
return (
<div style={{ width: '600px' }}>
{wrapOptions.map((wrap) => (
<>
<p>Wrap examples with {wrap} and gap set to spacingToken 2 (16px)</p>
<Stack direction="row" wrap={wrap} alignItems="center" justifyContent="center" gap={2}>
{Array.from({ length: 10 }).map((_, i) => (
<Item key={i} color={theme.colors.warning.main} text={i + 1} />
))}
</Stack>
</>
))}
</div>
);
};
export const DirectionExamples: StoryFn<typeof Stack> = () => {
const theme = useTheme2();
const directionOptions: Direction[] = ['row', 'row-reverse', 'column', 'column-reverse'];
return (
<div style={{ width: '600px' }}>
{directionOptions.map((direction) => (
<>
<p>Direction {direction}</p>
<Stack direction={direction} wrap="wrap" alignItems="center" justifyContent="center" gap={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Item key={i} color={theme.colors.warning.main} text={i + 1} />
))}
</Stack>
</>
))}
</div>
);
};
export default meta;

View File

@ -1,44 +1,27 @@
import { Meta, ArgTypes, Canvas } from '@storybook/blocks';
import { Stack, HorizontalStack } from './index';
import * as Stories from './Stack.internal.story';
import { Meta, ArgTypes } from '@storybook/blocks';
import { Stack } from './Stack';
<Meta title="MDX|Stack" component={Stack} />
# Stack
The `Stack` component is designed to assist with layout and positioning of elements within a container, offering a simple and flexible way to stack elements vertically or horizontally. This documentation outlines the proper usage of the Stack component and provides guidance on when to use it over the Grid or Flex components.
There is also a `HorizontalStack` component, which is a thin wrapper around Stack, equivalent to `<Stack direction="row">`.
The Stack component is a simple wrapper around the flexbox layout model that allows to easily create responsive and flexible layouts. It provides a simple and intuitive way to align and distribute items within a container either horizontally or vertically.
### Usage
#### When to use
Use the Stack component when you need to arrange elements in a linear format, either vertically or horizontally. It's particularly useful when you want to maintain consistent spacing between items.
Use the Stack component when:
- You need a simple way to stack elements with predictable spacing.
- You want to ensure consistent alignment.
- For creating responsive and flexible layouts that can adapt to different screen sizes and orientations.
- When needing a simple and intuitive way to align and distribute items within a container either horizontally or vertically.
- To easily reorder and rearrange elements without changing the HTML structure.
- When aiming to create equal height columns.
- To create a grid-like structure with automatic wrapping and sizing of items based on the available space.
#### When not to use
Use the `Grid` component instead for these use cases:
- For complex multi-dimensional layouts with intricate requirements that are better suited for CSS frameworks or grid systems.
- When precise control over spacing and positioning of elements is necessary.
- **Intricate Layouts:** Grids are ideal for complex dashboard and magazine-style designs with rows and columns.
- **Structured Grid:** Use Grids when items need to span multiple rows/columns, creating structured layouts.
Use the `Flex` component instead for these use cases:
- **Alignment:** More options for item alignment.
- **Flex items:** Custom flex basis or configure how items stretch and wrap.
## Props
### Stack
### Props
<ArgTypes of={Stack} />
### HorizontalStack
<ArgTypes of={HorizontalStack} />

View File

@ -1,26 +1,89 @@
import { css } from '@emotion/css';
import React from 'react';
import { ThemeSpacingTokens } from '@grafana/data';
import { GrafanaTheme2, ThemeSpacingTokens } from '@grafana/data';
import { useStyles2 } from '../../../themes';
import { ResponsiveProp, getResponsiveStyle } from '../utils/responsiveness';
export type AlignItems =
| 'stretch'
| 'flex-start'
| 'flex-end'
| 'center'
| 'baseline'
| 'start'
| 'end'
| 'self-start'
| 'self-end';
export type JustifyContent =
| 'flex-start'
| 'flex-end'
| 'center'
| 'space-between'
| 'space-around'
| 'space-evenly'
| 'start'
| 'end'
| 'left'
| 'right';
export type Direction = 'row' | 'row-reverse' | 'column' | 'column-reverse';
export type Wrap = 'nowrap' | 'wrap' | 'wrap-reverse';
import { Flex } from '../Flex/Flex';
import { ResponsiveProp } from '../utils/responsiveness';
interface StackProps extends Omit<React.HTMLAttributes<HTMLElement>, 'className' | 'style'> {
direction?: ResponsiveProp<'column' | 'row'>;
gap?: ResponsiveProp<ThemeSpacingTokens>;
alignItems?: ResponsiveProp<AlignItems>;
justifyContent?: ResponsiveProp<JustifyContent>;
direction?: ResponsiveProp<Direction>;
wrap?: ResponsiveProp<Wrap>;
children?: React.ReactNode;
}
export const Stack = React.forwardRef<HTMLDivElement, React.PropsWithChildren<StackProps>>(
({ gap = 1, direction = 'column', children, ...rest }, ref) => {
export const Stack = React.forwardRef<HTMLDivElement, StackProps>(
({ gap = 1, alignItems, justifyContent, direction, wrap, children, ...rest }, ref) => {
const styles = useStyles2(getStyles, gap, alignItems, justifyContent, direction, wrap);
return (
<Flex ref={ref} gap={gap} direction={direction} wrap="wrap" {...rest}>
{React.Children.toArray(children)
.filter(Boolean)
.map((child, index) => (
<div key={index}>{child}</div>
))}
</Flex>
<div ref={ref} className={styles.flex} {...rest}>
{children}
</div>
);
}
);
Stack.displayName = 'Stack';
const getStyles = (
theme: GrafanaTheme2,
gap: StackProps['gap'],
alignItems: StackProps['alignItems'],
justifyContent: StackProps['justifyContent'],
direction: StackProps['direction'],
wrap: StackProps['wrap']
) => {
return {
flex: css([
{
display: 'flex',
},
getResponsiveStyle<Direction>(theme, direction, (val) => ({
flexDirection: val,
})),
getResponsiveStyle<Wrap>(theme, wrap, (val) => ({
flexWrap: val,
})),
getResponsiveStyle<AlignItems>(theme, alignItems, (val) => ({
alignItems: val,
})),
getResponsiveStyle<JustifyContent>(theme, justifyContent, (val) => ({
justifyContent: val,
})),
getResponsiveStyle<ThemeSpacingTokens>(theme, gap, (val) => ({
gap: theme.spacing(val),
})),
]),
};
};

View File

@ -1,2 +0,0 @@
export { Stack } from './Stack';
export { HorizontalStack } from './HorizontalStack';

View File

@ -9,7 +9,5 @@
* be subject to the standard policies
*/
export * from './components/Layout/Flex/Flex';
export { Grid } from './components/Layout/Grid/Grid';
export { Stack, HorizontalStack } from './components/Layout/Stack';
export { Stack } from './components/Layout/Stack/Stack';

View File

@ -6,7 +6,7 @@ import { useLocation } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { CustomScrollbar, Icon, IconButton, useStyles2 } from '@grafana/ui';
import { Flex } from '@grafana/ui/src/unstable';
import { Stack } from '@grafana/ui/src/unstable';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { t } from 'app/core/internationalization';
import { useSelector } from 'app/types';
@ -55,7 +55,7 @@ export const MegaMenu = React.memo(
<CustomScrollbar showScrollIndicators hideHorizontalTrack>
<ul className={styles.itemList}>
{navItems.map((link, index) => (
<Flex key={link.text} direction="row" alignItems="center">
<Stack key={link.text} direction="row" alignItems="center">
<MegaMenuItem
link={link}
onClick={state.megaMenu === 'open' ? onClose : undefined}
@ -74,7 +74,7 @@ export const MegaMenu = React.memo(
variant="secondary"
/>
)}
</Flex>
</Stack>
))}
</ul>
</CustomScrollbar>

View File

@ -2,7 +2,7 @@ import React, { forwardRef, PropsWithChildren } from 'react';
import { IconName } from '@grafana/data';
import { Icon, Tooltip, Box } from '@grafana/ui';
import { Flex } from '@grafana/ui/src/unstable';
import { Stack } from '@grafana/ui/src/unstable';
import { Unit } from 'app/types';
type OrgUnitProps = { units?: Unit[]; icon: IconName };
@ -15,7 +15,7 @@ export const OrgUnits = ({ units, icon }: OrgUnitProps) => {
return units.length > 1 ? (
<Tooltip
placement={'top'}
content={<Flex direction={'column'}>{units?.map((unit) => <span key={unit.name}>{unit.name}</span>)}</Flex>}
content={<Stack direction={'column'}>{units?.map((unit) => <span key={unit.name}>{unit.name}</span>)}</Stack>}
>
<Content icon={icon}>{units.length}</Content>
</Tooltip>

View File

@ -16,7 +16,7 @@ import {
Avatar,
Box,
} from '@grafana/ui';
import { Flex, Stack } from '@grafana/ui/src/unstable';
import { Stack } from '@grafana/ui/src/unstable';
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
import { fetchRoleOptions } from 'app/core/components/RolePicker/api';
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
@ -223,9 +223,9 @@ export const OrgUsersTable = ({
getRowId={(user) => String(user.userId)}
fetchData={fetchData}
/>
<Flex justifyContent="flex-end">
<Stack justifyContent="flex-end">
<Pagination onNavigate={changePage} currentPage={page} numberOfPages={totalPages} hideWhenSinglePage={true} />
</Flex>
</Stack>
</TableWrapper>
{Boolean(userToRemove) && (
<ConfirmModal

View File

@ -12,7 +12,7 @@ import {
Text,
Avatar,
} from '@grafana/ui';
import { Flex, Stack } from '@grafana/ui/src/unstable';
import { Stack } from '@grafana/ui/src/unstable';
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
import { UserDTO } from 'app/types';
@ -68,14 +68,14 @@ export const UsersTable = ({
header: 'Belongs to',
cell: ({ cell: { value, row } }: Cell<'orgs'>) => {
return (
<Flex alignItems={'center'}>
<Stack alignItems={'center'}>
<OrgUnits units={value} icon={'building'} />
{row.original.isAdmin && (
<Tooltip placement="top" content="Grafana Admin">
<Icon name="shield" />
</Tooltip>
)}
</Flex>
</Stack>
);
},
sortType: (a, b) => (a.original.orgs?.length || 0) - (b.original.orgs?.length || 0),
@ -146,9 +146,9 @@ export const UsersTable = ({
<Stack gap={2}>
<InteractiveTable columns={columns} data={users} getRowId={(user) => String(user.id)} fetchData={fetchData} />
{showPaging && (
<Flex justifyContent={'flex-end'}>
<Stack justifyContent={'flex-end'}>
<Pagination numberOfPages={totalPages} currentPage={currentPage} onNavigate={onChangePage} />
</Flex>
</Stack>
)}
</Stack>
);

View File

@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config, locationService, reportInteraction } from '@grafana/runtime';
import { Button, useStyles2, Text, Box } from '@grafana/ui';
import { Flex } from '@grafana/ui/src/unstable';
import { Stack } from '@grafana/ui/src/unstable';
import { Trans } from 'app/core/internationalization';
import { DashboardModel } from 'app/features/dashboard/state';
import { onAddLibraryPanel, onCreateNewPanel, onImportDashboard } from 'app/features/dashboard/utils/dashboard';
@ -24,11 +24,11 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => {
const initialDatasource = useSelector((state) => state.dashboard.initialDatasource);
return (
<Flex alignItems="center" justifyContent="center">
<Stack alignItems="center" justifyContent="center">
<div className={styles.wrapper}>
<Flex alignItems="stretch" justifyContent="center" gap={4} direction="column">
<Stack alignItems="stretch" justifyContent="center" gap={4} direction="column">
<Box borderColor="strong" borderStyle="dashed" padding={4}>
<Flex direction="column" alignItems="center" gap={2}>
<Stack direction="column" alignItems="center" gap={2}>
<Text element="h1" textAlignment="center" weight="medium">
<Trans i18nKey="dashboard.empty.add-visualization-header">
Start your new dashboard by adding a visualization
@ -56,12 +56,12 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => {
>
<Trans i18nKey="dashboard.empty.add-visualization-button">Add visualization</Trans>
</Button>
</Flex>
</Stack>
</Box>
<Flex direction={{ xs: 'column', md: 'row' }} wrap="wrap" gap={4}>
<Stack direction={{ xs: 'column', md: 'row' }} wrap="wrap" gap={4}>
{config.featureToggles.vizAndWidgetSplit && (
<Box borderColor="strong" borderStyle="dashed" padding={3} grow={1}>
<Flex direction="column" alignItems="center" gap={1}>
<Stack direction="column" alignItems="center" gap={1}>
<Text element="h3" textAlignment="center" weight="medium">
<Trans i18nKey="dashboard.empty.add-widget-header">Add a widget</Trans>
</Text>
@ -82,11 +82,11 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => {
>
<Trans i18nKey="dashboard.empty.add-widget-button">Add widget</Trans>
</Button>
</Flex>
</Stack>
</Box>
)}
<Box borderColor="strong" borderStyle="dashed" padding={3} grow={1}>
<Flex direction="column" alignItems="center" gap={1}>
<Stack direction="column" alignItems="center" gap={1}>
<Text element="h3" textAlignment="center" weight="medium">
<Trans i18nKey="dashboard.empty.add-library-panel-header">Import panel</Trans>
</Text>
@ -109,10 +109,10 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => {
>
<Trans i18nKey="dashboard.empty.add-library-panel-button">Add library panel</Trans>
</Button>
</Flex>
</Stack>
</Box>
<Box borderColor="strong" borderStyle="dashed" padding={3} grow={1}>
<Flex direction="column" alignItems="center" gap={1}>
<Stack direction="column" alignItems="center" gap={1}>
<Text element="h3" textAlignment="center" weight="medium">
<Trans i18nKey="dashboard.empty.import-a-dashboard-header">Import a dashboard</Trans>
</Text>
@ -136,12 +136,12 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => {
>
<Trans i18nKey="dashboard.empty.import-dashboard-button">Import dashboard</Trans>
</Button>
</Flex>
</Stack>
</Box>
</Flex>
</Flex>
</Stack>
</Stack>
</div>
</Flex>
</Stack>
);
};

View File

@ -13,7 +13,7 @@ import {
InlineField,
Pagination,
} from '@grafana/ui';
import { Flex } from '@grafana/ui/src/unstable';
import { Stack } from '@grafana/ui/src/unstable';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { Page } from 'app/core/components/Page/Page';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
@ -253,9 +253,9 @@ export const ServiceAccountsListPageUnconnected = ({
</tbody>
</table>
<Flex justifyContent="flex-end">
<Stack justifyContent="flex-end">
<Pagination hideWhenSinglePage currentPage={page} numberOfPages={totalPages} onNavigate={changePage} />
</Flex>
</Stack>
</div>
</>
)}

View File

@ -14,7 +14,7 @@ import {
Pagination,
Avatar,
} from '@grafana/ui';
import { Stack, Flex } from '@grafana/ui/src/unstable';
import { Stack } from '@grafana/ui/src/unstable';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { Page } from 'app/core/components/Page/Page';
import { fetchRoleOptions } from 'app/core/components/RolePicker/api';
@ -169,14 +169,14 @@ export const TeamList = ({
getRowId={(team) => String(team.id)}
fetchData={changeSort}
/>
<Flex justifyContent="flex-end">
<Stack justifyContent="flex-end">
<Pagination
hideWhenSinglePage
currentPage={page}
numberOfPages={totalPages}
onNavigate={changePage}
/>
</Flex>
</Stack>
</TableWrapper>
</Stack>
</>