mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
grafana/ui: Selectable Card (#45538)
This commit is contained in:
parent
42e516523f
commit
15d681b823
@ -384,6 +384,28 @@ Card can have a disabled state, effectively making it and its actions non-clicka
|
||||
</Card>
|
||||
</Preview>
|
||||
|
||||
### Selectable
|
||||
|
||||
```jsx
|
||||
<Card isSelected disabled>
|
||||
<Card.Heading>Option #1</Card.Heading>
|
||||
<Card.Meta>This is a really great option, you won't regret it.</Card.Meta>
|
||||
<Card.Figure>
|
||||
<img src={logo} alt="Grafana Logo" width="40" height="40" />
|
||||
</Card.Figure>
|
||||
</Card>
|
||||
```
|
||||
|
||||
<Preview>
|
||||
<Card isSelected disabled>
|
||||
<Card.Heading>Option #1</Card.Heading>
|
||||
<Card.Description>This is a really great option, you won't regret it.</Card.Description>
|
||||
<Card.Figure>
|
||||
<img src={logo} alt="Grafana Logo" width="40" height="40" />
|
||||
</Card.Figure>
|
||||
</Card>
|
||||
</Preview>
|
||||
|
||||
### Props
|
||||
|
||||
<Props of={Card} />
|
||||
|
@ -154,3 +154,27 @@ export const Full: Story<Props> = ({ disabled }) => {
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const Selected: Story<Props> = () => {
|
||||
return (
|
||||
<Card isSelected>
|
||||
<Card.Heading>Spaces</Card.Heading>
|
||||
<Card.Description>Spaces are the superior form of indenting code.</Card.Description>
|
||||
<Card.Figure>
|
||||
<img src={logo} alt="Grafana Logo" width="40" height="40" />
|
||||
</Card.Figure>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const NotSelected: Story<Props> = () => {
|
||||
return (
|
||||
<Card isSelected={false}>
|
||||
<Card.Heading>Tabs</Card.Heading>
|
||||
<Card.Description>Tabs are the preferred way of indentation.</Card.Description>
|
||||
<Card.Figure>
|
||||
<img src={logo} alt="Grafana Logo" width="40" height="40" />
|
||||
</Card.Figure>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
@ -99,5 +99,33 @@ describe('Card', () => {
|
||||
expect(screen.getByRole('button', { name: 'Click Me' })).not.toBeDisabled();
|
||||
expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should allow selectable cards', () => {
|
||||
const { rerender } = render(
|
||||
<Card isSelected={true}>
|
||||
<Card.Heading>My Option</Card.Heading>
|
||||
</Card>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('radio')).toBeInTheDocument();
|
||||
expect(screen.getByRole('radio')).toBeChecked();
|
||||
|
||||
rerender(
|
||||
<Card isSelected={false}>
|
||||
<Card.Heading>My Option</Card.Heading>
|
||||
</Card>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('radio')).toBeInTheDocument();
|
||||
expect(screen.getByRole('radio')).not.toBeChecked();
|
||||
|
||||
rerender(
|
||||
<Card>
|
||||
<Card.Heading>My Option</Card.Heading>
|
||||
</Card>
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('radio')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -19,6 +19,7 @@ export interface Props extends Omit<CardContainerProps, 'disableEvents' | 'disab
|
||||
heading?: ReactNode;
|
||||
/** @deprecated Use `Card.Description` instead */
|
||||
description?: string;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
export interface CardInterface extends FC<Props> {
|
||||
@ -35,6 +36,7 @@ const CardContext = React.createContext<{
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
isSelected?: boolean;
|
||||
} | null>(null);
|
||||
|
||||
/**
|
||||
@ -49,6 +51,7 @@ export const Card: CardInterface = ({
|
||||
children,
|
||||
heading: deprecatedHeading,
|
||||
description: deprecatedDescription,
|
||||
isSelected,
|
||||
className,
|
||||
...htmlProps
|
||||
}) => {
|
||||
@ -63,16 +66,17 @@ export const Card: CardInterface = ({
|
||||
const disableHover = disabled || (!onClick && !href);
|
||||
const onCardClick = onClick && !disabled ? onClick : undefined;
|
||||
const theme = useTheme2();
|
||||
const styles = getCardContainerStyles(theme, disabled, disableHover);
|
||||
const styles = getCardContainerStyles(theme, disabled, disableHover, isSelected);
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
disableEvents={disabled}
|
||||
disableHover={disableHover}
|
||||
isSelected={isSelected}
|
||||
className={cx(styles.container, className)}
|
||||
{...htmlProps}
|
||||
>
|
||||
<CardContext.Provider value={{ href, onClick: onCardClick, disabled }}>
|
||||
<CardContext.Provider value={{ href, onClick: onCardClick, disabled, isSelected }}>
|
||||
{!hasHeadingComponent && <Heading />}
|
||||
{deprecatedHeading && <Heading>{deprecatedHeading}</Heading>}
|
||||
{deprecatedDescription && <Description>{deprecatedDescription}</Description>}
|
||||
@ -96,7 +100,7 @@ const Heading = ({ children, className, 'aria-label': ariaLabel }: ChildProps &
|
||||
const context = useContext(CardContext);
|
||||
const styles = useStyles2(getHeadingStyles);
|
||||
|
||||
const { href, onClick } = context ?? { href: undefined, onClick: undefined };
|
||||
const { href, onClick, isSelected } = context ?? { href: undefined, onClick: undefined, isSelected: undefined };
|
||||
|
||||
return (
|
||||
<h2 className={cx(styles.heading, className)}>
|
||||
@ -111,6 +115,7 @@ const Heading = ({ children, className, 'aria-label': ariaLabel }: ChildProps &
|
||||
) : (
|
||||
<>{children}</>
|
||||
)}
|
||||
{isSelected !== undefined && <input aria-label="option" type="radio" checked={isSelected} />}
|
||||
</h2>
|
||||
);
|
||||
};
|
||||
|
@ -39,6 +39,8 @@ export interface CardContainerProps extends HTMLAttributes<HTMLOrSVGElement>, Ca
|
||||
disableEvents?: boolean;
|
||||
/** No style change on hover */
|
||||
disableHover?: boolean;
|
||||
/** Makes the card selectable, set to "true" to apply selected styles */
|
||||
isSelected?: boolean;
|
||||
/** Custom container styles */
|
||||
className?: string;
|
||||
}
|
||||
@ -48,12 +50,13 @@ export const CardContainer = ({
|
||||
children,
|
||||
disableEvents,
|
||||
disableHover,
|
||||
isSelected,
|
||||
className,
|
||||
href,
|
||||
...props
|
||||
}: CardContainerProps) => {
|
||||
const theme = useTheme2();
|
||||
const { oldContainer } = getCardContainerStyles(theme, disableEvents, disableHover);
|
||||
const { oldContainer } = getCardContainerStyles(theme, disableEvents, disableHover, isSelected);
|
||||
return (
|
||||
<div {...props} className={cx(oldContainer, className)}>
|
||||
<CardInner href={href}>{children}</CardInner>
|
||||
@ -61,59 +64,71 @@ export const CardContainer = ({
|
||||
);
|
||||
};
|
||||
|
||||
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: `
|
||||
export const getCardContainerStyles = stylesFactory(
|
||||
(theme: GrafanaTheme2, disabled = false, disableHover = false, isSelected = false) => {
|
||||
const isSelectable = isSelected !== undefined;
|
||||
|
||||
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,
|
||||
}),
|
||||
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,
|
||||
borderRadius: theme.shape.borderRadius(),
|
||||
position: 'relative',
|
||||
pointerEvents: disabled ? 'none' : 'auto',
|
||||
marginBottom: theme.spacing(1),
|
||||
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),
|
||||
}),
|
||||
|
||||
...(!disableHover && {
|
||||
'&:hover': {
|
||||
background: theme.colors.emphasize(theme.colors.background.secondary, 0.03),
|
||||
...(isSelectable && {
|
||||
cursor: 'pointer',
|
||||
zIndex: 1,
|
||||
},
|
||||
'&:focus': styleMixins.getFocusStyles(theme),
|
||||
}),
|
||||
|
||||
...(isSelected && {
|
||||
outline: `solid 2px ${theme.colors.primary.border}`,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
});
|
||||
oldContainer: css({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
background: theme.colors.background.secondary,
|
||||
borderRadius: theme.shape.borderRadius(),
|
||||
position: 'relative',
|
||||
pointerEvents: disabled ? 'none' : 'auto',
|
||||
marginBottom: theme.spacing(1),
|
||||
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),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user