From 890c43adf17fce7764fb32adccf35efdbe71d994 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Thu, 6 Jan 2022 15:48:12 +0000 Subject: [PATCH] UI/Card: Refactor Card component for improved accessibility (#41890) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * UI/Card: Improve accessibility of Card component Co-authored-by: Torkel Ödegaard --- .../src/selectors/pages.ts | 2 + .../grafana-e2e/src/flows/addDataSource.ts | 2 +- .../src/components/Button/Button.story.tsx | 19 +- .../grafana-ui/src/components/Card/Card.mdx | 136 ++++-- .../src/components/Card/Card.story.tsx | 68 +-- .../src/components/Card/Card.test.tsx | 21 +- .../grafana-ui/src/components/Card/Card.tsx | 412 ++++++++++-------- .../src/components/Card/CardContainer.tsx | 65 ++- .../src/components/ThemeDemos/ThemeDemo.tsx | 19 +- public/app/core/components/Card/Card.tsx | 40 -- .../app/features/alerting/AlertRuleItem.tsx | 3 +- .../alerting/unified/RedirectToRuleViewer.tsx | 7 +- .../TransformationsEditor.tsx | 7 +- .../datasources/DataSourceList.test.tsx | 3 +- .../features/datasources/DataSourcesList.tsx | 18 +- .../datasources/NewDataSourcePage.tsx | 128 ++++-- .../features/playlist/PlaylistPageList.tsx | 67 +-- .../features/search/components/SearchItem.tsx | 9 +- .../app/plugins/panel/alertlist/AlertList.tsx | 10 +- 19 files changed, 619 insertions(+), 417 deletions(-) delete mode 100644 public/app/core/components/Card/Card.tsx diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index ec9066362d2..842d47e33c7 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -29,7 +29,9 @@ export const Pages = { }, AddDataSource: { url: '/datasources/new', + /** @deprecated Use dataSourcePluginsV2 */ dataSourcePlugins: (pluginName: string) => `Data source plugin item ${pluginName}`, + dataSourcePluginsV2: (pluginName: string) => `Add data source ${pluginName}`, }, ConfirmModal: { delete: 'Confirm Modal Danger Button', diff --git a/packages/grafana-e2e/src/flows/addDataSource.ts b/packages/grafana-e2e/src/flows/addDataSource.ts index 2522cf16c9a..bb507f12c49 100644 --- a/packages/grafana-e2e/src/flows/addDataSource.ts +++ b/packages/grafana-e2e/src/flows/addDataSource.ts @@ -47,7 +47,7 @@ export const addDataSource = (config?: Partial) => { e2e().logToConsole('Adding data source with name:', name); e2e.pages.AddDataSource.visit(); - e2e.pages.AddDataSource.dataSourcePlugins(type) + e2e.pages.AddDataSource.dataSourcePluginsV2(type) .scrollIntoView() .should('be.visible') // prevents flakiness .click(); diff --git a/packages/grafana-ui/src/components/Button/Button.story.tsx b/packages/grafana-ui/src/components/Button/Button.story.tsx index d6ff3edd9ab..9d70bf1b304 100644 --- a/packages/grafana-ui/src/components/Button/Button.story.tsx +++ b/packages/grafana-ui/src/components/Button/Button.story.tsx @@ -69,18 +69,17 @@ export const Variants: Story = () => { - ))} - - + ))} + diff --git a/packages/grafana-ui/src/components/Card/Card.mdx b/packages/grafana-ui/src/components/Card/Card.mdx index 083f99519bb..e3364418c5a 100644 --- a/packages/grafana-ui/src/components/Card/Card.mdx +++ b/packages/grafana-ui/src/components/Card/Card.mdx @@ -17,11 +17,17 @@ export const logo = 'https://grafana.com/static/assets/img/apple-touch-icon.png' A basic `Card` component expects at least a heading, used as a title. ```jsx - + + Filter by name + Filter data by query. + ``` - + + Filter by name + Filter data by query. + ### Multiple metadata elements @@ -29,13 +35,15 @@ A basic `Card` component expects at least a heading, used as a title. For providing metadata elements, which can be any extra information for the card, `Card.Meta` component should be used. If metadata consists of multiple strings, each of them has to be escaped (wrapped in brackets `{}`) or better passed in as an array. ```jsx - + + Test dashboard {['Folder: Test', 'Views: 100']} ``` - + + Test dashboard {['Folder: Test', 'Views: 100']} @@ -43,7 +51,8 @@ For providing metadata elements, which can be any extra information for the card Metadata also accepts HTML elements, which could be links, for example. For elements, that are not strings, a `key` prop has to be manually specified. ```jsx - + + Test dashboard Grafana @@ -54,7 +63,8 @@ Metadata also accepts HTML elements, which could be links, for example. For elem ``` - + + Test dashboard Grafana @@ -67,7 +77,8 @@ Metadata also accepts HTML elements, which could be links, for example. For elem The separator for multiple metadata elements defaults to a vertical line `|`, but can be customised. ```jsx - + + Test dashboard Grafana @@ -78,7 +89,8 @@ The separator for multiple metadata elements defaults to a vertical line `|`, bu ``` - + + Test dashboard Grafana @@ -93,7 +105,9 @@ The separator for multiple metadata elements defaults to a vertical line `|`, bu Tags can be rendered inside the Card, by being wrapped in `Card.Tags` component. Note that this component does not provide any tag styling and that should be handled by the children. It is recommended to use it with Grafana-UI's `TagList` component. ```jsx - + + Test dashboard + Card with a list of tags console.log(tag)} /> @@ -101,7 +115,9 @@ Tags can be rendered inside the Card, by being wrapped in `Card.Tags` component. ``` - + + Test dashboard + Card with a list of tags console.log(tag)} /> @@ -113,19 +129,17 @@ Tags can be rendered inside the Card, by being wrapped in `Card.Tags` component. Card can be used as a clickable link item by specifying `href` prop. In this case the Card's content will be rendered inside `a`. ```jsx - + + Redirect to Grafana + Clicking this card will redirect to grafana website + ``` - + + Redirect to Grafana + Clicking this card will redirect to grafana website + ### Inside a list item @@ -135,33 +149,57 @@ To render cards in a list, it is possible to nest them inside `li` items. ```jsx
  • - + + List card item + Card that is rendered inside li element. +
  • - + + List card item + Card that is rendered inside li element. +
  • - + + List card item + Card that is rendered inside li element. +
  • - + + List card item + Card that is rendered inside li element. +
``` -
    +
    • - + + List card item + Card that is rendered inside li element. +
    • - + + List card item + Card that is rendered inside li element. +
    • - + + List card item + Card that is rendered inside li element. +
    • - + + List card item + Card that is rendered inside li element. +
    @@ -171,9 +209,10 @@ To render cards in a list, it is possible to nest them inside `li` items. Cards can also be rendered with media content such icons or images. Such elements need to be wrapped in `Card.Figure` component. ```jsx - + + 1-ops-tools1-fallback - Grafana Logo + Grafana Logo Grafana @@ -185,9 +224,10 @@ Cards can also be rendered with media content such icons or images. Such element ``` - + + 1-ops-tools1-fallback - Grafana Logo + Grafana Logo Grafana @@ -203,7 +243,8 @@ Cards can also be rendered with media content such icons or images. Such element Cards also accept primary and secondary actions. Usually the primary actions are displayed as buttons while secondary actions are displayed as icon buttons. The actions need to be wrappd in `Card.Actions` and `Card.SecondaryActions` components respectively. ```jsx - + + 1-ops-tools1-fallback Grafana
    @@ -211,7 +252,7 @@ Cards also accept primary and secondary actions. Usually the primary actions are - Grafana Logo + Grafana Logo @@ -29,7 +34,8 @@ describe('Card', () => { expect(screen.getByRole('button', { name: 'Delete' })).not.toBeDisabled(); rerender( - + + Test Heading @@ -45,7 +51,8 @@ describe('Card', () => { it('Children should be independently enabled or disabled if explicitly set', () => { const { rerender } = render( - + + Test Heading @@ -59,7 +66,8 @@ describe('Card', () => { expect(screen.getByRole('button', { name: 'Delete' })).toBeDisabled(); rerender( - + + Test Heading @@ -76,7 +84,8 @@ describe('Card', () => { it('Children should be conditional', () => { const shouldNotRender = false; render( - + + Test Heading {shouldNotRender && } diff --git a/packages/grafana-ui/src/components/Card/Card.tsx b/packages/grafana-ui/src/components/Card/Card.tsx index 81d19268b76..92466b9315e 100644 --- a/packages/grafana-ui/src/components/Card/Card.tsx +++ b/packages/grafana-ui/src/components/Card/Card.tsx @@ -1,225 +1,230 @@ -import React, { memo, cloneElement, FC, ReactNode } from 'react'; +import React, { memo, cloneElement, FC, useMemo, useContext, ReactNode } from 'react'; import { css, cx } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { useTheme2, stylesFactory } from '../../themes'; -import { CardContainer, CardContainerProps } from './CardContainer'; +import { useStyles2, useTheme2 } from '../../themes'; +import { CardContainer, CardContainerProps, getCardContainerStyles } from './CardContainer'; +import { getFocusStyles } from '../../themes/mixins'; /** * @public */ export interface Props extends Omit { - /** Main heading for the Card **/ - heading: ReactNode; - /** Card description text */ - description?: string; /** Indicates if the card and all its actions can be interacted with */ disabled?: boolean; /** Link to redirect to on card click. If provided, the Card inner content will be rendered inside `a` */ href?: string; /** On click handler for the Card */ onClick?: () => void; + /** @deprecated Use `Card.Heading` instead */ + heading?: ReactNode; + /** @deprecated Use `Card.Description` instead */ + description?: string; } export interface CardInterface extends FC { + Heading: typeof Heading; Tags: typeof Tags; Figure: typeof Figure; Meta: typeof Meta; Actions: typeof Actions; SecondaryActions: typeof SecondaryActions; + Description: typeof Description; } +const CardContext = React.createContext<{ + href?: string; + onClick?: () => void; + disabled?: boolean; +} | null>(null); + /** * Generic card component * * @public */ -export const Card: CardInterface = ({ heading, description, disabled, href, onClick, children, ...htmlProps }) => { - const theme = useTheme2(); - const styles = getCardStyles(theme); - const [tags, figure, meta, actions, secondaryActions] = ['Tags', 'Figure', 'Meta', 'Actions', 'SecondaryActions'].map( - (item) => { - const found = React.Children.toArray(children as React.ReactElement[]).find((child) => { - return React.isValidElement(child) && child?.type && (child.type as any).displayName === item; - }); - - if (found && React.isValidElement(found)) { - return React.cloneElement(found, { disabled, styles, ...found.props }); - } - return found; - } +export const Card: CardInterface = ({ + disabled, + href, + onClick, + children, + heading: deprecatedHeading, + description: deprecatedDescription, + className, + ...htmlProps +}) => { + const hasHeadingComponent = useMemo( + () => + React.Children.toArray(children).some( + (c) => React.isValidElement(c) && (c.type as any).displayName === Heading.displayName + ), + [children] ); - const hasActions = Boolean(actions || secondaryActions); const disableHover = disabled || (!onClick && !href); - const disableEvents = disabled && !actions; const onCardClick = onClick && !disabled ? onClick : undefined; - const onEnterKey = onClick && !disabled ? getEnterKeyHandler(onClick) : undefined; + const theme = useTheme2(); + const styles = getCardContainerStyles(theme, disabled, disableHover); return ( - {figure} -
    -
    -
    -

    {heading}

    - {meta} - {description &&

    {description}

    } -
    - {tags} -
    - {hasActions && ( -
    - {actions} - {secondaryActions} -
    - )} -
    + + {!hasHeadingComponent && } + {deprecatedHeading && {deprecatedHeading}} + {deprecatedDescription && {deprecatedDescription}} + {children} +
    ); }; -function getEnterKeyHandler(onClick: () => void) { - return (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - onClick(); - } - }; +interface ChildProps { + className?: string; + disabled?: boolean; + children?: React.ReactNode; + + /** @deprecated Use `className` to add new styles */ + styles?: ReturnType; } -/** - * @public - */ -export const getCardStyles = stylesFactory((theme: GrafanaTheme2) => { - return { - inner: css` - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - flex-wrap: wrap; - `, - heading: css` - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - margin-bottom: 0; - font-size: ${theme.typography.size.md}; - letter-spacing: inherit; - line-height: ${theme.typography.body.lineHeight}; - color: ${theme.colors.text.primary}; - font-weight: ${theme.typography.fontWeightMedium}; - `, - info: css` - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - width: 100%; - `, - metadata: css` - display: flex; - align-items: center; - width: 100%; - font-size: ${theme.typography.size.sm}; - color: ${theme.colors.text.secondary}; - margin: ${theme.spacing(0.5, 0, 0)}; - line-height: ${theme.typography.bodySmall.lineHeight}; - overflow-wrap: anywhere; - `, - description: css` - width: 100%; - margin: ${theme.spacing(1, 0, 0)}; - color: ${theme.colors.text.secondary}; - line-height: ${theme.typography.body.lineHeight}; - `, - media: css` - margin-right: ${theme.spacing(2)}; - width: 40px; +/** Main heading for the card */ +const Heading = ({ children, className, 'aria-label': ariaLabel }: ChildProps & { 'aria-label'?: string }) => { + const context = useContext(CardContext); + const styles = useStyles2(getHeadingStyles); - & > * { - width: 100%; - } + const { href, onClick } = context ?? { href: undefined, onClick: undefined }; - &:empty { - display: none; - } - `, - actionRow: css` - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - margin-top: ${theme.spacing(2)}; - `, - actions: css` - & > * { - margin-right: ${theme.spacing(1)}; - } - `, - secondaryActions: css` - display: flex; - align-items: center; - color: ${theme.colors.text.secondary}; - // align to the right - margin-left: auto; - & > * { - margin-right: ${theme.spacing(1)} !important; - } - `, - separator: css` - margin: 0 ${theme.spacing(1)}; - `, - tagList: css` - max-width: 50%; - `, - }; + return ( +

    + {href ? ( + + {children} + + ) : onClick ? ( + + ) : ( + <>{children} + )} +

    + ); +}; +Heading.displayName = 'Heading'; + +const getHeadingStyles = (theme: GrafanaTheme2) => ({ + heading: css({ + gridArea: 'Heading', + justifySelf: 'start', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + width: '100%', + marginBottom: 0, + fontSize: theme.typography.size.md, + letterSpacing: 'inherit', + lineHeight: theme.typography.body.lineHeight, + color: theme.colors.text.primary, + fontWeight: theme.typography.fontWeightMedium, + }), + linkHack: css({ + all: 'unset', + '&::after': { + position: 'absolute', + content: '""', + top: 0, + bottom: 0, + left: 0, + right: 0, + borderRadius: theme.shape.borderRadius(1), + }, + + '&:focus-visible': { + outline: 'none', + outlineOffset: 0, + boxShadow: 'none', + + '&::after': { + ...getFocusStyles(theme), + zIndex: 1, + }, + }, + }), }); -interface ChildProps { - styles?: ReturnType; - disabled?: boolean; -} - -const Tags: FC = ({ children, styles }) => { - return
    {children}
    ; +const Tags = ({ children, className }: ChildProps) => { + const styles = useStyles2(getTagStyles); + return
    {children}
    ; }; Tags.displayName = 'Tags'; -const Figure: FC = ({ - children, - styles, - align = 'top', - className, -}) => { +const getTagStyles = (theme: GrafanaTheme2) => ({ + tagList: css({ + position: 'relative', + gridArea: 'Tags', + alignSelf: 'center', + }), +}); + +/** Card description text */ +const Description = ({ children, className }: ChildProps) => { + const styles = useStyles2(getDescriptionStyles); + return

    {children}

    ; +}; +Description.displayName = 'Description'; + +const getDescriptionStyles = (theme: GrafanaTheme2) => ({ + description: css({ + width: '100%', + gridArea: 'Description', + margin: theme.spacing(1, 0, 0), + color: theme.colors.text.secondary, + lineHeight: theme.typography.body.lineHeight, + }), +}); + +const Figure = ({ children, align = 'start', className }: ChildProps & { align?: 'start' | 'center' }) => { + const styles = useStyles2(getFigureStyles); return (
    {children}
    ); }; - Figure.displayName = 'Figure'; -const Meta: FC = memo(({ children, styles, separator = '|' }) => { +const getFigureStyles = (theme: GrafanaTheme2) => ({ + media: css({ + position: 'relative', + gridArea: 'Figure', + + marginRight: theme.spacing(2), + width: '40px', + + '> img': { + width: '100%', + }, + + '&:empty': { + display: 'none', + }, + }), +}); + +const Meta = memo(({ children, className, separator = '|' }: ChildProps & { separator?: string }) => { + const styles = useStyles2(getMetaStyles); let meta = children; // Join meta data elements by separator @@ -230,55 +235,118 @@ const Meta: FC = memo(({ children, styles, } meta = filtered.reduce((prev, curr, i) => [ prev, - + {separator} , curr, ]); } - return
    {meta}
    ; + return
    {meta}
    ; }); - Meta.displayName = 'Meta'; +const getMetaStyles = (theme: GrafanaTheme2) => ({ + metadata: css({ + gridArea: 'Meta', + display: 'flex', + alignItems: 'center', + width: '100%', + fontSize: theme.typography.size.sm, + color: theme.colors.text.secondary, + margin: theme.spacing(0.5, 0, 0), + lineHeight: theme.typography.bodySmall.lineHeight, + overflowWrap: 'anywhere', + }), + separator: css({ + margin: `0 ${theme.spacing(1)}`, + }), +}); + interface ActionsProps extends ChildProps { children?: React.ReactNode; variant?: 'primary' | 'secondary'; } -const BaseActions: FC = ({ children, styles, disabled, variant }) => { - const css = variant === 'primary' ? styles?.actions : styles?.secondaryActions; +const BaseActions = ({ children, disabled, variant, className }: ActionsProps) => { + const styles = useStyles2(getActionStyles); + const context = useContext(CardContext); + const isDisabled = context?.disabled || disabled; + + const css = variant === 'primary' ? styles.actions : styles.secondaryActions; return ( -
    +
    {React.Children.map(children, (child) => { - return React.isValidElement(child) ? cloneElement(child, { disabled, ...child.props }) : null; + return React.isValidElement(child) ? cloneElement(child, { disabled: isDisabled, ...child.props }) : null; })}
    ); }; -const Actions: FC = ({ children, styles, disabled }) => { +const getActionStyles = (theme: GrafanaTheme2) => ({ + actions: css({ + gridArea: 'Actions', + marginTop: theme.spacing(2), + '& > *': { + marginRight: theme.spacing(1), + }, + }), + secondaryActions: css({ + display: 'flex', + gridArea: 'Secondary', + alignSelf: 'center', + color: theme.colors.text.secondary, + marginTtop: theme.spacing(2), + + '& > *': { + marginRight: `${theme.spacing(1)} !important`, + }, + }), +}); + +const Actions = ({ children, disabled, className }: ChildProps) => { return ( - + {children} ); }; - Actions.displayName = 'Actions'; -const SecondaryActions: FC = ({ children, styles, disabled }) => { +const SecondaryActions = ({ children, disabled, className }: ChildProps) => { return ( - + {children} ); }; - SecondaryActions.displayName = 'SecondaryActions'; +/** + * @public + * @deprecated Use `className` on respective components to modify styles + */ +export const getCardStyles = (theme: GrafanaTheme2) => { + return { + inner: css` + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + flex-wrap: wrap; + `, + ...getHeadingStyles(theme), + ...getMetaStyles(theme), + ...getDescriptionStyles(theme), + ...getFigureStyles(theme), + ...getActionStyles(theme), + ...getTagStyles(theme), + }; +}; + +Card.Heading = Heading; Card.Tags = Tags; Card.Figure = Figure; Card.Meta = Meta; Card.Actions = Actions; Card.SecondaryActions = SecondaryActions; +Card.Description = Description; diff --git a/packages/grafana-ui/src/components/Card/CardContainer.tsx b/packages/grafana-ui/src/components/Card/CardContainer.tsx index 2ec56242040..8cfa2e2ae2b 100644 --- a/packages/grafana-ui/src/components/Card/CardContainer.tsx +++ b/packages/grafana-ui/src/components/Card/CardContainer.tsx @@ -1,28 +1,36 @@ -import React, { HTMLAttributes, ReactNode } from 'react'; +import React, { HTMLAttributes } from 'react'; import { css, cx } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { styleMixins, stylesFactory, useTheme2 } from '../../themes'; +import { styleMixins, stylesFactory, useStyles2, useTheme2 } from '../../themes'; /** * @public */ export interface CardInnerProps { href?: string; - children?: ReactNode; + children?: React.ReactNode; } +/** @deprecated This component will be removed in a future release */ const CardInner = ({ children, href }: CardInnerProps) => { - const theme = useTheme2(); - const { inner } = getCardContainerStyles(theme); + const { inner } = useStyles2(getCardInnerStyles); return href ? ( {children} ) : ( -
    {children}
    + <>{children} ); }; +const getCardInnerStyles = (theme: GrafanaTheme2) => ({ + inner: css({ + display: 'flex', + width: '100%', + padding: theme.spacing(2), + }), +}); + /** * @public */ @@ -35,26 +43,58 @@ export interface CardContainerProps extends HTMLAttributes, Ca className?: string; } +/** @deprecated Using `CardContainer` directly is discouraged and should be replaced with `Card` */ export const CardContainer = ({ - href, children, disableEvents, disableHover, className, + href, ...props }: CardContainerProps) => { const theme = useTheme2(); - const { container } = getCardContainerStyles(theme, disableEvents, disableHover); + const { oldContainer } = getCardContainerStyles(theme, disableEvents, disableHover); return ( -
    +
    {children}
    ); }; -const getCardContainerStyles = stylesFactory((theme: GrafanaTheme2, disabled = false, disableHover = false) => { +export const getCardContainerStyles = stylesFactory((theme: GrafanaTheme2, disabled = false, disableHover = false) => { return { container: css({ + display: 'grid', + position: 'relative', + gridTemplateColumns: 'auto 1fr auto', + gridTemplateRows: '1fr auto auto auto', + gridAutoColumns: '1fr', + gridAutoFlow: 'row', + gridTemplateAreas: ` + "Figure Heading Tags" + "Figure Meta Tags" + "Figure Description Tags" + "Figure Actions Secondary"`, + width: '100%', + padding: theme.spacing(2), + background: theme.colors.background.secondary, + borderRadius: theme.shape.borderRadius(), + marginBottom: '8px', + pointerEvents: disabled ? 'none' : 'auto', + transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], { + duration: theme.transitions.duration.short, + }), + + ...(!disableHover && { + '&:hover': { + background: theme.colors.emphasize(theme.colors.background.secondary, 0.03), + cursor: 'pointer', + zIndex: 1, + }, + '&:focus': styleMixins.getFocusStyles(theme), + }), + }), + oldContainer: css({ display: 'flex', width: '100%', background: theme.colors.background.secondary, @@ -75,10 +115,5 @@ const getCardContainerStyles = stylesFactory((theme: GrafanaTheme2, disabled = f '&:focus': styleMixins.getFocusStyles(theme), }), }), - inner: css({ - display: 'flex', - width: '100%', - padding: theme.spacing(2), - }), }; }); diff --git a/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx b/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx index f5e7a172fde..60ba274bd57 100644 --- a/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx +++ b/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.tsx @@ -190,18 +190,17 @@ export const ThemeDemo = () => { Disabled - + + Button inside card - <> - {allButtonVariants.map((variant) => ( - - ))} - - + ))} + diff --git a/public/app/core/components/Card/Card.tsx b/public/app/core/components/Card/Card.tsx deleted file mode 100644 index 007cb289341..00000000000 --- a/public/app/core/components/Card/Card.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { cx } from '@emotion/css'; - -export interface CardProps { - logoUrl?: string; - logoAlt?: string; - title: string; - description?: string; - labels?: React.ReactNode; - actions?: React.ReactNode; - onClick?: () => void; - ariaLabel?: string; - className?: string; -} - -export const Card: React.FC = ({ - logoUrl, - logoAlt, - title, - description, - labels, - actions, - onClick, - ariaLabel, - className, -}) => { - const mainClassName = cx('add-data-source-item', className); - - return ( -
    - {logoUrl && {logoAlt} -
    - {title} - {description && {description}} - {labels &&
    {labels}
    } -
    - {actions &&
    {actions}
    } -
    - ); -}; diff --git a/public/app/features/alerting/AlertRuleItem.tsx b/public/app/features/alerting/AlertRuleItem.tsx index cd170062b05..36d72ac579a 100644 --- a/public/app/features/alerting/AlertRuleItem.tsx +++ b/public/app/features/alerting/AlertRuleItem.tsx @@ -25,7 +25,8 @@ const AlertRuleItem = ({ rule, search, onTogglePause }: Props) => { ); return ( - {renderText(rule.name)}}> + + {renderText(rule.name)} diff --git a/public/app/features/alerting/unified/RedirectToRuleViewer.tsx b/public/app/features/alerting/unified/RedirectToRuleViewer.tsx index 5fe1c32b0da..51c6f21275e 100644 --- a/public/app/features/alerting/unified/RedirectToRuleViewer.tsx +++ b/public/app/features/alerting/unified/RedirectToRuleViewer.tsx @@ -70,11 +70,8 @@ export function RedirectToRuleViewer(props: RedirectToRuleViewerProps): JSX.Elem
    {rules.map((rule, index) => { return ( - + + {rule.name} {`${rule.namespace.name} / ${rule.group.name}`} diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx index b88c5f9aa3e..5450522ce61 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx @@ -375,10 +375,10 @@ function TransformationCard({ transform, onClick }: TransformationCardProps) { return ( + {transform.name} {transform.description} {transform.state && ( @@ -393,10 +393,7 @@ const getStyles = (theme: GrafanaTheme2) => { return { card: css` margin: 0; - - > div { - padding: ${theme.spacing(1)}; - } + padding: ${theme.spacing(1)}; `, }; }; diff --git a/public/app/features/datasources/DataSourceList.test.tsx b/public/app/features/datasources/DataSourceList.test.tsx index c6752e996d1..55c24291bce 100644 --- a/public/app/features/datasources/DataSourceList.test.tsx +++ b/public/app/features/datasources/DataSourceList.test.tsx @@ -23,7 +23,6 @@ describe('DataSourcesList', () => { it('should render all elements in the list item', () => { setup(); expect(screen.getByRole('heading', { name: 'dataSource-0' })).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'dataSource-0 dataSource-0' })).toBeInTheDocument(); - expect(screen.getByAltText('dataSource-0')).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'dataSource-0' })).toBeInTheDocument(); }); }); diff --git a/public/app/features/datasources/DataSourcesList.tsx b/public/app/features/datasources/DataSourcesList.tsx index faee371c5cf..3566177d7c0 100644 --- a/public/app/features/datasources/DataSourcesList.tsx +++ b/public/app/features/datasources/DataSourcesList.tsx @@ -16,12 +16,13 @@ export const DataSourcesList: FC = ({ dataSources, layoutMode }) => { return (
      - {dataSources.map((dataSource, index) => { + {dataSources.map((dataSource) => { return (
    • - + + {dataSource.name} - {dataSource.name} + {[ @@ -42,8 +43,13 @@ export default DataSourcesList; const getStyles = () => { return { - list: css` - list-style: none; - `, + list: css({ + listStyle: 'none', + display: 'grid', + // gap: '8px', Add back when legacy support for old Card interface is dropped + }), + logo: css({ + objectFit: 'contain', + }), }; }; diff --git a/public/app/features/datasources/NewDataSourcePage.tsx b/public/app/features/datasources/NewDataSourcePage.tsx index f07336c124b..21b68754131 100644 --- a/public/app/features/datasources/NewDataSourcePage.tsx +++ b/public/app/features/datasources/NewDataSourcePage.tsx @@ -1,7 +1,8 @@ import React, { FC, PureComponent } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { DataSourcePluginMeta, NavModel } from '@grafana/data'; -import { Button, LinkButton, List, PluginSignatureBadge, FilterInput } from '@grafana/ui'; +import { DataSourcePluginMeta, GrafanaTheme2, NavModel } from '@grafana/data'; +import { Card, LinkButton, List, PluginSignatureBadge, FilterInput, useStyles2 } from '@grafana/ui'; +import { css, cx } from '@emotion/css'; import { selectors } from '@grafana/e2e-selectors'; import Page from 'app/core/components/Page/Page'; @@ -9,7 +10,6 @@ import { StoreState } from 'app/types'; import { addDataSource, loadDataSourcePlugins } from './state/actions'; import { getDataSourcePlugins } from './state/selectors'; import { setDataSourceTypeSearchQuery } from './state/reducers'; -import { Card } from 'app/core/components/Card/Card'; import { PluginsErrorsInfo } from '../plugins/components/PluginsErrorsInfo'; function mapStateToProps(state: StoreState) { @@ -45,7 +45,7 @@ class NewDataSourcePage extends PureComponent { this.props.setDataSourceTypeSearchQuery(value); }; - renderPlugins(plugins: DataSourcePluginMeta[]) { + renderPlugins(plugins: DataSourcePluginMeta[], id?: string) { if (!plugins || !plugins.length) { return null; } @@ -53,6 +53,11 @@ class NewDataSourcePage extends PureComponent { return ( li { + margin-bottom: 2px; + } + `} getItemKey={(item) => item.id.toString()} renderItem={(item) => ( { onLearnMoreClick={this.onLearnMoreClick} /> )} + aria-labelledby={id} /> ); } @@ -76,8 +82,10 @@ class NewDataSourcePage extends PureComponent { <> {categories.map((category) => (
      -
      {category.title}
      - {this.renderPlugins(category.plugins)} +
      + {category.title} +
      + {this.renderPlugins(category.plugins, category.id)}
      ))}
      @@ -131,37 +139,91 @@ const DataSourceTypeCard: FC = (props) => { // find first plugin info link const learnMoreLink = plugin.info?.links?.length > 0 ? plugin.info.links[0] : null; + const styles = useStyles2(getStyles); + return ( - - {learnMoreLink && ( - - {learnMoreLink.name} - - )} - {!isPhantom && } - - } - labels={!isPhantom && } - className={isPhantom ? 'add-data-source-item--phantom' : ''} - onClick={onClick} - aria-label={selectors.pages.AddDataSource.dataSourcePlugins(plugin.name)} - /> + + + {plugin.name} + + + + + {plugin.info.description} + {!isPhantom && ( + + + + )} + + {learnMoreLink && ( + + {learnMoreLink.name} + + )} + + ); }; +function getStyles(theme: GrafanaTheme2) { + return { + heading: css({ + fontSize: theme.v1.typography.heading.h5, + fontWeight: 'inherit', + }), + figure: css({ + width: 'inherit', + marginRight: '0px', + '> img': { + width: theme.spacing(7), + }, + }), + meta: css({ + marginTop: '6px', + position: 'relative', + }), + description: css({ + margin: '0px', + fontSize: theme.typography.size.sm, + }), + actions: css({ + position: 'relative', + alignSelf: 'center', + marginTop: '0px', + opacity: 0, + + '.card-parent:hover &, .card-parent:focus-within &': { + opacity: 1, + }, + }), + card: css({ + gridTemplateAreas: ` + "Figure Heading Actions" + "Figure Description Actions" + "Figure Meta Actions" + "Figure - Actions"`, + }), + logo: css({ + marginRight: theme.v1.spacing.lg, + marginLeft: theme.v1.spacing.sm, + width: theme.spacing(7), + maxHeight: theme.spacing(7), + }), + }; +} + export function getNavModel(): NavModel { const main = { icon: 'database', diff --git a/public/app/features/playlist/PlaylistPageList.tsx b/public/app/features/playlist/PlaylistPageList.tsx index 1bcec873f4a..9c31ac7e4ff 100644 --- a/public/app/features/playlist/PlaylistPageList.tsx +++ b/public/app/features/playlist/PlaylistPageList.tsx @@ -1,7 +1,9 @@ import React from 'react'; import { PlaylistDTO } from './types'; -import { Button, Card, LinkButton } from '@grafana/ui'; +import { Button, Card, LinkButton, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; +import { GrafanaTheme2 } from '@grafana/data'; +import { css } from '@emotion/css'; interface Props { setStartPlaylist: (playlistItem: PlaylistDTO) => void; @@ -10,32 +12,47 @@ interface Props { } export const PlaylistPageList = ({ playlists, setStartPlaylist, setPlaylistToDelete }: Props) => { + const styles = useStyles2(getStyles); return ( - <> +
        {playlists!.map((playlist: PlaylistDTO) => ( - - - - {contextSrv.isEditor && ( - <> - - Edit playlist - - - - )} - - +
      • + + {playlist.name} + + + {contextSrv.isEditor && ( + <> + + Edit playlist + + + + )} + + +
      • ))} - +
      ); }; + +function getStyles(theme: GrafanaTheme2) { + return { + list: css({ + display: 'grid', + }), + listItem: css({ + listStyle: 'none', + }), + }; +} diff --git a/public/app/features/search/components/SearchItem.tsx b/public/app/features/search/components/SearchItem.tsx index 6cf87e9a950..5ba6b4449f8 100644 --- a/public/app/features/search/components/SearchItem.tsx +++ b/public/app/features/search/components/SearchItem.tsx @@ -52,11 +52,11 @@ export const SearchItem: FC = ({ item, editable, onToggleChecked, onTagSe return ( + {item.title} = ({ item, editable, onToggleChecked, onTagSe - + {folderTitle} {item.sortMetaName && ( @@ -88,10 +88,7 @@ const getStyles = (theme: GrafanaTheme2) => { return { container: css` margin-bottom: ${theme.spacing(0.75)}; - - a { - padding: ${theme.spacing(1)} ${theme.spacing(2)}; - } + padding: ${theme.spacing(1)} ${theme.spacing(2)}; `, metaContainer: css` display: flex; diff --git a/public/app/plugins/panel/alertlist/AlertList.tsx b/public/app/plugins/panel/alertlist/AlertList.tsx index 7cef2903a79..b2f6abe28bb 100644 --- a/public/app/plugins/panel/alertlist/AlertList.tsx +++ b/public/app/plugins/panel/alertlist/AlertList.tsx @@ -145,11 +145,8 @@ export function AlertList(props: PanelProps) { currentAlertState.value && currentAlertState.value!.map((alert) => (
    • - + + {alert.name} @@ -166,7 +163,8 @@ export function AlertList(props: PanelProps) { recentStateChanges.value && recentStateChanges.value.map((alert) => (
    • - + + {alert.alertName}