mirror of https://github.com/grafana/grafana
GrafanaUI: Create Box component (#73637)
parent
7e4ae5fdb6
commit
42cc6b1842
@ -0,0 +1,138 @@ |
||||
import { Meta, StoryFn } from '@storybook/react'; |
||||
import React from 'react'; |
||||
|
||||
import { SpacingTokenControl } from '../../../utils/storybook/themeStorybookControls'; |
||||
import { Text } from '../../Text/Text'; |
||||
import { Flex } from '../Flex/Flex'; |
||||
|
||||
import { Box, BackgroundColor, BorderColor, BorderStyle, BorderRadius, BoxShadow } from './Box'; |
||||
import mdx from './Box.mdx'; |
||||
|
||||
const backgroundOptions: BackgroundColor[] = ['primary', 'secondary', 'canvas', 'error', 'success', 'warning', 'info']; |
||||
const borderColorOptions: BorderColor[] = ['weak', 'medium', 'strong', 'error', 'success', 'warning', 'info']; |
||||
const borderStyleOptions: BorderStyle[] = ['dashed', 'solid']; |
||||
const borderRadiusOptions: BorderRadius[] = ['default', 'pill', 'circle']; |
||||
const boxShadowOptions: BoxShadow[] = ['z1', 'z2', 'z3']; |
||||
|
||||
const meta: Meta<typeof Box> = { |
||||
title: 'General/Layout/Box', |
||||
component: Box, |
||||
parameters: { |
||||
docs: { |
||||
page: mdx, |
||||
}, |
||||
controls: { exclude: ['element'] }, |
||||
}, |
||||
}; |
||||
|
||||
const Item = ({ background }: { background?: string }) => { |
||||
return ( |
||||
<div |
||||
style={{ |
||||
width: '50px', |
||||
height: '50px', |
||||
background, |
||||
}} |
||||
/> |
||||
); |
||||
}; |
||||
|
||||
export const Basic: StoryFn<typeof Box> = (args) => { |
||||
return ( |
||||
<div style={{ backgroundColor: 'green' }}> |
||||
<Box {...args}> |
||||
<Item background="red" /> |
||||
</Box> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
Basic.argTypes = { |
||||
grow: { control: 'number' }, |
||||
shrink: { control: 'number' }, |
||||
margin: SpacingTokenControl, |
||||
marginX: SpacingTokenControl, |
||||
marginY: SpacingTokenControl, |
||||
marginTop: SpacingTokenControl, |
||||
marginBottom: SpacingTokenControl, |
||||
marginLeft: SpacingTokenControl, |
||||
marginRight: SpacingTokenControl, |
||||
padding: SpacingTokenControl, |
||||
paddingX: SpacingTokenControl, |
||||
paddingY: SpacingTokenControl, |
||||
paddingTop: SpacingTokenControl, |
||||
paddingBottom: SpacingTokenControl, |
||||
paddingLeft: SpacingTokenControl, |
||||
paddingRight: SpacingTokenControl, |
||||
display: { control: 'select', options: ['flex', 'block', 'inline', 'none'] }, |
||||
backgroundColor: { control: 'select', options: backgroundOptions }, |
||||
borderStyle: { control: 'select', options: borderStyleOptions }, |
||||
borderColor: { control: 'select', options: borderColorOptions }, |
||||
borderRadius: { control: 'select', options: borderRadiusOptions }, |
||||
boxShadow: { control: 'select', options: boxShadowOptions }, |
||||
}; |
||||
|
||||
export const Background: StoryFn<typeof Box> = () => { |
||||
return ( |
||||
<Flex gap={4}> |
||||
{backgroundOptions.map((background) => ( |
||||
<Flex key={background} direction="column" alignItems="flex-start"> |
||||
{background} |
||||
<Box backgroundColor={background} borderColor="strong" borderStyle="solid"> |
||||
<Item /> |
||||
</Box> |
||||
</Flex> |
||||
))} |
||||
</Flex> |
||||
); |
||||
}; |
||||
|
||||
export const Border: StoryFn<typeof Box> = () => { |
||||
return ( |
||||
<Flex direction="column" gap={4}> |
||||
<div> |
||||
<Text variant="h4">Border Color</Text> |
||||
<Flex gap={4} wrap="wrap"> |
||||
{borderColorOptions.map((border) => ( |
||||
<Flex key={border} direction="column" alignItems="flex-start"> |
||||
{border} |
||||
<Box borderColor={border} borderStyle="solid"> |
||||
<Item /> |
||||
</Box> |
||||
</Flex> |
||||
))} |
||||
</Flex> |
||||
</div> |
||||
<div> |
||||
<Text variant="h4">Border Style</Text> |
||||
<Flex gap={4} wrap="wrap"> |
||||
{borderStyleOptions.map((border) => ( |
||||
<Flex key={border} direction="column" alignItems="flex-start"> |
||||
{border} |
||||
<Box borderColor="info" borderStyle={border}> |
||||
<Item /> |
||||
</Box> |
||||
</Flex> |
||||
))} |
||||
</Flex> |
||||
</div> |
||||
</Flex> |
||||
); |
||||
}; |
||||
|
||||
export const Shadow: StoryFn<typeof Box> = () => { |
||||
return ( |
||||
<Flex gap={4}> |
||||
{boxShadowOptions.map((shadow) => ( |
||||
<Flex key={shadow} direction="column" alignItems="flex-start"> |
||||
{shadow} |
||||
<Box boxShadow={shadow} borderColor="strong" borderStyle="solid"> |
||||
<Item /> |
||||
</Box> |
||||
</Flex> |
||||
))} |
||||
</Flex> |
||||
); |
||||
}; |
||||
|
||||
export default meta; |
@ -0,0 +1,39 @@ |
||||
import { Meta, ArgTypes } from '@storybook/blocks'; |
||||
import { Box } from './Box'; |
||||
|
||||
<Meta title="MDX|Box" component={Box} /> |
||||
|
||||
# Box |
||||
|
||||
The Box Component is the most basic layout component. It can be used to build more complex components and layouts with properties |
||||
that use our design tokens instead of using CSS. |
||||
|
||||
### Usage |
||||
|
||||
#### When to use |
||||
|
||||
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. |
||||
|
||||
### How to add a prop to Box |
||||
|
||||
1. Make sure you absolutely need this prop. If in doubt, ask someone from the design system team. |
||||
2. Add the prop to the `BoxProps` interface in `Box.tsx`. |
||||
- Make sure it is strictly typed, making use of design tokens if needed. Instead of `[propName]: number`, use `[propName]: ThemeSpacingTokens`; |
||||
- If it is a CSS prop, you should make it responsive. To do so, instead of defining it as `[propName]: ThemeSpacingTokens`, |
||||
define it as `[propName]: ResponsiveProp<ThemeSpacingTokens>`. |
||||
3. Add it to the CSS array in `getStyles` in `Box.tsx`. |
||||
- If it is a `ResponsiveProp`, you should use the `getResponsiveStyle` helper function |
||||
``` |
||||
getResponsiveStyle(theme, [propName], (val) => ({ |
||||
[cssProp]: theme.spacing(val), |
||||
})), |
||||
``` |
||||
4. Add it to the `Box` story in `Box.internal.story.tsx`, by explicity adding it to `Basic.argTypes` |
||||
|
||||
### Props |
||||
|
||||
<ArgTypes of={Box} /> |
@ -0,0 +1,267 @@ |
||||
import { css } from '@emotion/css'; |
||||
import React, { ElementType } from 'react'; |
||||
|
||||
import { GrafanaTheme2, ThemeSpacingTokens, ThemeShape, ThemeShadows } from '@grafana/data'; |
||||
|
||||
import { useStyles2 } from '../../../themes'; |
||||
import { AlignItems, JustifyContent } from '../Flex/Flex'; |
||||
import { ResponsiveProp, getResponsiveStyle } from '../utils/responsiveness'; |
||||
|
||||
type Display = 'flex' | 'block' | 'inline' | 'none'; |
||||
export type BackgroundColor = keyof GrafanaTheme2['colors']['background'] | 'error' | 'success' | 'warning' | 'info'; |
||||
export type BorderStyle = 'solid' | 'dashed'; |
||||
export type BorderColor = keyof GrafanaTheme2['colors']['border'] | 'error' | 'success' | 'warning' | 'info'; |
||||
export type BorderRadius = keyof ThemeShape['radius']; |
||||
export type BoxShadow = keyof ThemeShadows; |
||||
|
||||
interface BoxProps { |
||||
// Margin props
|
||||
/** Sets the property `margin` */ |
||||
margin?: ResponsiveProp<ThemeSpacingTokens>; |
||||
/** Sets the properties `margin-top` and `margin-bottom`. Higher priority than margin. */ |
||||
marginX?: ResponsiveProp<ThemeSpacingTokens>; |
||||
/** Sets the properties `margin-left` and `margin-right`. Higher priority than margin. */ |
||||
marginY?: ResponsiveProp<ThemeSpacingTokens>; |
||||
/** Sets the property `margin-top`. Higher priority than margin and marginY. */ |
||||
marginTop?: ResponsiveProp<ThemeSpacingTokens>; |
||||
/** Sets the property `margin-bottom`. Higher priority than margin and marginXY */ |
||||
marginBottom?: ResponsiveProp<ThemeSpacingTokens>; |
||||
/** Sets the property `margin-left`. Higher priority than margin and marginX. */ |
||||
marginLeft?: ResponsiveProp<ThemeSpacingTokens>; |
||||
/** Sets the property `margin-right`. Higher priority than margin and marginX. */ |
||||
marginRight?: ResponsiveProp<ThemeSpacingTokens>; |
||||
|
||||
// Padding props
|
||||
/** Sets the property `padding` */ |
||||
padding?: ResponsiveProp<ThemeSpacingTokens>; |
||||
/** Sets the properties `padding-top` and `padding-bottom`. Higher priority than padding. */ |
||||
paddingX?: ResponsiveProp<ThemeSpacingTokens>; |
||||
/** Sets the properties `padding-left` and `padding-right`. Higher priority than padding. */ |
||||
paddingY?: ResponsiveProp<ThemeSpacingTokens>; |
||||
/** Sets the property `padding-top`. Higher priority than padding and paddingY. */ |
||||
paddingTop?: ResponsiveProp<ThemeSpacingTokens>; |
||||
/** Sets the property `padding-bottom`. Higher priority than padding and paddingY. */ |
||||
paddingBottom?: ResponsiveProp<ThemeSpacingTokens>; |
||||
/** Sets the property `padding-left`. Higher priority than padding and paddingX. */ |
||||
paddingLeft?: ResponsiveProp<ThemeSpacingTokens>; |
||||
/** Sets the property `padding-right`. Higher priority than padding and paddingX. */ |
||||
paddingRight?: ResponsiveProp<ThemeSpacingTokens>; |
||||
|
||||
// Border Props
|
||||
borderStyle?: ResponsiveProp<BorderStyle>; |
||||
borderColor?: ResponsiveProp<BorderColor>; |
||||
borderRadius?: ResponsiveProp<BorderRadius>; |
||||
|
||||
// Flex Props
|
||||
/** Sets the property `flex` */ |
||||
grow?: ResponsiveProp<number>; |
||||
/** Sets the property `flex-shrink` */ |
||||
shrink?: ResponsiveProp<number>; |
||||
alignItems?: ResponsiveProp<AlignItems>; |
||||
justifyContent?: ResponsiveProp<JustifyContent>; |
||||
|
||||
// Other props
|
||||
backgroundColor?: ResponsiveProp<BackgroundColor>; |
||||
display?: ResponsiveProp<Display>; |
||||
boxShadow?: ResponsiveProp<BoxShadow>; |
||||
/** Sets the HTML element that will be rendered as a Box. Defaults to 'div' */ |
||||
element?: ElementType; |
||||
} |
||||
|
||||
export const Box = ({ |
||||
children, |
||||
margin, |
||||
marginX, |
||||
marginY, |
||||
marginTop, |
||||
marginBottom, |
||||
marginLeft, |
||||
marginRight, |
||||
padding, |
||||
paddingX, |
||||
paddingY, |
||||
paddingTop, |
||||
paddingBottom, |
||||
paddingLeft, |
||||
paddingRight, |
||||
display, |
||||
backgroundColor, |
||||
grow, |
||||
shrink, |
||||
borderColor, |
||||
borderStyle, |
||||
borderRadius, |
||||
justifyContent, |
||||
alignItems, |
||||
boxShadow, |
||||
element, |
||||
}: React.PropsWithChildren<BoxProps>) => { |
||||
const styles = useStyles2( |
||||
getStyles, |
||||
margin, |
||||
marginX, |
||||
marginY, |
||||
marginTop, |
||||
marginBottom, |
||||
marginLeft, |
||||
marginRight, |
||||
padding, |
||||
paddingX, |
||||
paddingY, |
||||
paddingTop, |
||||
paddingBottom, |
||||
paddingLeft, |
||||
paddingRight, |
||||
display, |
||||
backgroundColor, |
||||
grow, |
||||
shrink, |
||||
borderColor, |
||||
borderStyle, |
||||
borderRadius, |
||||
justifyContent, |
||||
alignItems, |
||||
boxShadow |
||||
); |
||||
const Element = element ?? 'div'; |
||||
|
||||
return <Element className={styles.root}>{children}</Element>; |
||||
}; |
||||
|
||||
Box.displayName = 'Box'; |
||||
|
||||
const customBorderColor = (color: BorderColor, theme: GrafanaTheme2) => { |
||||
switch (color) { |
||||
case 'error': |
||||
case 'success': |
||||
case 'info': |
||||
case 'warning': |
||||
return theme.colors[color].border; |
||||
default: |
||||
return color ? theme.colors.border[color] : undefined; |
||||
} |
||||
}; |
||||
|
||||
const customBackgroundColor = (color: BackgroundColor, theme: GrafanaTheme2) => { |
||||
switch (color) { |
||||
case 'error': |
||||
case 'success': |
||||
case 'info': |
||||
case 'warning': |
||||
return theme.colors[color].transparent; |
||||
default: |
||||
return color ? theme.colors.background[color] : undefined; |
||||
} |
||||
}; |
||||
|
||||
const getStyles = ( |
||||
theme: GrafanaTheme2, |
||||
margin: BoxProps['margin'], |
||||
marginX: BoxProps['marginX'], |
||||
marginY: BoxProps['marginY'], |
||||
marginTop: BoxProps['marginTop'], |
||||
marginBottom: BoxProps['marginBottom'], |
||||
marginLeft: BoxProps['marginLeft'], |
||||
marginRight: BoxProps['marginRight'], |
||||
padding: BoxProps['padding'], |
||||
paddingX: BoxProps['paddingX'], |
||||
paddingY: BoxProps['paddingY'], |
||||
paddingTop: BoxProps['paddingTop'], |
||||
paddingBottom: BoxProps['paddingBottom'], |
||||
paddingLeft: BoxProps['paddingLeft'], |
||||
paddingRight: BoxProps['paddingRight'], |
||||
display: BoxProps['display'], |
||||
backgroundColor: BoxProps['backgroundColor'], |
||||
grow: BoxProps['grow'], |
||||
shrink: BoxProps['shrink'], |
||||
borderColor: BoxProps['borderColor'], |
||||
borderStyle: BoxProps['borderStyle'], |
||||
borderRadius: BoxProps['borderRadius'], |
||||
justifyContent: BoxProps['justifyContent'], |
||||
alignItems: BoxProps['alignItems'], |
||||
boxShadow: BoxProps['boxShadow'] |
||||
) => { |
||||
return { |
||||
root: css([ |
||||
getResponsiveStyle(theme, margin, (val) => ({ |
||||
margin: theme.spacing(val), |
||||
})), |
||||
getResponsiveStyle(theme, marginX, (val) => ({ |
||||
marginLeft: theme.spacing(val), |
||||
marginRight: theme.spacing(val), |
||||
})), |
||||
getResponsiveStyle(theme, marginY, (val) => ({ |
||||
marginTop: theme.spacing(val), |
||||
marginBottom: theme.spacing(val), |
||||
})), |
||||
getResponsiveStyle(theme, marginTop, (val) => ({ |
||||
marginTop: theme.spacing(val), |
||||
})), |
||||
getResponsiveStyle(theme, marginBottom, (val) => ({ |
||||
marginBottom: theme.spacing(val), |
||||
})), |
||||
getResponsiveStyle(theme, marginLeft, (val) => ({ |
||||
marginLeft: theme.spacing(val), |
||||
})), |
||||
getResponsiveStyle(theme, marginRight, (val) => ({ |
||||
marginRight: theme.spacing(val), |
||||
})), |
||||
getResponsiveStyle(theme, padding, (val) => ({ |
||||
padding: theme.spacing(val), |
||||
})), |
||||
getResponsiveStyle(theme, paddingX, (val) => ({ |
||||
paddingLeft: theme.spacing(val), |
||||
paddingRight: theme.spacing(val), |
||||
})), |
||||
getResponsiveStyle(theme, paddingY, (val) => ({ |
||||
paddingTop: theme.spacing(val), |
||||
paddingBottom: theme.spacing(val), |
||||
})), |
||||
getResponsiveStyle(theme, paddingTop, (val) => ({ |
||||
paddingTop: theme.spacing(val), |
||||
})), |
||||
getResponsiveStyle(theme, paddingBottom, (val) => ({ |
||||
paddingBottom: theme.spacing(val), |
||||
})), |
||||
getResponsiveStyle(theme, paddingLeft, (val) => ({ |
||||
paddingLeft: theme.spacing(val), |
||||
})), |
||||
getResponsiveStyle(theme, paddingRight, (val) => ({ |
||||
paddingRight: theme.spacing(val), |
||||
})), |
||||
getResponsiveStyle(theme, display, (val) => ({ |
||||
display: val, |
||||
})), |
||||
getResponsiveStyle(theme, backgroundColor, (val) => ({ |
||||
backgroundColor: customBackgroundColor(val, theme), |
||||
})), |
||||
getResponsiveStyle(theme, grow, (val) => ({ |
||||
flex: val, |
||||
})), |
||||
getResponsiveStyle(theme, shrink, (val) => ({ |
||||
flexShrink: val, |
||||
})), |
||||
getResponsiveStyle(theme, borderStyle, (val) => ({ |
||||
borderStyle: val, |
||||
})), |
||||
getResponsiveStyle(theme, borderColor, (val) => ({ |
||||
borderColor: customBorderColor(val, theme), |
||||
})), |
||||
(borderStyle || borderColor) && { |
||||
borderWidth: '1px', |
||||
}, |
||||
getResponsiveStyle(theme, justifyContent, (val) => ({ |
||||
justifyContent: val, |
||||
})), |
||||
getResponsiveStyle(theme, alignItems, (val) => ({ |
||||
alignItems: val, |
||||
})), |
||||
getResponsiveStyle(theme, borderRadius, (val) => ({ |
||||
borderRadius: theme.shape.radius[val], |
||||
})), |
||||
getResponsiveStyle(theme, boxShadow, (val) => ({ |
||||
boxShadow: theme.shadows[val], |
||||
})), |
||||
]), |
||||
}; |
||||
}; |
@ -0,0 +1,65 @@ |
||||
import { CSSInterpolation } from '@emotion/css'; |
||||
|
||||
import { GrafanaTheme2, ThemeBreakpointsKey } from '@grafana/data'; |
||||
|
||||
/** |
||||
* Type that represents a prop that can be responsive. |
||||
* |
||||
* @example To turn a prop like `margin: number` responsive, change it to `margin: ResponsiveProp<number>`. |
||||
*/ |
||||
export type ResponsiveProp<T> = T | Responsive<T>; |
||||
|
||||
type Responsive<T> = { |
||||
xs: T; |
||||
sm?: T; |
||||
md?: T; |
||||
lg?: T; |
||||
xl?: T; |
||||
xxl?: T; |
||||
}; |
||||
|
||||
function breakpointCSS<T>( |
||||
theme: GrafanaTheme2, |
||||
prop: Responsive<T>, |
||||
getCSS: (val: T) => CSSInterpolation, |
||||
key: ThemeBreakpointsKey |
||||
) { |
||||
const value = prop[key]; |
||||
if (value !== undefined && value !== null) { |
||||
return { |
||||
[theme.breakpoints.up(key)]: getCSS(value), |
||||
}; |
||||
} |
||||
return; |
||||
} |
||||
/** |
||||
* Function that converts a ResponsiveProp object into CSS |
||||
* |
||||
* @param theme Grafana theme object |
||||
* @param prop Prop as it is passed to the component |
||||
* @param getCSS Function that returns the css block for the prop |
||||
* @returns The CSS block repeated for each breakpoint |
||||
* |
||||
* @example To get the responsive css equivalent of `margin && { margin }`, you can write `getResponsiveStyle(theme, margin, (val) => { margin: val })` |
||||
*/ |
||||
export function getResponsiveStyle<T>( |
||||
theme: GrafanaTheme2, |
||||
prop: ResponsiveProp<T> | undefined, |
||||
getCSS: (val: T) => CSSInterpolation |
||||
): CSSInterpolation { |
||||
if (prop === undefined || prop === null) { |
||||
return null; |
||||
} |
||||
if (typeof prop !== 'object' || !('xs' in prop)) { |
||||
return getCSS(prop); |
||||
} |
||||
|
||||
return [ |
||||
breakpointCSS(theme, prop, getCSS, 'xs'), |
||||
breakpointCSS(theme, prop, getCSS, 'sm'), |
||||
breakpointCSS(theme, prop, getCSS, 'md'), |
||||
breakpointCSS(theme, prop, getCSS, 'lg'), |
||||
breakpointCSS(theme, prop, getCSS, 'xl'), |
||||
breakpointCSS(theme, prop, getCSS, 'xxl'), |
||||
]; |
||||
} |
@ -0,0 +1 @@ |
||||
export const SpacingTokenControl = { control: 'select', options: [0, 0.25, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 8, 10] }; |
Loading…
Reference in new issue