Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Design System: Refactor IconButton and update documentation #66774

Merged
merged 27 commits into from May 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c151b6b
refactor: remove unnecessary styling and adjust to button styling
L-M-K-B Apr 18, 2023
4edaf78
refactor: improve story
L-M-K-B Apr 18, 2023
c584610
refactor: use new default styling for border radius
L-M-K-B Apr 18, 2023
4acfb78
refactor: add missing pseudo classes and clean up
L-M-K-B Apr 18, 2023
19aba85
refactor: repair disabled styling and add to story
L-M-K-B Apr 18, 2023
2378f13
refactor: clean up and apply styles defined in figma
L-M-K-B Apr 20, 2023
890f9b3
refactor: set hover background in a pseudo-element
L-M-K-B Apr 20, 2023
f5bb0cc
refactor: unify large sizes
L-M-K-B Apr 25, 2023
d640c00
Merge branch 'main' into laura/unify-IconButton-styling-with-Button
L-M-K-B Apr 25, 2023
6bcae65
Merge branch 'main' into laura/unify-IconButton-styling-with-Button
L-M-K-B May 2, 2023
17e23e2
refactor: add information for further use
L-M-K-B May 2, 2023
e618957
refactor: add comment
L-M-K-B May 3, 2023
89278eb
refactor: comments and clean up import
L-M-K-B May 3, 2023
40550c2
refactor: add changes after code review
L-M-K-B May 4, 2023
55ca13a
refactor: replace some bad example code in documentation
L-M-K-B May 4, 2023
a08eff5
refactor: update comment
L-M-K-B May 4, 2023
27c1ede
refactor: add changes requested in review
L-M-K-B May 5, 2023
d6f124c
refactor: move deprecation warning
L-M-K-B May 5, 2023
1923605
Merge branch 'main' into laura/unify-IconButton-styling-with-Button
L-M-K-B May 10, 2023
f4275d3
refactor: replace padding
L-M-K-B May 11, 2023
cf28f1f
refactor: remove local styling
L-M-K-B May 11, 2023
2f7ce5a
Merge main branch into feat branch
L-M-K-B May 11, 2023
8e8f175
Merge branch 'main' into laura/unify-IconButton-styling-with-Button
L-M-K-B May 12, 2023
20a5c35
refactor: create separate stories for different examples
L-M-K-B May 12, 2023
9ea630a
refactor: change style of story
L-M-K-B May 12, 2023
f77eabc
refactor: replace absolute value by variable
L-M-K-B May 12, 2023
024e57c
Update toggles_gen.go
L-M-K-B May 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/grafana-ui/src/components/Button/Button.mdx
@@ -1,10 +1,17 @@
import { Meta, Preview, ArgTypes } from '@storybook/blocks';
import { Button, LinkButton } from './Button';
import { Alert } from '../Alert/Alert';

<Meta title="MDX|Button" component={Button} />

# Button

<Alert severity="warning" title={'Please note:'}>
After reviewing this component we are asking you to use the IconButton when you require a button with only an icon.
</Alert>

When using a button please always follow a11y rules (e.g. W3C Recommendation [3.3.2 Labels or instructions](https://www.w3.org/TR/WCAG21/#labels-or-instructions)) and make sure the context in which the button is located is also communicated by screen readers.

## Primary

Used for "call to action", i.e. triggering the main action. There should never be more than one on a page. If you need multiple buttons for different actions, decide which action is the most important and make that the primary `Button`. All other `Button` components should be secondary.
Expand Down
@@ -1,7 +1,7 @@
import { StoryFn } from '@storybook/react';
import React from 'react';

import { ComponentSize } from '../../types/size';
import { ComponentSize } from '../../types';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { Card } from '../Card/Card';
import { HorizontalGroup, VerticalGroup } from '../Layout/Layout';
Expand Down Expand Up @@ -69,12 +69,6 @@ export const Examples: StoryFn<typeof Button> = () => {
</Button>
</HorizontalGroup>
<div />
<HorizontalGroup spacing="lg">
<div>With icon only</div>
<Button icon="cloud" size="sm" />
<Button icon="cloud" size="md" />
<Button icon="cloud" size="lg" />
</HorizontalGroup>
<div />
<Button icon="plus" fullWidth>
Button with fullWidth
Expand Down
5 changes: 3 additions & 2 deletions packages/grafana-ui/src/components/Button/Button.tsx
Expand Up @@ -5,8 +5,8 @@ import { colorManipulator, GrafanaTheme2, ThemeRichColor } from '@grafana/data';

import { useTheme2 } from '../../themes';
import { getFocusStyles, getMouseFocusStyles } from '../../themes/mixins';
import { ComponentSize } from '../../types';
import { IconName } from '../../types/icon';
import { ComponentSize } from '../../types/size';
import { getPropertiesForButtonSize } from '../Forms/commonStyles';
import { Icon } from '../Icon/Icon';
import { PopoverContent, Tooltip, TooltipPlacement } from '../Tooltip';
Expand Down Expand Up @@ -60,6 +60,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
iconOnly: !children,
});

// In order to standardise Button please always consider using IconButton when you need a button with an icon only
const button = (
<button className={cx(styles.button, className)} type={type} {...otherProps} ref={ref}>
{icon && <Icon name={icon} size={size} className={styles.icon} />}
Expand Down Expand Up @@ -175,7 +176,7 @@ export const getButtonStyles = (props: StyleProps) => {
lineHeight: `${theme.spacing.gridSize * height - 2}px`,
verticalAlign: 'middle',
cursor: 'pointer',
borderRadius: theme.shape.borderRadius(1),
borderRadius: theme.shape.radius.default,
'&:focus': focusStyle,
'&:focus-visible': focusStyle,
'&:focus:not(:focus-visible)': getMouseFocusStyles(theme),
Expand Down
14 changes: 11 additions & 3 deletions packages/grafana-ui/src/components/IconButton/IconButton.mdx
@@ -1,12 +1,20 @@
import { Meta, Story, Preview, ArgTypes } from '@storybook/blocks';
import { Meta, ArgTypes } from '@storybook/blocks';
import { IconButton } from './IconButton';
import { Alert } from '../Alert/Alert';

<Meta title="MDX|IconButton" component={IconButton} />

# IconButton

This component looks like just an icon but behaves like a button. It fulfils an action when you click it and has hover and focus states. You can choose which icon size you would like to use.
This component looks just like an icon but behaves like a button. It fulfils an action when you click it and has hover and focus states. You can choose which icon size you would like to use.

`IconButton` is best used when an actual button would look out of place, for example when you want to place a solitary clickable icon next to text. An example where an `IconButton` is used in Grafana is the top left back arrow in the panel edit mode.
`IconButton` is best used when you only want an icon instead of a button with text, for example when you want to place a solitary clickable icon next to text. An example where an `IconButton` is used in Grafana is the hamburger icon at the top left which opens the new navigation.

Always keep in mind to add text for a tooltip and an aria label.

<Alert severity="warning" title={'Please note:'}>
After reviewing this component we would like you to know that there are only 5 sizes available (sizes xs to xl). Sizes
xxl and xxxl are now shown in size xl as well and will be deprecated in the future.
</Alert>

<ArgTypes of={IconButton} />
113 changes: 84 additions & 29 deletions packages/grafana-ui/src/components/IconButton/IconButton.story.tsx
Expand Up @@ -5,11 +5,15 @@ import React from 'react';
import { useTheme2 } from '../../themes';
import { IconSize, IconName } from '../../types';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { VerticalGroup } from '../Layout/Layout';
import { HorizontalGroup, VerticalGroup } from '../Layout/Layout';

import { IconButton, IconButtonVariant, Props as IconButtonProps } from './IconButton';
import mdx from './IconButton.mdx';

interface ScenarioProps {
background: 'canvas' | 'primary' | 'secondary';
}

const meta: Meta<typeof IconButton> = {
title: 'Buttons/IconButton',
component: IconButton,
Expand All @@ -35,57 +39,108 @@ const meta: Meta<typeof IconButton> = {
},
};

export const Examples = () => {
return (
<div>
<RenderScenario background="canvas" />
<RenderScenario background="primary" />
<RenderScenario background="secondary" />
</div>
);
};

export const Basic: StoryFn<typeof IconButton> = (args: IconButtonProps) => {
return <IconButton {...args} />;
};

interface ScenarioProps {
background: 'canvas' | 'primary' | 'secondary';
}

const RenderScenario = ({ background }: ScenarioProps) => {
const theme = useTheme2();
const sizes: IconSize[] = ['sm', 'md', 'lg', 'xl', 'xxl'];
export const ExamplesSizes = () => {
const sizes: IconSize[] = ['xs', 'sm', 'md', 'lg', 'xl'];
const icons: IconName[] = ['search', 'trash-alt', 'arrow-left', 'times'];
const variants: IconButtonVariant[] = ['secondary', 'primary', 'destructive'];
const variants: IconButtonVariant[] = ['primary', 'secondary', 'destructive'];

return (
<div
className={css`
padding: 30px;
background: ${theme.colors.background[background]};
button {
margin-right: 8px;
margin-left: 8px;
margin-bottom: 8px;
margin-bottom: 20px;
}
`}
>
<VerticalGroup spacing="md">
<div>{background}</div>
<HorizontalGroup spacing="md">
{variants.map((variant) => {
return (
<div key={variant}>
<p>{variant}</p>
{icons.map((icon) => {
return sizes.map((size) => (
<span key={icon + size}>
<IconButton name={icon} size={size} variant={variant} />
</span>
));
return (
<div
className={css`
display: flex;
`}
key={icon}
>
{sizes.map((size) => (
<span key={icon + size}>
<IconButton name={icon} size={size} variant={variant} />
</span>
))}
</div>
);
})}
</div>
);
})}
<div>
<p>disabled</p>
{icons.map((icon) => (
<div
className={css`
display: flex;
`}
key={icon}
>
{sizes.map((size) => (
<span key={icon + size}>
<IconButton name={icon} size={size} disabled />
</span>
))}
</div>
))}
</div>
</HorizontalGroup>
</div>
);
};

export const ExamplesBackground = () => {
return (
<div>
<RenderBackgroundScenario background="canvas" />
<RenderBackgroundScenario background="primary" />
<RenderBackgroundScenario background="secondary" />
</div>
);
};

const RenderBackgroundScenario = ({ background }: ScenarioProps) => {
const theme = useTheme2();
const variants: IconButtonVariant[] = ['primary', 'secondary', 'destructive'];

return (
<div
className={css`
padding: 30px;
background: ${theme.colors.background[background]};
button {
margin-right: 8px;
margin-left: 8px;
}
`}
>
<VerticalGroup spacing="md">
<div>{background}</div>
<div
className={css`
display: flex;
`}
>
{variants.map((variant) => {
return <IconButton name="times" size="xl" variant={variant} key={variant} />;
})}
<IconButton name="times" size="xl" disabled />
</div>
</VerticalGroup>
</div>
);
Expand Down
78 changes: 36 additions & 42 deletions packages/grafana-ui/src/components/IconButton/IconButton.tsx
@@ -1,22 +1,24 @@
import { css, cx } from '@emotion/css';
import React from 'react';

import { GrafanaTheme2, colorManipulator } from '@grafana/data';
import { GrafanaTheme2, colorManipulator, deprecationWarning } from '@grafana/data';

import { useTheme2 } from '../../themes/ThemeContext';
import { useTheme2, stylesFactory } from '../../themes';
import { getFocusStyles, getMouseFocusStyles } from '../../themes/mixins';
import { stylesFactory } from '../../themes/stylesFactory';
import { ComponentSize } from '../../types';
import { IconName, IconSize, IconType } from '../../types/icon';
import { Icon } from '../Icon/Icon';
import { getSvgSize } from '../Icon/utils';
import { TooltipPlacement, PopoverContent, Tooltip } from '../Tooltip';

export type IconButtonVariant = 'primary' | 'secondary' | 'destructive';

type LimitedIconSize = ComponentSize | 'xl';

export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** Name of the icon **/
name: IconName;
/** Icon size */
/** Icon size - sizes xxl and xxxl are deprecated and when used being decreased to xl*/
size?: IconSize;
/** Type of the icon - mono or default */
iconType?: IconType;
Expand Down Expand Up @@ -46,12 +48,22 @@ export const IconButton = React.forwardRef<HTMLButtonElement, Props>(
ref
) => {
const theme = useTheme2();
const styles = getStyles(theme, size, variant);
let limitedIconSize: LimitedIconSize;

// very large icons (xl to xxxl) are unified to size xl
if (size === 'xxl' || size === 'xxxl') {
deprecationWarning('IconButton', 'size="xxl" and size="xxxl"', 'size="xl"');
limitedIconSize = 'xl';
L-M-K-B marked this conversation as resolved.
Show resolved Hide resolved
} else {
limitedIconSize = size;
}

const styles = getStyles(theme, limitedIconSize, variant);
const tooltipString = typeof tooltip === 'string' ? tooltip : '';

const button = (
<button ref={ref} aria-label={ariaLabel || tooltipString} {...restProps} className={cx(styles.button, className)}>
<Icon name={name} size={size} className={styles.icon} type={iconType} />
<Icon name={name} size={limitedIconSize} className={styles.icon} type={iconType} />
</button>
);

Expand All @@ -69,9 +81,11 @@ export const IconButton = React.forwardRef<HTMLButtonElement, Props>(

IconButton.displayName = 'IconButton';

const getStyles = stylesFactory((theme: GrafanaTheme2, size: IconSize, variant: IconButtonVariant) => {
const pixelSize = getSvgSize(size);
const hoverSize = Math.max(pixelSize / 3, 8);
const getStyles = stylesFactory((theme: GrafanaTheme2, size, variant: IconButtonVariant) => {
// overall size of the IconButton on hover
// theme.spacing.gridSize originates from 2*4px for padding and letting the IconSize generally decide on the hoverSize
const hoverSize = getSvgSize(size) + theme.spacing.gridSize;

let iconColor = theme.colors.text.primary;

if (variant === 'primary') {
Expand All @@ -82,47 +96,34 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, size: IconSize, variant:

return {
button: css`
width: ${pixelSize}px;
height: ${pixelSize}px;
background: transparent;
border: none;
color: ${iconColor};
padding: 0;
margin: 0;
outline: none;
z-index: 0;
position: relative;
margin: 0 ${theme.spacing(0.5)} 0 0;
box-shadow: none;
border: none;
display: inline-flex;
align-items: center;
background: transparent;
justify-content: center;
position: relative;
border-radius: ${theme.shape.borderRadius()};
z-index: 0;
margin-right: ${theme.spacing(0.5)};
align-items: center;
padding: 0;
color: ${iconColor};

&[disabled],
&:disabled {
cursor: not-allowed;
color: ${theme.colors.action.disabledText};
opacity: 0.65;
box-shadow: none;
}

&:before {
content: '';
display: block;
opacity: 1;
z-index: -1;
position: absolute;
width: ${hoverSize}px;
height: ${hoverSize}px;
border-radius: ${theme.shape.radius.default};
content: '';
transition-duration: 0.2s;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
z-index: -1;
bottom: -${hoverSize}px;
left: -${hoverSize}px;
right: -${hoverSize}px;
top: -${hoverSize}px;
background: none;
border-radius: 50%;
box-sizing: border-box;
transform: scale(0);
transition-property: transform, opacity;
}

Expand All @@ -136,22 +137,15 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, size: IconSize, variant:
}

&:hover {
color: ${iconColor};

&:before {
background-color: ${variant === 'secondary'
? theme.colors.action.hover
: colorManipulator.alpha(iconColor, 0.12)};
border: none;
box-shadow: none;
opacity: 1;
transform: scale(0.8);
}
}
`,
icon: css`
vertical-align: baseline;
display: flex;
`,
};
});