Docs: Add styling.md with guide to Emotion at Grafana (#19411)

* Add styling.md with guide to emotion

* Update style_guides/styling.md

* Update style_guides/styling.md

* Update style_guides/styling.md

* Update PR guide

* Add stylesFactory helper function

* Simplify styles creator signature

* Make styles factory deps optional

* Update typing

* First batch of updates

* Remove unused import

* Update tests
This commit is contained in:
Dominik Prokop 2019-09-26 20:32:44 +02:00 committed by GitHub
parent a3008ffb05
commit 75b21c7603
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 310 additions and 184 deletions

View File

@ -42,7 +42,7 @@ Whether you are contributing or doing code review, first read and understand htt
### Low-level checks
- [ ] The pull request contains a title that explains it. It follows [PR and commit messages guidelines](#Pull-Requests-titles-and-message).
- [ ] The pull request contains necessary links to issues.
- [ ] The pull request contains necessary links to issues.
- [ ] The pull request contains commits with messages that are small and understandable. It follows [PR and commit messages guidelines](#Pull-Requests-titles-and-message).
- [ ] The pull request does not contain magic strings or numbers that could be replaced with an `Enum` or `const` instead.
@ -58,6 +58,8 @@ Whether you are contributing or doing code review, first read and understand htt
- [ ] The pull request does not contain uses of `any` or `{}` without comments describing why.
- [ ] The pull request does not contain large React components that could easily be split into several smaller components.
- [ ] The pull request does not contain back end calls directly from components, use actions and Redux instead.
- [ ] The pull request follows our [styling with Emotion convention](./style_guides/styling.md)
> We still use a lot of SASS, but any new CSS work should be using or migrating existing code to Emotion
#### Redux specific checks (skip if your pull request does not contain Redux changes)

View File

@ -9,6 +9,7 @@ import { getColorFromHexRgbOrName } from '../../utils';
// Types
import { Themeable } from '../../types';
import { stylesFactory } from '../../themes/stylesFactory';
export interface BigValueSparkline {
data: any[][]; // [[number,number]]
@ -31,6 +32,33 @@ export interface Props extends Themeable {
className?: string;
}
const getStyles = stylesFactory(() => {
return {
wrapper: css`
position: 'relative';
display: 'table';
`,
title: css`
line-height: 1;
text-align: 'center';
z-index: 1;
display: 'block';
width: '100%';
position: 'absolute';
`,
value: css`
line-height: 1;
text-align: 'center';
z-index: 1;
display: 'table-cell';
vertical-align: 'middle';
position: 'relative';
font-size: '3em';
font-weight: 500;
`,
};
});
/*
* This visualization is still in POC state, needed more tests & better structure
*/
@ -122,46 +150,12 @@ export class BigValue extends PureComponent<Props> {
render() {
const { height, width, value, prefix, suffix, sparkline, backgroundColor, onClick, className } = this.props;
const styles = getStyles();
return (
<div
className={cx(
css({
position: 'relative',
display: 'table',
}),
className
)}
style={{ width, height, backgroundColor }}
onClick={onClick}
>
{value.title && (
<div
className={css({
lineHeight: 1,
textAlign: 'center',
zIndex: 1,
display: 'block',
width: '100%',
position: 'absolute',
})}
>
{value.title}
</div>
)}
<div className={cx(styles.wrapper, className)} style={{ width, height, backgroundColor }} onClick={onClick}>
{value.title && <div className={styles.title}>{value.title}</div>}
<span
className={css({
lineHeight: 1,
textAlign: 'center',
zIndex: 1,
display: 'table-cell',
verticalAlign: 'middle',
position: 'relative',
fontSize: '3em',
fontWeight: 500, // TODO: $font-weight-semi-bold
})}
>
<span className={styles.value}>
{this.renderText(prefix, '0px 2px 0px 0px')}
{this.renderText(value)}
{this.renderText(suffix)}

View File

@ -3,6 +3,7 @@ import tinycolor from 'tinycolor2';
import { css, cx } from 'emotion';
import { Themeable, GrafanaTheme } from '../../types';
import { selectThemeVariant } from '../../themes/selectThemeVariant';
import { stylesFactory } from '../../themes/stylesFactory';
export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'inverse' | 'transparent';
@ -49,7 +50,13 @@ const buttonVariantStyles = (
}
`;
const getButtonStyles = (theme: GrafanaTheme, size: ButtonSize, variant: ButtonVariant, withIcon: boolean) => {
interface StyleDeps {
theme: GrafanaTheme;
size: ButtonSize;
variant: ButtonVariant;
withIcon: boolean;
}
const getButtonStyles = stylesFactory(({ theme, size, variant, withIcon }: StyleDeps) => {
const borderRadius = theme.border.radius.sm;
let padding,
background,
@ -155,7 +162,7 @@ const getButtonStyles = (theme: GrafanaTheme, size: ButtonSize, variant: ButtonV
filter: brightness(100);
`,
};
};
});
export const AbstractButton: React.FunctionComponent<AbstractButtonProps> = ({
renderAs,
@ -167,7 +174,7 @@ export const AbstractButton: React.FunctionComponent<AbstractButtonProps> = ({
children,
...otherProps
}) => {
const buttonStyles = getButtonStyles(theme, size, variant, !!icon);
const buttonStyles = getButtonStyles({ theme, size, variant, withIcon: !!icon });
const nonHtmlProps = {
theme,
size,

View File

@ -2,6 +2,7 @@ import React from 'react';
import { Themeable, GrafanaTheme } from '../../types/theme';
import { selectThemeVariant } from '../../themes/selectThemeVariant';
import { css, cx } from 'emotion';
import { stylesFactory } from '../../themes';
export interface CallToActionCardProps extends Themeable {
message?: string | JSX.Element;
@ -10,7 +11,7 @@ export interface CallToActionCardProps extends Themeable {
className?: string;
}
const getCallToActionCardStyles = (theme: GrafanaTheme) => ({
const getCallToActionCardStyles = stylesFactory((theme: GrafanaTheme) => ({
wrapper: css`
label: call-to-action-card;
padding: ${theme.spacing.lg};
@ -28,7 +29,7 @@ const getCallToActionCardStyles = (theme: GrafanaTheme) => ({
footer: css`
margin-top: ${theme.spacing.lg};
`,
});
}));
export const CallToActionCard: React.FunctionComponent<CallToActionCardProps> = ({
message,

View File

@ -3,9 +3,10 @@ import { css, cx } from 'emotion';
import { GrafanaTheme } from '../../types/theme';
import { selectThemeVariant } from '../../themes/selectThemeVariant';
import { ThemeContext } from '../../themes/index';
import { ThemeContext } from '../../themes/ThemeContext';
import { stylesFactory } from '../../themes/stylesFactory';
const getStyles = (theme: GrafanaTheme) => ({
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
collapse: css`
label: collapse;
margin-top: ${theme.spacing.sm};
@ -79,7 +80,7 @@ const getStyles = (theme: GrafanaTheme) => ({
font-size: ${theme.typography.heading.h6};
box-shadow: ${selectThemeVariant({ light: 'none', dark: '1px 1px 4px rgb(45, 45, 45)' }, theme.type)};
`,
});
}));
interface Props {
isOpen: boolean;

View File

@ -2,6 +2,7 @@ import React, { useContext, useRef } from 'react';
import { css, cx } from 'emotion';
import useClickAway from 'react-use/lib/useClickAway';
import { GrafanaTheme, selectThemeVariant, ThemeContext } from '../../index';
import { stylesFactory } from '../../themes/stylesFactory';
import { Portal, List } from '../index';
import { LinkTarget } from '@grafana/data';
@ -26,7 +27,7 @@ export interface ContextMenuProps {
renderHeader?: () => JSX.Element;
}
const getContextMenuStyles = (theme: GrafanaTheme) => {
const getContextMenuStyles = stylesFactory((theme: GrafanaTheme) => {
const linkColor = selectThemeVariant(
{
light: theme.colors.dark2,
@ -146,7 +147,7 @@ const getContextMenuStyles = (theme: GrafanaTheme) => {
top: 4px;
`,
};
};
});
export const ContextMenu: React.FC<ContextMenuProps> = React.memo(({ x, y, onClose, items, renderHeader }) => {
const theme = useContext(ThemeContext);

View File

@ -3,8 +3,9 @@ import { DataLink } from '@grafana/data';
import { FormField, Switch } from '../index';
import { VariableSuggestion } from './DataLinkSuggestions';
import { css } from 'emotion';
import { ThemeContext } from '../../themes/index';
import { ThemeContext, stylesFactory } from '../../themes/index';
import { DataLinkInput } from './DataLinkInput';
import { GrafanaTheme } from '../../types';
interface DataLinkEditorProps {
index: number;
@ -15,9 +16,21 @@ interface DataLinkEditorProps {
onRemove: (link: DataLink) => void;
}
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
listItem: css`
margin-bottom: ${theme.spacing.sm};
`,
infoText: css`
padding-bottom: ${theme.spacing.md};
margin-left: 66px;
color: ${theme.colors.textWeak};
`,
}));
export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
({ index, value, onChange, onRemove, suggestions, isLast }) => {
const theme = useContext(ThemeContext);
const styles = getStyles(theme);
const [title, setTitle] = useState(value.title);
const onUrlChange = (url: string, callback?: () => void) => {
@ -39,18 +52,8 @@ export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
onChange(index, { ...value, targetBlank: !value.targetBlank });
};
const listItemStyle = css`
margin-bottom: ${theme.spacing.sm};
`;
const infoTextStyle = css`
padding-bottom: ${theme.spacing.md};
margin-left: 66px;
color: ${theme.colors.textWeak};
`;
return (
<div className={listItemStyle}>
<div className={styles.listItem}>
<div className="gf-form gf-form--inline">
<FormField
className="gf-form--grow"
@ -76,7 +79,7 @@ export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
`}
/>
{isLast && (
<div className={infoTextStyle}>
<div className={styles.infoText}>
With data links you can reference data variables like series name, labels and values. Type CMD+Space,
CTRL+Space, or $ to open variable suggestions.
</div>

View File

@ -1,4 +1,4 @@
import React, { useState, useMemo, useCallback, useContext, useRef, RefObject } from 'react';
import React, { useState, useMemo, useContext, useRef, RefObject } from 'react';
import { VariableSuggestion, VariableOrigin, DataLinkSuggestions } from './DataLinkSuggestions';
import { ThemeContext, DataLinkBuiltInVars, makeValue } from '../../index';
import { SelectionReference } from './SelectionReference';
@ -12,6 +12,8 @@ import { css, cx } from 'emotion';
import { SlatePrism } from '../../slate-plugins';
import { SCHEMA } from '../../utils/slate';
import { stylesFactory } from '../../themes';
import { GrafanaTheme } from '../../types';
const modulo = (a: number, n: number) => a - n * Math.floor(a / n);
@ -28,26 +30,25 @@ const plugins = [
}),
];
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
editor: css`
.token.builtInVariable {
color: ${theme.colors.queryGreen};
}
.token.variable {
color: ${theme.colors.queryKeyword};
}
`,
}));
export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, suggestions }) => {
const editorRef = useRef<Editor>() as RefObject<Editor>;
const theme = useContext(ThemeContext);
const styles = getStyles(theme);
const [showingSuggestions, setShowingSuggestions] = useState(false);
const [suggestionsIndex, setSuggestionsIndex] = useState(0);
const [linkUrl, setLinkUrl] = useState<Value>(makeValue(value));
const getStyles = useCallback(() => {
return {
editor: css`
.token.builtInVariable {
color: ${theme.colors.queryGreen};
}
.token.variable {
color: ${theme.colors.queryKeyword};
}
`,
};
}, [theme]);
// Workaround for https://github.com/ianstormtaylor/slate/issues/2927
const stateRef = useRef({ showingSuggestions, suggestions, suggestionsIndex, linkUrl, onChange });
stateRef.current = { showingSuggestions, suggestions, suggestionsIndex, linkUrl, onChange };
@ -155,7 +156,7 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, s
onBlur={onUrlBlur}
onKeyDown={(event, _editor, next) => onKeyDown(event as KeyboardEvent, next)}
plugins={plugins}
className={getStyles().editor}
className={styles.editor}
/>
</div>
</div>

View File

@ -5,6 +5,7 @@ import React, { useRef, useContext, useMemo } from 'react';
import useClickAway from 'react-use/lib/useClickAway';
import { List } from '../index';
import tinycolor from 'tinycolor2';
import { stylesFactory } from '../../themes';
export enum VariableOrigin {
Series = 'series',
@ -28,7 +29,7 @@ interface DataLinkSuggestionsProps {
onClose?: () => void;
}
const getStyles = (theme: GrafanaTheme) => {
const getStyles = stylesFactory((theme: GrafanaTheme) => {
const wrapperBg = selectThemeVariant(
{
light: theme.colors.white,
@ -129,7 +130,7 @@ const getStyles = (theme: GrafanaTheme) => {
color: ${itemDocsColor};
`,
};
};
});
export const DataLinkSuggestions: React.FC<DataLinkSuggestionsProps> = ({ suggestions, ...otherProps }) => {
const ref = useRef(null);

View File

@ -1,6 +1,7 @@
import React, { PureComponent, ReactNode } from 'react';
import { Alert } from '../Alert/Alert';
import { css } from 'emotion';
import { stylesFactory } from '../../themes';
interface ErrorInfo {
componentStack: string;
@ -44,12 +45,12 @@ export class ErrorBoundary extends PureComponent<Props, State> {
}
}
function getAlertPageStyle() {
const getStyles = stylesFactory(() => {
return css`
width: 500px;
margin: 64px auto;
`;
}
});
interface WithAlertBoxProps {
title?: string;
@ -85,7 +86,7 @@ export class ErrorBoundaryAlert extends PureComponent<WithAlertBoxProps> {
);
} else {
return (
<div className={getAlertPageStyle()}>
<div className={getStyles()}>
<h2>{title}</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{error && error.toString()}

View File

@ -5,6 +5,8 @@ import { LegendItem } from '../Legend/Legend';
import { SeriesColorChangeHandler } from './GraphWithLegend';
import { LegendStatsList } from '../Legend/LegendStatsList';
import { ThemeContext } from '../../themes/ThemeContext';
import { stylesFactory } from '../../themes';
import { GrafanaTheme } from '../../types';
export interface GraphLegendItemProps {
key?: React.Key;
@ -56,6 +58,32 @@ export const GraphLegendListItem: React.FunctionComponent<GraphLegendItemProps>
);
};
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
row: css`
font-size: ${theme.typography.size.sm};
td {
padding: ${theme.spacing.xxs} ${theme.spacing.sm};
white-space: nowrap;
}
`,
label: css`
cursor: pointer;
white-space: nowrap;
`,
itemWrapper: css`
display: flex;
white-space: nowrap;
`,
value: css`
text-align: right;
`,
yAxisLabel: css`
color: ${theme.colors.gray2};
`,
};
});
export const GraphLegendTableRow: React.FunctionComponent<GraphLegendItemProps> = ({
item,
onSeriesColorChange,
@ -64,27 +92,11 @@ export const GraphLegendTableRow: React.FunctionComponent<GraphLegendItemProps>
className,
}) => {
const theme = useContext(ThemeContext);
const styles = getStyles(theme);
return (
<tr
className={cx(
css`
font-size: ${theme.typography.size.sm};
td {
padding: ${theme.spacing.xxs} ${theme.spacing.sm};
white-space: nowrap;
}
`,
className
)}
>
<tr className={cx(styles.row, className)}>
<td>
<span
className={css`
display: flex;
white-space: nowrap;
`}
>
<span className={styles.itemWrapper}>
<LegendSeriesIcon
disabled={!!onSeriesColorChange}
color={item.color}
@ -102,33 +114,16 @@ export const GraphLegendTableRow: React.FunctionComponent<GraphLegendItemProps>
onLabelClick(item, event);
}
}}
className={css`
cursor: pointer;
white-space: nowrap;
`}
className={styles.label}
>
{item.label}{' '}
{item.yAxis === 2 && (
<span
className={css`
color: ${theme.colors.gray2};
`}
>
(right y-axis)
</span>
)}
{item.label} {item.yAxis === 2 && <span className={styles.yAxisLabel}>(right y-axis)</span>}
</div>
</span>
</td>
{item.displayValues &&
item.displayValues.map((stat, index) => {
return (
<td
className={css`
text-align: right;
`}
key={`${stat.title}-${index}`}
>
<td className={styles.value} key={`${stat.title}-${index}`}>
{stat.text}
</td>
);

View File

@ -8,6 +8,7 @@ import { Graph, GraphProps } from './Graph';
import { LegendRenderOptions, LegendItem, LegendDisplayMode } from '../Legend/Legend';
import { GraphLegend } from './GraphLegend';
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
import { stylesFactory } from '../../themes';
export type SeriesOptionChangeHandler<TOption> = (label: string, option: TOption) => void;
export type SeriesColorChangeHandler = SeriesOptionChangeHandler<string>;
@ -24,7 +25,7 @@ export interface GraphWithLegendProps extends GraphProps, LegendRenderOptions {
onToggleSort: (sortBy: string) => void;
}
const getGraphWithLegendStyles = ({ placement }: GraphWithLegendProps) => ({
const getGraphWithLegendStyles = stylesFactory(({ placement }: GraphWithLegendProps) => ({
wrapper: css`
display: flex;
flex-direction: ${placement === 'under' ? 'column' : 'row'};
@ -38,7 +39,7 @@ const getGraphWithLegendStyles = ({ placement }: GraphWithLegendProps) => ({
padding: 10px 0;
max-height: ${placement === 'under' ? '35%' : 'none'};
`,
});
}));
const shouldHideLegendItem = (data: GraphSeriesValue[][], hideEmpty = false, hideZero = false) => {
const isZeroOnlySeries = data.reduce((acc, current) => acc + (current[1] || 0), 0) === 0;

View File

@ -4,6 +4,30 @@ import { InlineList } from '../List/InlineList';
import { List } from '../List/List';
import { css, cx } from 'emotion';
import { ThemeContext } from '../../themes/ThemeContext';
import { stylesFactory } from '../../themes';
import { GrafanaTheme } from '../../types';
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
item: css`
padding-left: 10px;
display: flex;
font-size: ${theme.typography.size.sm};
white-space: nowrap;
`,
wrapper: css`
display: flex;
flex-wrap: wrap;
justify-content: space-between;
width: 100%;
`,
section: css`
display: flex;
`,
sectionRight: css`
justify-content: flex-end;
flex-grow: 1;
`,
}));
export const LegendList: React.FunctionComponent<LegendComponentProps> = ({
items,
@ -12,45 +36,16 @@ export const LegendList: React.FunctionComponent<LegendComponentProps> = ({
className,
}) => {
const theme = useContext(ThemeContext);
const styles = getStyles(theme);
const renderItem = (item: LegendItem, index: number) => {
return (
<span
className={css`
padding-left: 10px;
display: flex;
font-size: ${theme.typography.size.sm};
white-space: nowrap;
`}
>
{itemRenderer ? itemRenderer(item, index) : item.label}
</span>
);
return <span className={styles.item}>{itemRenderer ? itemRenderer(item, index) : item.label}</span>;
};
const getItemKey = (item: LegendItem) => `${item.label}`;
const styles = {
wrapper: cx(
css`
display: flex;
flex-wrap: wrap;
justify-content: space-between;
width: 100%;
`,
className
),
section: css`
display: flex;
`,
sectionRight: css`
justify-content: flex-end;
flex-grow: 1;
`,
};
return placement === 'under' ? (
<div className={styles.wrapper}>
<div className={cx(styles.wrapper, className)}>
<div className={styles.section}>
<InlineList items={items.filter(item => item.yAxis === 1)} renderItem={renderItem} getItemKey={getItemKey} />
</div>

View File

@ -1,5 +1,6 @@
import React from 'react';
import { cx, css } from 'emotion';
import { stylesFactory } from '../../themes';
export interface ListProps<T> {
items: T[];
@ -12,32 +13,27 @@ interface AbstractListProps<T> extends ListProps<T> {
inline?: boolean;
}
const getStyles = stylesFactory((inlineList = false) => ({
list: css`
list-style-type: none;
margin: 0;
padding: 0;
`,
item: css`
display: ${(inlineList && 'inline-block') || 'block'};
`,
}));
export class AbstractList<T> extends React.PureComponent<AbstractListProps<T>> {
constructor(props: AbstractListProps<T>) {
super(props);
this.getListStyles = this.getListStyles.bind(this);
}
getListStyles() {
const { inline, className } = this.props;
return {
list: cx([
css`
list-style-type: none;
margin: 0;
padding: 0;
`,
className,
]),
item: css`
display: ${(inline && 'inline-block') || 'block'};
`,
};
}
render() {
const { items, renderItem, getItemKey, className } = this.props;
const styles = this.getListStyles();
const { items, renderItem, getItemKey, className, inline } = this.props;
const styles = getStyles(inline);
return (
<ul className={cx(styles.list, className)}>
{items.map((item, i) => {

View File

@ -2,10 +2,10 @@
exports[`AbstractList allows custom item key 1`] = `
<ul
className="css-9xf0yn"
className="css-1ld8h5b"
>
<li
className="css-rwbibe"
className="css-8qpjjf"
key="item1"
>
<div>
@ -18,7 +18,7 @@ exports[`AbstractList allows custom item key 1`] = `
</div>
</li>
<li
className="css-rwbibe"
className="css-8qpjjf"
key="item2"
>
<div>
@ -31,7 +31,7 @@ exports[`AbstractList allows custom item key 1`] = `
</div>
</li>
<li
className="css-rwbibe"
className="css-8qpjjf"
key="item3"
>
<div>
@ -48,10 +48,10 @@ exports[`AbstractList allows custom item key 1`] = `
exports[`AbstractList renders items using renderItem prop function 1`] = `
<ul
className="css-9xf0yn"
className="css-1ld8h5b"
>
<li
className="css-rwbibe"
className="css-8qpjjf"
key="0"
>
<div>
@ -64,7 +64,7 @@ exports[`AbstractList renders items using renderItem prop function 1`] = `
</div>
</li>
<li
className="css-rwbibe"
className="css-8qpjjf"
key="1"
>
<div>
@ -77,7 +77,7 @@ exports[`AbstractList renders items using renderItem prop function 1`] = `
</div>
</li>
<li
className="css-rwbibe"
className="css-8qpjjf"
key="2"
>
<div>

View File

@ -1,5 +1,5 @@
import { ThemeContext, withTheme, useTheme } from './ThemeContext';
import { getTheme, mockTheme } from './getTheme';
import { selectThemeVariant } from './selectThemeVariant';
export { stylesFactory } from './stylesFactory';
export { ThemeContext, withTheme, mockTheme, getTheme, selectThemeVariant, useTheme };

View File

@ -0,0 +1,31 @@
import { stylesFactory } from './stylesFactory';
interface FakeProps {
theme: {
a: string;
};
}
describe('Stylesheet creation', () => {
it('memoizes results', () => {
const spy = jest.fn();
const getStyles = stylesFactory(({ theme }: FakeProps) => {
spy();
return {
className: `someClass${theme.a}`,
};
});
const props: FakeProps = { theme: { a: '-interpolated' } };
const changedProps: FakeProps = { theme: { a: '-interpolatedChanged' } };
const styles = getStyles(props);
getStyles(props);
expect(spy).toBeCalledTimes(1);
expect(styles.className).toBe('someClass-interpolated');
const styles2 = getStyles(changedProps);
expect(spy).toBeCalledTimes(2);
expect(styles2.className).toBe('someClass-interpolatedChanged');
});
});

View File

@ -0,0 +1,12 @@
import memoizeOne from 'memoize-one';
// import { KeyValue } from '@grafana/data';
/**
* Creates memoized version of styles creator
* @param stylesCreator function accepting dependencies based on which styles are created
*/
export function stylesFactory<ResultFn extends (this: any, ...newArgs: any[]) => ReturnType<ResultFn>>(
stylesCreator: ResultFn
) {
return memoizeOne(stylesCreator);
}

84
style_guides/styling.md Normal file
View File

@ -0,0 +1,84 @@
# Styling Grafana
## Emotion
[Emotion](https://emotion.sh/docs/introduction) is our default-to-be approach to styling React components. It provides a way for styles to be a consequence of properties and state of a component.
### Usage
#### Basic styling
For styling components use Emotion's `css` function
```tsx
import { css } from 'emotion';
const ComponentA = () => {
return (
<div className={css`background: red;`}>
As red as you can ge
</div>
);
}
```
#### Styling complex components
In more complex cases, especially when you need to style multiple DOM elements in one component or when your styles that depend on properties and/or state, you should create a helper function that returns an object with desired stylesheet. Let's say you need to style a component that has different background depending on the theme:
```tsx
import { css, cx } from 'emotion';
import { GrafanaTheme, useTheme, selectThemeVariant } from '@grafana/ui';
const getStyles = (theme: GrafanaTheme) => {
const backgroundColor = selectThemeVariant({ light: theme.colors.red, dark: theme.colors.blue }, theme.type);
return {
wrapper: css`
background: ${backgroundColor};
`,
icon: css`font-size:${theme.typography.size.sm}`;
};
}
const ComponentA = () => {
const theme = useTheme();
const styles = getStyles(theme);
return (
<div className={styles.wrapper}>
As red as you can ge
<i className={styles.icon} /\>
</div>
);
}
```
For more information about themes at Grafana please see [themes guide](./themes.md)
#### Composing class names
For class composition use Emotion's `cx` function
```tsx
import { css, cx } from 'emotion';
interface Props {
className?: string;
}
const ComponentA: React.FC<Props> = ({ className }) => {
const finalClassName = cx(
className,
css`background: red`,
)
return (
<div className={finalClassName}>
As red as you can ge
</div>
);
}
```