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 {
const getStyles = stylesFactory(({ theme, name }: { theme: GrafanaTheme; name: string }) => {
const { color, borderColor } = getTagColorsFromName(name);
const height = theme.spacing.formInputHeight - 8;
return {
itemStyle: css`
display: flex;
align-items: center;
height: ${height}px;
line-height: ${height - 2}px;
background-color: ${color};
color: ${theme.palette.white};
border: 1px solid ${borderColor};
border-radius: 3px;
padding: 3px 6px;
margin: 3px;
padding: 0 ${theme.spacing.xs};
margin-right: 3px;
white-space: nowrap;
text-shadow: none;
font-weight: 500;
font-size: ${theme.typography.size.sm};
display: flex;
align-items: center;
`,
nameStyle: css`
......@@ -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 }) => {
const theme = useTheme();
const styles = getStyles({ theme, name });
......
import React from 'react';
import { action } from '@storybook/addon-actions';
import React, { useState } from 'react';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { UseState } from '../../utils/storybook/UseState';
import { TagsInput } from '@grafana/ui';
import mdx from './TagsInput.mdx';
const mockTags = ['Some', 'Tags', 'With', 'This', 'New', 'Component'];
import { StoryExample } from '../../utils/storybook/StoryExample';
import { VerticalGroup } from '../Layout/Layout';
export default {
title: 'Forms/TagsInput',
......@@ -18,16 +16,18 @@ export default {
},
};
export const basic = () => {
return <TagsInput tags={[]} onChange={(tags) => action('tags updated')(tags)} />;
export const Basic = () => {
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 (
<UseState initialState={mockTags}>
{(tags) => {
return <TagsInput tags={tags} onChange={(tags) => action('tags updated')(tags)} />;
}}
</UseState>
<VerticalGroup>
<StoryExample name="With many tags">
<TagsInput tags={tags} onChange={setTags} />
</StoryExample>
</VerticalGroup>
);
};
import React, { ChangeEvent, KeyboardEvent, PureComponent } from 'react';
import { css, cx } from 'emotion';
import { stylesFactory } from '../../themes/stylesFactory';
import React, { ChangeEvent, KeyboardEvent, FC, useState } from 'react';
import { css } from 'emotion';
import { Button } from '../Button';
import { Input } from '../Forms/Legacy/Input/Input';
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;
tags?: string[];
onChange: (tags: string[]) => void;
}
interface State {
newTag: string;
tags: string[];
}
export class TagsInput extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
newTag: '',
tags: this.props.tags || [],
};
}
export const TagsInput: FC<Props> = ({ placeholder = 'New tag (enter key to add)', tags = [], onChange }) => {
const [newTagName, setNewName] = useState('');
const styles = useStyles(getStyles);
onNameChange = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({
newTag: event.target.value,
});
const onNameChange = (event: ChangeEvent<HTMLInputElement>) => {
setNewName(event.target.value);
};
onRemove = (tagToRemove: string) => {
this.setState(
(prevState: State) => ({
...prevState,
tags: prevState.tags.filter((tag) => tagToRemove !== tag),
}),
() => this.onChange()
);
const onRemove = (tagToRemove: string) => {
onChange(tags?.filter((x) => x !== tagToRemove));
};
// Using React.MouseEvent to avoid tslint error
onAdd = (event: React.MouseEvent) => {
const onAdd = (event: React.MouseEvent) => {
event.preventDefault();
if (this.state.newTag !== '') {
this.setNewTags();
}
onChange(tags.concat(newTagName));
setNewName('');
};
onKeyboardAdd = (event: KeyboardEvent) => {
const onKeyboardAdd = (event: KeyboardEvent) => {
event.preventDefault();
if (event.key === 'Enter' && this.state.newTag !== '') {
this.setNewTags();
}
};
setNewTags = () => {
// We don't want to duplicate tags, clearing the input if
// the user is trying to add the same tag.
if (!this.state.tags.includes(this.state.newTag)) {
this.setState(
(prevState: State) => ({
...prevState,
tags: [...prevState.tags, prevState.newTag],
newTag: '',
}),
() => this.onChange()
);
} else {
this.setState({ newTag: '' });
if (event.key === 'Enter' && newTagName !== '') {
onChange(tags.concat(newTagName));
setNewName('');
}
};
onChange = () => {
this.props.onChange(this.state.tags);
};
render() {
const { placeholder = 'Add name' } = this.props;
const { tags, newTag } = this.state;
const getStyles = stylesFactory(() => ({
tagsCloudStyle: css`
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
`,
addButtonStyle: css`
margin-left: 8px;
`,
}));
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>
return (
<div className={styles.wrapper}>
<div className={styles.tags}>
{tags?.map((tag: string, index: number) => {
return <TagItem key={`${tag}-${index}`} name={tag} onRemove={onRemove} />;
})}
</div>
);
}
}
<div>
<Input
placeholder={placeholder}
onChange={onNameChange}
value={newTagName}
onKeyUp={onKeyboardAdd}
suffix={
<Button
variant="link"
className={styles.addButtonStyle}
onClick={onAdd}
size="md"
disabled={newTagName.length === 0}
>
Add
</Button>
}
/>
</div>
</div>
);
};
const getStyles = (theme: GrafanaTheme) => ({
wrapper: css`
height: ${theme.spacing.formInputHeight}px;
align-items: center;
display: flex;
flex-wrap: wrap;
`,
tags: css`
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
margin-right: ${theme.spacing.xs};
`,
addButtonStyle: css`
margin: 0 -${theme.spacing.sm};
`,
});
......@@ -79,7 +79,7 @@ export const GeneralSettings: React.FC<Props> = ({ dashboard }) => {
<Field label="Description">
<Input name="description" onBlur={onBlur} defaultValue={dashboard.description} />
</Field>
<Field label="Tags" description="Press enter to add a tag">
<Field label="Tags">
<TagsInput tags={dashboard.tags} onChange={onTagsChange} />
</Field>
<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