1import React, { HTMLProps } from 'react';
2import { css, cx } from '@emotion/css';
3import { GrafanaTheme } from '@grafana/data';
4import { stylesFactory, useTheme } from '../../themes';
5
6enum Orientation {
7  Horizontal,
8  Vertical,
9}
10type Spacing = 'none' | 'xs' | 'sm' | 'md' | 'lg';
11type Justify = 'flex-start' | 'flex-end' | 'space-between' | 'center';
12type Align = 'normal' | 'flex-start' | 'flex-end' | 'center';
13
14export interface LayoutProps extends Omit<HTMLProps<HTMLDivElement>, 'align' | 'children' | 'wrap'> {
15  children: React.ReactNode[] | React.ReactNode;
16  orientation?: Orientation;
17  spacing?: Spacing;
18  justify?: Justify;
19  align?: Align;
20  width?: string;
21  wrap?: boolean;
22}
23
24export interface ContainerProps {
25  padding?: Spacing;
26  margin?: Spacing;
27  grow?: number;
28  shrink?: number;
29}
30
31export const Layout: React.FC<LayoutProps> = ({
32  children,
33  orientation = Orientation.Horizontal,
34  spacing = 'sm',
35  justify = 'flex-start',
36  align = 'normal',
37  wrap = false,
38  width = '100%',
39  height = '100%',
40  ...rest
41}) => {
42  const theme = useTheme();
43  const styles = getStyles(theme, orientation, spacing, justify, align, wrap);
44  return (
45    <div className={styles.layout} style={{ width, height }} {...rest}>
46      {React.Children.toArray(children)
47        .filter(Boolean)
48        .map((child, index) => {
49          return (
50            <div className={styles.childWrapper} key={index}>
51              {child}
52            </div>
53          );
54        })}
55    </div>
56  );
57};
58
59export const HorizontalGroup: React.FC<Omit<LayoutProps, 'orientation'>> = ({
60  children,
61  spacing,
62  justify,
63  align = 'center',
64  wrap,
65  width,
66  height,
67}) => (
68  <Layout
69    spacing={spacing}
70    justify={justify}
71    orientation={Orientation.Horizontal}
72    align={align}
73    width={width}
74    height={height}
75    wrap={wrap}
76  >
77    {children}
78  </Layout>
79);
80export const VerticalGroup: React.FC<Omit<LayoutProps, 'orientation' | 'wrap'>> = ({
81  children,
82  spacing,
83  justify,
84  align,
85  width,
86  height,
87}) => (
88  <Layout
89    spacing={spacing}
90    justify={justify}
91    orientation={Orientation.Vertical}
92    align={align}
93    width={width}
94    height={height}
95  >
96    {children}
97  </Layout>
98);
99
100export const Container: React.FC<ContainerProps> = ({ children, padding, margin, grow, shrink }) => {
101  const theme = useTheme();
102  const styles = getContainerStyles(theme, padding, margin);
103  return (
104    <div
105      className={cx(
106        styles.wrapper,
107        grow !== undefined &&
108          css`
109            flex-grow: ${grow};
110          `,
111        shrink !== undefined &&
112          css`
113            flex-shrink: ${shrink};
114          `
115      )}
116    >
117      {children}
118    </div>
119  );
120};
121
122const getStyles = stylesFactory(
123  (theme: GrafanaTheme, orientation: Orientation, spacing: Spacing, justify: Justify, align, wrap) => {
124    const finalSpacing = spacing !== 'none' ? theme.spacing[spacing] : 0;
125    // compensate for last row margin when wrapped, horizontal layout
126    const marginCompensation =
127      (orientation === Orientation.Horizontal && !wrap) || orientation === Orientation.Vertical
128        ? 0
129        : `-${finalSpacing}`;
130
131    const label = orientation === Orientation.Vertical ? 'vertical-group' : 'horizontal-group';
132
133    return {
134      layout: css`
135        label: ${label};
136        display: flex;
137        flex-direction: ${orientation === Orientation.Vertical ? 'column' : 'row'};
138        flex-wrap: ${wrap ? 'wrap' : 'nowrap'};
139        justify-content: ${justify};
140        align-items: ${align};
141        height: 100%;
142        max-width: 100%;
143        // compensate for last row margin when wrapped, horizontal layout
144        margin-bottom: ${marginCompensation};
145      `,
146      childWrapper: css`
147        label: layoutChildrenWrapper;
148        margin-bottom: ${orientation === Orientation.Horizontal && !wrap ? 0 : finalSpacing};
149        margin-right: ${orientation === Orientation.Horizontal ? finalSpacing : 0};
150        display: flex;
151        align-items: ${align};
152
153        &:last-child {
154          margin-bottom: ${orientation === Orientation.Vertical && 0};
155          margin-right: ${orientation === Orientation.Horizontal && 0};
156        }
157      `,
158    };
159  }
160);
161
162const getContainerStyles = stylesFactory((theme: GrafanaTheme, padding?: Spacing, margin?: Spacing) => {
163  const paddingSize = (padding && padding !== 'none' && theme.spacing[padding]) || 0;
164  const marginSize = (margin && margin !== 'none' && theme.spacing[margin]) || 0;
165  return {
166    wrapper: css`
167      label: container;
168      margin: ${marginSize};
169      padding: ${paddingSize};
170    `,
171  };
172});
173