Improve styles
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -209,6 +209,10 @@
|
||||
.bui-Input {
|
||||
border-radius: var(--bui-radius-3);
|
||||
}
|
||||
|
||||
.bui-Tag {
|
||||
border-radius: var(--bui-radius-full);
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme-mode='light'][data-theme-name='spotify'] {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,114 @@
|
||||
import { PropsTable } from '@/components/PropsTable';
|
||||
import { Snippet } from '@/components/Snippet';
|
||||
import { CodeBlock } from '@/components/CodeBlock';
|
||||
import { TagGroupSnippet } from '@/snippets/stories-snippets';
|
||||
import {
|
||||
tagGroupPropDefs,
|
||||
tagPropDefs,
|
||||
usage,
|
||||
preview,
|
||||
withLink,
|
||||
disabled,
|
||||
withIcons,
|
||||
sizes,
|
||||
removingTags,
|
||||
} from './tag-group.props';
|
||||
import { PageTitle } from '@/components/PageTitle';
|
||||
import { Theming } from '@/components/Theming';
|
||||
import { ChangelogComponent } from '@/components/ChangelogComponent';
|
||||
|
||||
<PageTitle
|
||||
title="TagGroup"
|
||||
description="A tag group is a list of labels, categories, keywords, filters, or other items, that can be used to group and filter content."
|
||||
/>
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
preview={<TagGroupSnippet story="Default" />}
|
||||
code={preview}
|
||||
/>
|
||||
|
||||
## Usage
|
||||
|
||||
<CodeBlock code={usage} />
|
||||
|
||||
## API reference
|
||||
|
||||
### TagGroup
|
||||
|
||||
A tag group is a list of tags.
|
||||
|
||||
<PropsTable data={tagGroupPropDefs} />
|
||||
|
||||
### Tag
|
||||
|
||||
A tag is a single item in a tag group.
|
||||
|
||||
<PropsTable data={tagPropDefs} />
|
||||
|
||||
## Examples
|
||||
|
||||
### With Links
|
||||
|
||||
A tag can be a link by passing a `href` prop.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<TagGroupSnippet story="WithLink" />}
|
||||
code={withLink}
|
||||
/>
|
||||
|
||||
### With Icons
|
||||
|
||||
A tag can have an icon by passing a `icon` prop.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<TagGroupSnippet story="WithIcon" />}
|
||||
code={withIcons}
|
||||
/>
|
||||
|
||||
### Sizes
|
||||
|
||||
A tag can have a size by passing a `size` prop. It could be `small` or `medium`.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<TagGroupSnippet story="Sizes" />}
|
||||
code={sizes}
|
||||
/>
|
||||
|
||||
### Removing tags
|
||||
|
||||
A tag can be removed by passing a `onRemove` prop.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<TagGroupSnippet story="RemovingTags" />}
|
||||
code={removingTags}
|
||||
/>
|
||||
|
||||
### Disabled
|
||||
|
||||
A switch can be disabled using the `isDisabled` prop.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<TagGroupSnippet story="Disabled" />}
|
||||
code={disabled}
|
||||
/>
|
||||
|
||||
<Theming component="Switch" />
|
||||
|
||||
<ChangelogComponent component="switch" />
|
||||
@@ -0,0 +1,161 @@
|
||||
import { classNamePropDefs, stylePropDefs } from '@/utils/propDefs';
|
||||
import type { PropDef } from '@/utils/propDefs';
|
||||
|
||||
export const tagGroupPropDefs: Record<string, PropDef> = {
|
||||
disabledKeys: {
|
||||
type: 'enum',
|
||||
values: ['Iterable<Key>'],
|
||||
description: 'A list of row keys to disable.',
|
||||
},
|
||||
selectionMode: {
|
||||
type: 'enum',
|
||||
values: ['none', 'single', 'multiple'],
|
||||
description: 'The type of selection that is allowed in the collection.',
|
||||
},
|
||||
selectedKeys: {
|
||||
type: 'enum',
|
||||
values: ['all', 'Iterable<Key>'],
|
||||
description: 'The currently selected keys in the collection (controlled).',
|
||||
},
|
||||
defaultSelectedKeys: {
|
||||
type: 'enum',
|
||||
values: ['all', 'Iterable<Key>'],
|
||||
description: 'The initial selected keys in the collection (uncontrolled).',
|
||||
},
|
||||
onRemove: {
|
||||
type: 'enum',
|
||||
values: ['(keys: Set<Key>) => void'],
|
||||
description: 'Handler that is called when a tag is removed.',
|
||||
},
|
||||
onSelectionChange: {
|
||||
type: 'enum',
|
||||
values: ['(keys: Selection) => void'],
|
||||
description: 'Handler that is called when the selection changes.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const tagPropDefs: Record<string, PropDef> = {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'The id of the tag.',
|
||||
},
|
||||
textValue: {
|
||||
type: 'string',
|
||||
description: 'The text value of the tag.',
|
||||
},
|
||||
isDisabled: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the tag is disabled.',
|
||||
},
|
||||
href: {
|
||||
type: 'string',
|
||||
description: 'The href of the tag.',
|
||||
},
|
||||
icon: {
|
||||
type: 'enum',
|
||||
values: ['ReactNode'],
|
||||
description: 'The icon of the tag.',
|
||||
},
|
||||
...classNamePropDefs,
|
||||
...stylePropDefs,
|
||||
};
|
||||
|
||||
export const preview = `import { TagGroup, Tag } from '@backstage/ui';
|
||||
|
||||
<TagGroup aria-label="Tag Group">
|
||||
<Tag>Banana</Tag>
|
||||
<Tag>Apple</Tag>
|
||||
<Tag>Orange</Tag>
|
||||
<Tag>Pear</Tag>
|
||||
<Tag>Grape</Tag>
|
||||
<Tag>Pineapple</Tag>
|
||||
<Tag>Strawberry</Tag>
|
||||
</TagGroup>
|
||||
`;
|
||||
|
||||
export const usage = `import { TagGroup, Tag } from '@backstage/ui';
|
||||
|
||||
// Basic usage
|
||||
<TagGroup aria-label="Tag Group">
|
||||
<Tag>Tag 1</Tag>
|
||||
<Tag>Tag 2</Tag>
|
||||
<Tag>Tag 3</Tag>
|
||||
</TagGroup>
|
||||
|
||||
// With the items prop
|
||||
const list = [
|
||||
{ id: '1', name: 'Tag 1' },
|
||||
{ id: '2', name: 'Tag 2' },
|
||||
{ id: '3', name: 'Tag 3' },
|
||||
];
|
||||
|
||||
<TagGroup aria-label="Tag Group" items={list}>
|
||||
{item => <Tag>{item.name}</Tag>}
|
||||
</TagGroup>`;
|
||||
|
||||
export const withLink = `import { TagGroup, Tag } from '@backstage/ui';
|
||||
|
||||
<TagGroup aria-label="Tag Group">
|
||||
<Tag href="/items/banana">Banana</Tag>
|
||||
<Tag href="/items/apple">Apple</Tag>
|
||||
<Tag href="/items/orange">Orange</Tag>
|
||||
...
|
||||
</TagGroup>`;
|
||||
|
||||
export const disabled = `import { TagGroup, Tag } from '@backstage/ui';
|
||||
|
||||
<TagGroup aria-label="Tag Group">
|
||||
<Tag>Banana</Tag>
|
||||
<Tag isDisabled>Apple</Tag>
|
||||
<Tag isDisabled>Orange</Tag>
|
||||
<Tag>Pear</Tag>
|
||||
...
|
||||
</TagGroup>`;
|
||||
|
||||
export const withIcons = `import { TagGroup, Tag } from '@backstage/ui';
|
||||
|
||||
<TagGroup aria-label="Tag Group">
|
||||
<Tag icon={<RiBugLine />}>Banana</Tag>
|
||||
<Tag icon={<RiAccountCircleLine />}>Apple</Tag>
|
||||
<Tag icon={<RiEyeLine />}>Orange</Tag>
|
||||
...
|
||||
</TagGroup>`;
|
||||
|
||||
export const sizes = `import { TagGroup, Tag, Flex } from '@backstage/ui';
|
||||
|
||||
<Flex direction="column">
|
||||
<TagGroup aria-label="Tag Group">
|
||||
<Tag size="small">Banana</Tag>
|
||||
<Tag size="small">Apple</Tag>
|
||||
<Tag size="small">Orange</Tag>
|
||||
...
|
||||
</TagGroup>
|
||||
<TagGroup aria-label="Tag Group">
|
||||
<Tag size="medium">Banana</Tag>
|
||||
<Tag size="medium">Apple</Tag>
|
||||
<Tag size="medium">Orange</Tag>
|
||||
...
|
||||
</TagGroup>
|
||||
</Flex>`;
|
||||
|
||||
export const removingTags = `import { TagGroup, Tag } from '@backstage/ui';
|
||||
import type { Selection } from 'react-aria-components';
|
||||
import { useListData } from 'react-stately';
|
||||
|
||||
const [selected, setSelected] = useState<Selection>(new Set(['travel']));
|
||||
|
||||
const list = useListData({
|
||||
initialItems: initialList,
|
||||
});
|
||||
|
||||
<TagGroup
|
||||
items={list.items}
|
||||
onRemove={keys => list.remove(...keys)}
|
||||
selectedKeys={selected}
|
||||
onSelectionChange={setSelected}
|
||||
{...args}
|
||||
>
|
||||
{item => <Tag>{item.name}</Tag>}
|
||||
</TagGroup>`;
|
||||
@@ -27,6 +27,7 @@ import * as CardStories from '../../../packages/ui/src/components/Card/Card.stor
|
||||
import * as HeaderStories from '../../../packages/ui/src/components/Header/Header.stories';
|
||||
import * as HeaderPageStories from '../../../packages/ui/src/components/HeaderPage/HeaderPage.stories';
|
||||
import * as TableStories from '../../../packages/ui/src/components/Table/Table.stories';
|
||||
import * as TagGroupStories from '../../../packages/ui/src/components/TagGroup/TagGroup.stories';
|
||||
|
||||
// Helper function to create snippet components
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -69,3 +70,4 @@ export const CardSnippet = createSnippetComponent(CardStories);
|
||||
export const HeaderSnippet = createSnippetComponent(HeaderStories);
|
||||
export const HeaderPageSnippet = createSnippetComponent(HeaderPageStories);
|
||||
export const TableSnippet = createSnippetComponent(TableStories);
|
||||
export const TagGroupSnippet = createSnippetComponent(TagGroupStories);
|
||||
|
||||
@@ -154,13 +154,18 @@ export const components: Page[] = [
|
||||
{
|
||||
title: 'Table',
|
||||
slug: 'table',
|
||||
status: 'inProgress',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'Tabs',
|
||||
slug: 'tabs',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'TagGroup',
|
||||
slug: 'tag-group',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'Text',
|
||||
slug: 'text',
|
||||
|
||||
@@ -10269,7 +10269,7 @@
|
||||
.bui-Tag {
|
||||
color: var(--bui-fg-primary);
|
||||
background-color: var(--bui-gray-2);
|
||||
border-radius: var(--bui-radius-full);
|
||||
border-radius: var(--bui-radius-2);
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -10307,6 +10307,11 @@
|
||||
box-shadow: inset 0 0 0 1px var(--bui-gray-8);
|
||||
}
|
||||
|
||||
.bui-Tag[data-disabled] {
|
||||
color: var(--bui-fg-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.bui-TagRemoveButton {
|
||||
cursor: pointer;
|
||||
color: var(--bui-fg-primary);
|
||||
|
||||
@@ -18,13 +18,15 @@ import { useState } from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { TagGroup, Tag } from '.';
|
||||
import type { Selection } from 'react-aria-components';
|
||||
import { Icon, IconNames } from '@backstage/ui';
|
||||
import { Flex, Icon, IconNames } from '../../';
|
||||
import { useListData } from 'react-stately';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
export interface ListItem {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: IconNames;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
const meta = {
|
||||
@@ -39,6 +41,13 @@ const meta = {
|
||||
control: { type: 'text' },
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
Story => (
|
||||
<MemoryRouter>
|
||||
<Story />
|
||||
</MemoryRouter>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof TagGroup<ListItem>>;
|
||||
|
||||
export default meta;
|
||||
@@ -46,8 +55,8 @@ type Story = StoryObj<typeof meta>;
|
||||
|
||||
const initialList: ListItem[] = [
|
||||
{ id: 'banana', name: 'Banana', icon: 'bug' },
|
||||
{ id: 'apple', name: 'Apple', icon: 'account-circle' },
|
||||
{ id: 'orange', name: 'Orange', icon: 'eye' },
|
||||
{ id: 'apple', name: 'Apple', icon: 'account-circle', isDisabled: true },
|
||||
{ id: 'orange', name: 'Orange', icon: 'eye', isDisabled: true },
|
||||
{ id: 'pear', name: 'Pear', icon: 'heart' },
|
||||
{ id: 'grape', name: 'Grape', icon: 'bug' },
|
||||
{ id: 'pineapple', name: 'Pineapple', icon: 'eye' },
|
||||
@@ -67,6 +76,30 @@ export const Default: Story = {
|
||||
),
|
||||
};
|
||||
|
||||
export const Sizes: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
render: args => (
|
||||
<Flex direction="column">
|
||||
<TagGroup {...args}>
|
||||
{initialList.map(item => (
|
||||
<Tag key={item.id} size="small" icon={<Icon name={item.icon} />}>
|
||||
{item.name}
|
||||
</Tag>
|
||||
))}
|
||||
</TagGroup>
|
||||
<TagGroup {...args}>
|
||||
{initialList.map(item => (
|
||||
<Tag key={item.id} size="medium" icon={<Icon name={item.icon} />}>
|
||||
{item.name}
|
||||
</Tag>
|
||||
))}
|
||||
</TagGroup>
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
|
||||
export const SelectionModeSingle: Story = {
|
||||
args: {
|
||||
selectionMode: 'single',
|
||||
@@ -129,7 +162,31 @@ export const WithIcon: Story = {
|
||||
),
|
||||
};
|
||||
|
||||
export const WithRemoveButton: Story = {
|
||||
export const WithLink: Story = {
|
||||
render: args => (
|
||||
<TagGroup {...args}>
|
||||
{initialList.map(item => (
|
||||
<Tag key={item.id} href={`/items/${item.id}`}>
|
||||
{item.name}
|
||||
</Tag>
|
||||
))}
|
||||
</TagGroup>
|
||||
),
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
render: args => (
|
||||
<TagGroup {...args}>
|
||||
{initialList.map(item => (
|
||||
<Tag key={item.id} isDisabled={item.isDisabled}>
|
||||
{item.name}
|
||||
</Tag>
|
||||
))}
|
||||
</TagGroup>
|
||||
),
|
||||
};
|
||||
|
||||
export const RemovingTags: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
.bui-Tag {
|
||||
color: var(--bui-fg-primary);
|
||||
background-color: var(--bui-gray-2);
|
||||
border-radius: var(--bui-radius-full);
|
||||
border-radius: var(--bui-radius-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -61,6 +61,11 @@
|
||||
box-shadow: inset 0 0 0 1px var(--bui-gray-8);
|
||||
}
|
||||
|
||||
.bui-Tag[data-disabled] {
|
||||
color: var(--bui-fg-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.bui-TagRemoveButton {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
@@ -20,11 +20,14 @@ import {
|
||||
TagList as ReactAriaTagList,
|
||||
Tag as ReactAriaTag,
|
||||
Button as ReactAriaButton,
|
||||
RouterProvider,
|
||||
} from 'react-aria-components';
|
||||
import type { ReactNode } from 'react';
|
||||
import { RiCloseCircleLine } from '@remixicon/react';
|
||||
import clsx from 'clsx';
|
||||
import { useStyles } from '../../hooks/useStyles';
|
||||
import { isExternalLink } from '../../utils/isExternalLink';
|
||||
import { useNavigate, useHref } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* A component that renders a list of tags.
|
||||
@@ -57,15 +60,19 @@ export const TagGroup = <T extends object>({
|
||||
* @public
|
||||
*/
|
||||
export const Tag = (props: TagProps) => {
|
||||
const { children, className, icon, size = 'small', ...rest } = props;
|
||||
const { children, className, icon, size = 'small', href, ...rest } = props;
|
||||
const textValue = typeof children === 'string' ? children : undefined;
|
||||
const { classNames } = useStyles('TagGroup');
|
||||
const navigate = useNavigate();
|
||||
const isLink = href !== undefined;
|
||||
const isExternal = isExternalLink(href);
|
||||
|
||||
return (
|
||||
const content = (
|
||||
<ReactAriaTag
|
||||
textValue={textValue}
|
||||
className={clsx(classNames.tag, className)}
|
||||
data-size={size}
|
||||
href={href}
|
||||
{...rest}
|
||||
>
|
||||
{({ allowsRemoving }) => (
|
||||
@@ -84,4 +91,14 @@ export const Tag = (props: TagProps) => {
|
||||
)}
|
||||
</ReactAriaTag>
|
||||
);
|
||||
|
||||
if (isLink && !isExternal) {
|
||||
return (
|
||||
<RouterProvider navigate={navigate} useHref={useHref}>
|
||||
{content}
|
||||
</RouterProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user