From 75b21c7603464779c6b406db66fd7d6eb1a0e18e Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Thu, 26 Sep 2019 20:32:44 +0200 Subject: [PATCH] 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 --- CONTRIBUTING.md | 4 +- .../src/components/BigValue/BigValue.tsx | 70 +++++++--------- .../src/components/Button/AbstractButton.tsx | 13 ++- .../CallToActionCard/CallToActionCard.tsx | 5 +- .../src/components/Collapse/Collapse.tsx | 7 +- .../components/ContextMenu/ContextMenu.tsx | 5 +- .../components/DataLinks/DataLinkEditor.tsx | 29 ++++--- .../components/DataLinks/DataLinkInput.tsx | 31 +++---- .../DataLinks/DataLinkSuggestions.tsx | 5 +- .../ErrorBoundary/ErrorBoundary.tsx | 7 +- .../src/components/Graph/GraphLegendItem.tsx | 73 ++++++++-------- .../src/components/Graph/GraphWithLegend.tsx | 5 +- .../src/components/Legend/LegendList.tsx | 59 ++++++------- .../src/components/List/AbstractList.tsx | 36 ++++---- .../__snapshots__/AbstractList.test.tsx.snap | 16 ++-- packages/grafana-ui/src/themes/index.ts | 2 +- .../src/themes/stylesFactory.test.ts | 31 +++++++ .../grafana-ui/src/themes/stylesFactory.ts | 12 +++ style_guides/styling.md | 84 +++++++++++++++++++ 19 files changed, 310 insertions(+), 184 deletions(-) create mode 100644 packages/grafana-ui/src/themes/stylesFactory.test.ts create mode 100644 packages/grafana-ui/src/themes/stylesFactory.ts create mode 100644 style_guides/styling.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00c2c61588c..28cbda5ab9a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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) diff --git a/packages/grafana-ui/src/components/BigValue/BigValue.tsx b/packages/grafana-ui/src/components/BigValue/BigValue.tsx index e922ad131f0..3ec95605ebe 100644 --- a/packages/grafana-ui/src/components/BigValue/BigValue.tsx +++ b/packages/grafana-ui/src/components/BigValue/BigValue.tsx @@ -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 { render() { const { height, width, value, prefix, suffix, sparkline, backgroundColor, onClick, className } = this.props; - + const styles = getStyles(); return ( -
- {value.title && ( -
- {value.title} -
- )} +
+ {value.title &&
{value.title}
} - + {this.renderText(prefix, '0px 2px 0px 0px')} {this.renderText(value)} {this.renderText(suffix)} diff --git a/packages/grafana-ui/src/components/Button/AbstractButton.tsx b/packages/grafana-ui/src/components/Button/AbstractButton.tsx index 6ce86ac4176..9d401077569 100644 --- a/packages/grafana-ui/src/components/Button/AbstractButton.tsx +++ b/packages/grafana-ui/src/components/Button/AbstractButton.tsx @@ -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 = ({ renderAs, @@ -167,7 +174,7 @@ export const AbstractButton: React.FunctionComponent = ({ children, ...otherProps }) => { - const buttonStyles = getButtonStyles(theme, size, variant, !!icon); + const buttonStyles = getButtonStyles({ theme, size, variant, withIcon: !!icon }); const nonHtmlProps = { theme, size, diff --git a/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx b/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx index 002382762cc..af50edb7c8b 100644 --- a/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx +++ b/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx @@ -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 = ({ message, diff --git a/packages/grafana-ui/src/components/Collapse/Collapse.tsx b/packages/grafana-ui/src/components/Collapse/Collapse.tsx index 0437b1ed5a7..f2e56467dba 100644 --- a/packages/grafana-ui/src/components/Collapse/Collapse.tsx +++ b/packages/grafana-ui/src/components/Collapse/Collapse.tsx @@ -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; diff --git a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx index 4a11ce65a2e..4ab5899496a 100644 --- a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx +++ b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx @@ -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 = React.memo(({ x, y, onClose, items, renderHeader }) => { const theme = useContext(ThemeContext); diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx index d976ef1c234..9d199fe36f1 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx @@ -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 = 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 = 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 ( -
+
= React.memo( `} /> {isLast && ( -
+
With data links you can reference data variables like series name, labels and values. Type CMD+Space, CTRL+Space, or $ to open variable suggestions.
diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx index d6938ac3848..61d2822cba7 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx @@ -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 = ({ value, onChange, suggestions }) => { const editorRef = useRef() as RefObject; const theme = useContext(ThemeContext); + const styles = getStyles(theme); const [showingSuggestions, setShowingSuggestions] = useState(false); const [suggestionsIndex, setSuggestionsIndex] = useState(0); const [linkUrl, setLinkUrl] = useState(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 = ({ value, onChange, s onBlur={onUrlBlur} onKeyDown={(event, _editor, next) => onKeyDown(event as KeyboardEvent, next)} plugins={plugins} - className={getStyles().editor} + className={styles.editor} />
diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkSuggestions.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkSuggestions.tsx index 2b2ee4e6b48..821616d4454 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkSuggestions.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkSuggestions.tsx @@ -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 = ({ suggestions, ...otherProps }) => { const ref = useRef(null); diff --git a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx index 6e5299eadf3..ba0c935767c 100644 --- a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -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 { } } -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 { ); } else { return ( -
+

{title}

{error && error.toString()} diff --git a/packages/grafana-ui/src/components/Graph/GraphLegendItem.tsx b/packages/grafana-ui/src/components/Graph/GraphLegendItem.tsx index 7b6d8ad0ecf..3102e504f8a 100644 --- a/packages/grafana-ui/src/components/Graph/GraphLegendItem.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphLegendItem.tsx @@ -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 ); }; +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 = ({ item, onSeriesColorChange, @@ -64,27 +92,11 @@ export const GraphLegendTableRow: React.FunctionComponent className, }) => { const theme = useContext(ThemeContext); - + const styles = getStyles(theme); return ( - + - + onLabelClick(item, event); } }} - className={css` - cursor: pointer; - white-space: nowrap; - `} + className={styles.label} > - {item.label}{' '} - {item.yAxis === 2 && ( - - (right y-axis) - - )} + {item.label} {item.yAxis === 2 && (right y-axis)}
{item.displayValues && item.displayValues.map((stat, index) => { return ( - + {stat.text} ); diff --git a/packages/grafana-ui/src/components/Graph/GraphWithLegend.tsx b/packages/grafana-ui/src/components/Graph/GraphWithLegend.tsx index e8e33c3873d..b2aaefe9314 100644 --- a/packages/grafana-ui/src/components/Graph/GraphWithLegend.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphWithLegend.tsx @@ -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 = (label: string, option: TOption) => void; export type SeriesColorChangeHandler = SeriesOptionChangeHandler; @@ -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; diff --git a/packages/grafana-ui/src/components/Legend/LegendList.tsx b/packages/grafana-ui/src/components/Legend/LegendList.tsx index f220b10a587..ec5098d0fed 100644 --- a/packages/grafana-ui/src/components/Legend/LegendList.tsx +++ b/packages/grafana-ui/src/components/Legend/LegendList.tsx @@ -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 = ({ items, @@ -12,45 +36,16 @@ export const LegendList: React.FunctionComponent = ({ className, }) => { const theme = useContext(ThemeContext); + const styles = getStyles(theme); const renderItem = (item: LegendItem, index: number) => { - return ( - - {itemRenderer ? itemRenderer(item, index) : item.label} - - ); + return {itemRenderer ? itemRenderer(item, index) : item.label}; }; 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' ? ( -
+
item.yAxis === 1)} renderItem={renderItem} getItemKey={getItemKey} />
diff --git a/packages/grafana-ui/src/components/List/AbstractList.tsx b/packages/grafana-ui/src/components/List/AbstractList.tsx index 643e6cf398d..7d312d933b3 100644 --- a/packages/grafana-ui/src/components/List/AbstractList.tsx +++ b/packages/grafana-ui/src/components/List/AbstractList.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { cx, css } from 'emotion'; +import { stylesFactory } from '../../themes'; export interface ListProps { items: T[]; @@ -12,32 +13,27 @@ interface AbstractListProps extends ListProps { 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 extends React.PureComponent> { constructor(props: AbstractListProps) { 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 (
    {items.map((item, i) => { diff --git a/packages/grafana-ui/src/components/List/__snapshots__/AbstractList.test.tsx.snap b/packages/grafana-ui/src/components/List/__snapshots__/AbstractList.test.tsx.snap index b4c30ebce06..2cb4b48d77b 100644 --- a/packages/grafana-ui/src/components/List/__snapshots__/AbstractList.test.tsx.snap +++ b/packages/grafana-ui/src/components/List/__snapshots__/AbstractList.test.tsx.snap @@ -2,10 +2,10 @@ exports[`AbstractList allows custom item key 1`] = `
    • @@ -18,7 +18,7 @@ exports[`AbstractList allows custom item key 1`] = `
    • @@ -31,7 +31,7 @@ exports[`AbstractList allows custom item key 1`] = `
    • @@ -48,10 +48,10 @@ exports[`AbstractList allows custom item key 1`] = ` exports[`AbstractList renders items using renderItem prop function 1`] = `
      • @@ -64,7 +64,7 @@ exports[`AbstractList renders items using renderItem prop function 1`] = `
      • @@ -77,7 +77,7 @@ exports[`AbstractList renders items using renderItem prop function 1`] = `
      • diff --git a/packages/grafana-ui/src/themes/index.ts b/packages/grafana-ui/src/themes/index.ts index 89b8ab2877d..dedbd347bca 100644 --- a/packages/grafana-ui/src/themes/index.ts +++ b/packages/grafana-ui/src/themes/index.ts @@ -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 }; diff --git a/packages/grafana-ui/src/themes/stylesFactory.test.ts b/packages/grafana-ui/src/themes/stylesFactory.test.ts new file mode 100644 index 00000000000..568a5c39611 --- /dev/null +++ b/packages/grafana-ui/src/themes/stylesFactory.test.ts @@ -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'); + }); +}); diff --git a/packages/grafana-ui/src/themes/stylesFactory.ts b/packages/grafana-ui/src/themes/stylesFactory.ts new file mode 100644 index 00000000000..2a6990840a8 --- /dev/null +++ b/packages/grafana-ui/src/themes/stylesFactory.ts @@ -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 ReturnType>( + stylesCreator: ResultFn +) { + return memoizeOne(stylesCreator); +} diff --git a/style_guides/styling.md b/style_guides/styling.md new file mode 100644 index 00000000000..d476aaa5cf2 --- /dev/null +++ b/style_guides/styling.md @@ -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 ( +
        + As red as you can ge +
        + ); +} +``` + +#### 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 ( +
        + As red as you can ge + +
        + ); +} +``` + +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 = ({ className }) => { + const finalClassName = cx( + className, + css`background: red`, + ) + + return ( +
        + As red as you can ge +
        + ); +} +```