mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Design System: Refactor IconButton and update documentation (#66774)
* refactor: remove unnecessary styling and adjust to button styling * refactor: improve story * refactor: use new default styling for border radius * refactor: add missing pseudo classes and clean up * refactor: repair disabled styling and add to story * refactor: clean up and apply styles defined in figma * refactor: set hover background in a pseudo-element * refactor: unify large sizes * refactor: add information for further use * refactor: add comment * refactor: comments and clean up import * refactor: add changes after code review * refactor: replace some bad example code in documentation * refactor: update comment Co-authored-by: Joao Silva <100691367+JoaoSilvaGrafana@users.noreply.github.com> * refactor: add changes requested in review * refactor: move deprecation warning * refactor: replace padding * refactor: remove local styling * refactor: create separate stories for different examples * refactor: change style of story * refactor: replace absolute value by variable * Update toggles_gen.go --------- Co-authored-by: Joao Silva <100691367+JoaoSilvaGrafana@users.noreply.github.com>
This commit is contained in:
parent
9441692fe9
commit
e7dc3575d1
@ -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.
|
||||
|
@ -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';
|
||||
@ -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
|
||||
|
@ -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';
|
||||
@ -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} />}
|
||||
@ -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),
|
||||
|
@ -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} />
|
||||
|
@ -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,
|
||||
@ -35,29 +39,84 @@ 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`
|
||||
button {
|
||||
margin-right: 8px;
|
||||
margin-left: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<HorizontalGroup spacing="md">
|
||||
{variants.map((variant) => {
|
||||
return (
|
||||
<div key={variant}>
|
||||
<p>{variant}</p>
|
||||
{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>
|
||||
<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
|
||||
@ -67,25 +126,21 @@ const RenderScenario = ({ background }: ScenarioProps) => {
|
||||
button {
|
||||
margin-right: 8px;
|
||||
margin-left: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<VerticalGroup spacing="md">
|
||||
<div>{background}</div>
|
||||
{variants.map((variant) => {
|
||||
return (
|
||||
<div key={variant}>
|
||||
{icons.map((icon) => {
|
||||
return sizes.map((size) => (
|
||||
<span key={icon + size}>
|
||||
<IconButton name={icon} size={size} variant={variant} />
|
||||
</span>
|
||||
));
|
||||
})}
|
||||
</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>
|
||||
);
|
||||
|
@ -1,11 +1,11 @@
|
||||
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';
|
||||
@ -13,10 +13,12 @@ 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;
|
||||
@ -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';
|
||||
} 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>
|
||||
);
|
||||
|
||||
@ -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') {
|
||||
@ -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;
|
||||
box-shadow: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
border-radius: ${theme.shape.borderRadius()};
|
||||
z-index: 0;
|
||||
margin-right: ${theme.spacing(0.5)};
|
||||
position: relative;
|
||||
margin: 0 ${theme.spacing(0.5)} 0 0;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
display: inline-flex;
|
||||
background: transparent;
|
||||
justify-content: center;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
@ -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} />
|
||||
|
||||
@ -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">
|
||||
@ -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">
|
||||
|
@ -71,9 +71,7 @@ return (
|
||||
closeButton={true}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Button type="button">
|
||||
<Icon name="question-circle" />
|
||||
</Button>
|
||||
<IconButton name="question-circle" tooltip="IconButton containing a Toggletip" />
|
||||
</Toogletip>
|
||||
);
|
||||
```
|
||||
|
@ -125,9 +125,8 @@ export const DynamicTable = <T extends object>({
|
||||
<div className={cx(styles.cell, styles.expandCell)}>
|
||||
<IconButton
|
||||
aria-label={`${isItemExpanded ? 'Collapse' : 'Expand'} row`}
|
||||
size="lg"
|
||||
size="md"
|
||||
data-testid="collapse-toggle"
|
||||
className={styles.expandButton}
|
||||
name={isItemExpanded ? 'angle-down' : 'angle-right'}
|
||||
onClick={() => toggleExpanded(item)}
|
||||
type="button"
|
||||
@ -278,9 +277,5 @@ const getStyles = <T extends unknown>(
|
||||
padding: ${theme.spacing(1)} 0 0 0;
|
||||
}
|
||||
`,
|
||||
expandButton: css`
|
||||
margin-right: 0;
|
||||
display: block;
|
||||
`,
|
||||
});
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user