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 22 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} />
Expand Up @@ -55,9 +55,9 @@ interface ScenarioProps {

const RenderScenario = ({ background }: ScenarioProps) => {
L-M-K-B marked this conversation as resolved.
Show resolved Hide resolved
const theme = useTheme2();
const sizes: IconSize[] = ['sm', 'md', 'lg', 'xl', 'xxl'];
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
Expand All @@ -67,25 +67,61 @@ const RenderScenario = ({ background }: ScenarioProps) => {
button {
margin-right: 8px;
margin-left: 8px;
margin-bottom: 8px;
margin-bottom: 30px;
}
`}
>
<VerticalGroup spacing="md">
<div>{background}</div>
{variants.map((variant) => {
return (
<div key={variant}>
{icons.map((icon) => {
return sizes.map((size) => (
<div
className={css`
display: flex;
`}
>
{variants.map((variant) => {
return (
<div key={variant}>
{icons.map((icon) => {
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
className={css`
display: flex;
flex-direction: column;
`}
>
{icons.map((icon) => (
<div
className={css`
display: flex;
`}
key={icon}
>
{sizes.map((size) => (
<span key={icon + size}>
<IconButton name={icon} size={size} variant={variant} />
<IconButton name={icon} size={size} disabled />
</span>
));
})}
</div>
);
})}
))}
</div>
))}
</div>
</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
// the value 8 originates from 2*4px and letting the IconSize generally decide on the hoverSize
const hoverSize = getSvgSize(size) + 8;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should make it use the theme here, so 2 * theme.spacing(0.5)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately theme.spacing() is returning a string including 'px'.
I could do something like parseInt(theme.spacing(0.5).substring(0)) but this is cumbersome. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh thats true... We could also use theme.spacing.gridSize which returns 8? And keep this in mind when we do spacing tokens

Copy link
Contributor Author

@L-M-K-B L-M-K-B May 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to theme.spacing.gridSize, modified the related comment in the code and added a reminder to the related epic for spacing tokens.


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;
`,
};
});
38 changes: 26 additions & 12 deletions packages/grafana-ui/src/components/PanelChrome/PanelChrome.mdx
@@ -1,13 +1,13 @@
import { Meta, Preview, ArgTypes } from '@storybook/blocks';
import { css } from '@emotion/css';
import { Meta } from '@storybook/addon-docs/blocks';
import { PanelChrome } from './PanelChrome';
import { LoadingIndicator } from './LoadingIndicator';

import { action } from '@storybook/addon-actions';
import { DashboardStoryCanvas } from '../../utils/storybook/DashboardStoryCanvas';
import { HorizontalGroup } from '../Layout/Layout';
import { LoadingState } from '@grafana/data';
import { Button } from '../Button/Button.tsx';
import { Button } from '../Button';
import { Menu } from '../Menu/Menu';
import { IconButton } from '../IconButton/IconButton';

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

Expand Down Expand Up @@ -306,10 +306,17 @@ Component used for rendering content wrapped in the same style as grafana panels
<PanelChrome
title="My awesome panel title"
titleItems={
<div>
<Button fill="text" icon="github" variant="secondary" tooltip="extra content to render" />
<Button fill="text" icon="sliders-v-alt" variant="secondary" tooltip="extra content2 to render" />
</div>
<>
<IconButton
className={css`
margin-right: 10px;
`}
name="github"
variant="secondary"
tooltip="extra content to render"
/>
<IconButton name="sliders-v-alt" variant="secondary" tooltip="extra content2 to render" />
</>
}
actions={
<Button size="sm" variant="secondary" key="A">
Expand Down Expand Up @@ -342,10 +349,17 @@ Component used for rendering content wrapped in the same style as grafana panels
<PanelChrome
title="My awesome panel title"
titleItems={
<div>
<Button fill="text" icon="github" variant="secondary" tooltip="extra content to render" />
<Button fill="text" icon="sliders-v-alt" variant="secondary" tooltip="extra content2 to render" />
</div>
<>
<IconButton
className={css`
margin-right: 10px;
`}
name="github"
variant="secondary"
tooltip="extra content to render"
/>
<IconButton name="sliders-v-alt" variant="secondary" tooltip="extra content2 to render" />
</>
}
actions={
<Button size="sm" variant="secondary" key="A">
Expand Down