Commit 4dfce12a by Torkel Ödegaard Committed by GitHub

TagsInput: Design update and component refactor (#31163)

* TagsInput: Design update and component refactor

* Update packages/grafana-ui/src/components/TagsInput/TagsInput.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Update packages/grafana-ui/src/components/TagsInput/TagsInput.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Update packages/grafana-ui/src/components/TagsInput/TagsInput.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Update packages/grafana-ui/src/components/TagsInput/TagsInput.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Updated

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
parent f993f2c7
...@@ -13,21 +13,24 @@ interface Props { ...@@ -13,21 +13,24 @@ interface Props {
const getStyles = stylesFactory(({ theme, name }: { theme: GrafanaTheme; name: string }) => { const getStyles = stylesFactory(({ theme, name }: { theme: GrafanaTheme; name: string }) => {
const { color, borderColor } = getTagColorsFromName(name); const { color, borderColor } = getTagColorsFromName(name);
const height = theme.spacing.formInputHeight - 8;
return { return {
itemStyle: css` itemStyle: css`
display: flex;
align-items: center;
height: ${height}px;
line-height: ${height - 2}px;
background-color: ${color}; background-color: ${color};
color: ${theme.palette.white}; color: ${theme.palette.white};
border: 1px solid ${borderColor}; border: 1px solid ${borderColor};
border-radius: 3px; border-radius: 3px;
padding: 3px 6px; padding: 0 ${theme.spacing.xs};
margin: 3px; margin-right: 3px;
white-space: nowrap; white-space: nowrap;
text-shadow: none; text-shadow: none;
font-weight: 500; font-weight: 500;
font-size: ${theme.typography.size.sm}; font-size: ${theme.typography.size.sm};
display: flex;
align-items: center;
`, `,
nameStyle: css` nameStyle: css`
...@@ -36,6 +39,10 @@ const getStyles = stylesFactory(({ theme, name }: { theme: GrafanaTheme; name: s ...@@ -36,6 +39,10 @@ const getStyles = stylesFactory(({ theme, name }: { theme: GrafanaTheme; name: s
}; };
}); });
/**
* @internal
* Only used internally by TagsInput
* */
export const TagItem: FC<Props> = ({ name, onRemove }) => { export const TagItem: FC<Props> = ({ name, onRemove }) => {
const theme = useTheme(); const theme = useTheme();
const styles = getStyles({ theme, name }); const styles = getStyles({ theme, name });
......
import React from 'react'; import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { UseState } from '../../utils/storybook/UseState';
import { TagsInput } from '@grafana/ui'; import { TagsInput } from '@grafana/ui';
import mdx from './TagsInput.mdx'; import mdx from './TagsInput.mdx';
import { StoryExample } from '../../utils/storybook/StoryExample';
const mockTags = ['Some', 'Tags', 'With', 'This', 'New', 'Component']; import { VerticalGroup } from '../Layout/Layout';
export default { export default {
title: 'Forms/TagsInput', title: 'Forms/TagsInput',
...@@ -18,16 +16,18 @@ export default { ...@@ -18,16 +16,18 @@ export default {
}, },
}; };
export const basic = () => { export const Basic = () => {
return <TagsInput tags={[]} onChange={(tags) => action('tags updated')(tags)} />; const [tags, setTags] = useState<string[]>([]);
return <TagsInput tags={tags} onChange={setTags} />;
}; };
export const withMockTags = () => { export const WithManyTags = () => {
const [tags, setTags] = useState<string[]>(['dashboard', 'prod', 'server', 'frontend', 'game', 'kubernetes']);
return ( return (
<UseState initialState={mockTags}> <VerticalGroup>
{(tags) => { <StoryExample name="With many tags">
return <TagsInput tags={tags} onChange={(tags) => action('tags updated')(tags)} />; <TagsInput tags={tags} onChange={setTags} />
}} </StoryExample>
</UseState> </VerticalGroup>
); );
}; };
import React, { ChangeEvent, KeyboardEvent, PureComponent } from 'react'; import React, { ChangeEvent, KeyboardEvent, FC, useState } from 'react';
import { css, cx } from 'emotion'; import { css } from 'emotion';
import { stylesFactory } from '../../themes/stylesFactory';
import { Button } from '../Button'; import { Button } from '../Button';
import { Input } from '../Forms/Legacy/Input/Input';
import { TagItem } from './TagItem'; import { TagItem } from './TagItem';
import { useStyles } from '../../themes/ThemeContext';
import { GrafanaTheme } from '@grafana/data';
import { Input } from '../Input/Input';
interface Props { export interface Props {
placeholder?: string; placeholder?: string;
tags?: string[]; tags?: string[];
onChange: (tags: string[]) => void; onChange: (tags: string[]) => void;
} }
interface State { export const TagsInput: FC<Props> = ({ placeholder = 'New tag (enter key to add)', tags = [], onChange }) => {
newTag: string; const [newTagName, setNewName] = useState('');
tags: string[]; const styles = useStyles(getStyles);
}
export class TagsInput extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
newTag: '',
tags: this.props.tags || [],
};
}
onNameChange = (event: ChangeEvent<HTMLInputElement>) => { const onNameChange = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({ setNewName(event.target.value);
newTag: event.target.value,
});
}; };
onRemove = (tagToRemove: string) => { const onRemove = (tagToRemove: string) => {
this.setState( onChange(tags?.filter((x) => x !== tagToRemove));
(prevState: State) => ({
...prevState,
tags: prevState.tags.filter((tag) => tagToRemove !== tag),
}),
() => this.onChange()
);
}; };
// Using React.MouseEvent to avoid tslint error const onAdd = (event: React.MouseEvent) => {
onAdd = (event: React.MouseEvent) => {
event.preventDefault(); event.preventDefault();
if (this.state.newTag !== '') { onChange(tags.concat(newTagName));
this.setNewTags(); setNewName('');
}
}; };
onKeyboardAdd = (event: KeyboardEvent) => { const onKeyboardAdd = (event: KeyboardEvent) => {
event.preventDefault(); event.preventDefault();
if (event.key === 'Enter' && this.state.newTag !== '') { if (event.key === 'Enter' && newTagName !== '') {
this.setNewTags(); onChange(tags.concat(newTagName));
setNewName('');
} }
}; };
setNewTags = () => { return (
// We don't want to duplicate tags, clearing the input if <div className={styles.wrapper}>
// the user is trying to add the same tag. <div className={styles.tags}>
if (!this.state.tags.includes(this.state.newTag)) { {tags?.map((tag: string, index: number) => {
this.setState( return <TagItem key={`${tag}-${index}`} name={tag} onRemove={onRemove} />;
(prevState: State) => ({ })}
...prevState, </div>
tags: [...prevState.tags, prevState.newTag], <div>
newTag: '', <Input
}), placeholder={placeholder}
() => this.onChange() onChange={onNameChange}
); value={newTagName}
} else { onKeyUp={onKeyboardAdd}
this.setState({ newTag: '' }); suffix={
<Button
variant="link"
className={styles.addButtonStyle}
onClick={onAdd}
size="md"
disabled={newTagName.length === 0}
>
Add
</Button>
} }
}; />
</div>
onChange = () => { </div>
this.props.onChange(this.state.tags); );
}; };
render() {
const { placeholder = 'Add name' } = this.props;
const { tags, newTag } = this.state;
const getStyles = stylesFactory(() => ({ const getStyles = (theme: GrafanaTheme) => ({
tagsCloudStyle: css` wrapper: css`
height: ${theme.spacing.formInputHeight}px;
align-items: center;
display: flex;
flex-wrap: wrap;
`,
tags: css`
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
flex-wrap: wrap; flex-wrap: wrap;
margin-right: ${theme.spacing.xs};
`, `,
addButtonStyle: css` addButtonStyle: css`
margin-left: 8px; margin: 0 -${theme.spacing.sm};
`, `,
})); });
return (
<div className="width-20">
<div
className={cx(
['gf-form-inline'],
css`
margin-bottom: 4px;
`
)}
>
<Input placeholder={placeholder} onChange={this.onNameChange} value={newTag} onKeyUp={this.onKeyboardAdd} />
<Button className={getStyles().addButtonStyle} onClick={this.onAdd} variant="secondary" size="md">
Add
</Button>
</div>
<div className={getStyles().tagsCloudStyle}>
{tags &&
tags.map((tag: string, index: number) => {
return <TagItem key={`${tag}-${index}`} name={tag} onRemove={this.onRemove} />;
})}
</div>
</div>
);
}
}
...@@ -79,7 +79,7 @@ export const GeneralSettings: React.FC<Props> = ({ dashboard }) => { ...@@ -79,7 +79,7 @@ export const GeneralSettings: React.FC<Props> = ({ dashboard }) => {
<Field label="Description"> <Field label="Description">
<Input name="description" onBlur={onBlur} defaultValue={dashboard.description} /> <Input name="description" onBlur={onBlur} defaultValue={dashboard.description} />
</Field> </Field>
<Field label="Tags" description="Press enter to add a tag"> <Field label="Tags">
<TagsInput tags={dashboard.tags} onChange={onTagsChange} /> <TagsInput tags={dashboard.tags} onChange={onTagsChange} />
</Field> </Field>
<Field label="Folder"> <Field label="Folder">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment