[Glitch] Tags component
Port 3f460340391f19d1cabd5320f0c227fc6ed693f4 to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
36be9bbebf
commit
da2cb50aaa
@ -0,0 +1,50 @@
|
|||||||
|
.tag {
|
||||||
|
border-radius: 9999px;
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
appearance: none;
|
||||||
|
background: none;
|
||||||
|
padding: 8px;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.tag:hover,
|
||||||
|
button.tag:focus {
|
||||||
|
border-color: var(--color-bg-brand-base-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.tag:focus-visible {
|
||||||
|
outline: var(--outline-focus-default);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
border-color: var(--color-text-brand);
|
||||||
|
background: var(--color-bg-brand-softer);
|
||||||
|
color: var(--color-text-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
height: 1.2em;
|
||||||
|
width: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
svg {
|
||||||
|
height: 1.2em;
|
||||||
|
width: 1.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.active .closeButton {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagsWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
import { action } from 'storybook/actions';
|
||||||
|
|
||||||
|
import MusicNoteIcon from '@/material-icons/400-24px/music_note.svg?react';
|
||||||
|
|
||||||
|
import { EditableTag, Tag } from './tag';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
component: Tag,
|
||||||
|
title: 'Components/Tags/Single Tag',
|
||||||
|
args: {
|
||||||
|
name: 'example-tag',
|
||||||
|
active: false,
|
||||||
|
onClick: action('Click'),
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Tag>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
export const WithIcon: Story = {
|
||||||
|
args: {
|
||||||
|
icon: MusicNoteIcon,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Editable: Story = {
|
||||||
|
render(args) {
|
||||||
|
return <EditableTag {...args} onRemove={action('Remove')} />;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditableWithIcon: Story = {
|
||||||
|
render(args) {
|
||||||
|
return (
|
||||||
|
<EditableTag
|
||||||
|
{...args}
|
||||||
|
removeIcon={MusicNoteIcon}
|
||||||
|
onRemove={action('Remove')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
94
app/javascript/flavours/glitch/components/tags/tag.tsx
Normal file
94
app/javascript/flavours/glitch/components/tags/tag.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
|
|
||||||
|
import type { IconProp } from '../icon';
|
||||||
|
import { Icon } from '../icon';
|
||||||
|
import { IconButton } from '../icon_button';
|
||||||
|
|
||||||
|
import classes from './style.module.css';
|
||||||
|
|
||||||
|
export interface TagProps {
|
||||||
|
name: ReactNode;
|
||||||
|
active?: boolean;
|
||||||
|
icon?: IconProp;
|
||||||
|
className?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tag = forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
TagProps & ComponentPropsWithoutRef<'button'>
|
||||||
|
>(({ name, active, icon, className, children, ...props }, ref) => {
|
||||||
|
if (!name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
type='button'
|
||||||
|
ref={ref}
|
||||||
|
className={classNames(className, classes.tag, active && classes.active)}
|
||||||
|
>
|
||||||
|
{icon && <Icon icon={icon} id='tag-icon' className={classes.icon} />}
|
||||||
|
{typeof name === 'string' ? `#${name}` : name}
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Tag.displayName = 'Tag';
|
||||||
|
|
||||||
|
export const EditableTag = forwardRef<
|
||||||
|
HTMLSpanElement,
|
||||||
|
TagProps & {
|
||||||
|
onRemove: () => void;
|
||||||
|
removeIcon?: IconProp;
|
||||||
|
} & ComponentPropsWithoutRef<'span'>
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
active,
|
||||||
|
icon,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
removeIcon = CloseIcon,
|
||||||
|
onRemove,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
className={classNames(className, classes.tag, active && classes.active)}
|
||||||
|
>
|
||||||
|
{icon && <Icon icon={icon} id='tag-icon' className={classes.icon} />}
|
||||||
|
{typeof name === 'string' ? `#${name}` : name}
|
||||||
|
{children}
|
||||||
|
<IconButton
|
||||||
|
className={classes.closeButton}
|
||||||
|
iconComponent={removeIcon}
|
||||||
|
onClick={onRemove}
|
||||||
|
icon='remove'
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'tag.remove',
|
||||||
|
defaultMessage: 'Remove',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
EditableTag.displayName = 'EditableTag';
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
import { action } from 'storybook/actions';
|
||||||
|
|
||||||
|
import { Tags } from './tags';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
component: Tags,
|
||||||
|
title: 'Components/Tags/List',
|
||||||
|
args: {
|
||||||
|
tags: [{ name: 'tag-one' }, { name: 'tag-two' }],
|
||||||
|
active: 'tag-one',
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Tags>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render(args) {
|
||||||
|
return <Tags {...args} />;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Editable: Story = {
|
||||||
|
args: {
|
||||||
|
onRemove: action('Remove'),
|
||||||
|
},
|
||||||
|
};
|
||||||
54
app/javascript/flavours/glitch/components/tags/tags.tsx
Normal file
54
app/javascript/flavours/glitch/components/tags/tags.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import type { ComponentPropsWithoutRef, FC } from 'react';
|
||||||
|
|
||||||
|
import classes from './style.module.css';
|
||||||
|
import { EditableTag, Tag } from './tag';
|
||||||
|
import type { TagProps } from './tag';
|
||||||
|
|
||||||
|
type Tag = TagProps & { name: string };
|
||||||
|
|
||||||
|
export type TagsProps = {
|
||||||
|
tags: Tag[];
|
||||||
|
active?: string;
|
||||||
|
} & (
|
||||||
|
| ({
|
||||||
|
onRemove?: never;
|
||||||
|
} & ComponentPropsWithoutRef<'button'>)
|
||||||
|
| ({ onRemove?: (tag: string) => void } & ComponentPropsWithoutRef<'span'>)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Tags: FC<TagsProps> = ({ tags, active, onRemove, ...props }) => {
|
||||||
|
if (onRemove) {
|
||||||
|
return (
|
||||||
|
<div className={classes.tagsWrapper}>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<MappedTag
|
||||||
|
key={tag.name}
|
||||||
|
active={tag.name === active}
|
||||||
|
onRemove={onRemove}
|
||||||
|
{...tag}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.tagsWrapper}>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<Tag key={tag.name} active={tag.name === active} {...tag} {...props} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MappedTag: FC<Tag & { onRemove?: (tag: string) => void }> = ({
|
||||||
|
onRemove,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const handleRemove = useCallback(() => {
|
||||||
|
onRemove?.(props.name);
|
||||||
|
}, [onRemove, props.name]);
|
||||||
|
return <EditableTag {...props} onRemove={handleRemove} />;
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user