diff options
Diffstat (limited to 'src/components/molecules/meta-list/meta-item')
5 files changed, 358 insertions, 0 deletions
diff --git a/src/components/molecules/meta-list/meta-item/index.ts b/src/components/molecules/meta-list/meta-item/index.ts new file mode 100644 index 0000000..47795de --- /dev/null +++ b/src/components/molecules/meta-list/meta-item/index.ts @@ -0,0 +1 @@ +export * from './meta-item'; diff --git a/src/components/molecules/meta-list/meta-item/meta-item.module.scss b/src/components/molecules/meta-list/meta-item/meta-item.module.scss new file mode 100644 index 0000000..a1c2d47 --- /dev/null +++ b/src/components/molecules/meta-list/meta-item/meta-item.module.scss @@ -0,0 +1,62 @@ +@use "../../../../styles/abstracts/functions" as fun; + +.item { + column-gap: var(--spacing-2xs); + align-content: baseline; + + &--bordered-values { + row-gap: var(--spacing-2xs); + } + + &--centered { + margin-inline: auto; + text-align: center; + place-items: center; + justify-content: center; + } + + &--inlined { + align-items: first baseline; + } + + &--inlined-values { + flex-flow: row wrap; + } + + &:not(#{&}--bordered-values) { + row-gap: fun.convert-px(3); + } +} + +.value { + width: fit-content; + height: fit-content; + color: var(--color-fg); + font-weight: 400; +} + +:where(.item--bordered-values) { + .value { + padding: fun.convert-px(2) var(--spacing-2xs); + border: fun.convert-px(1) solid var(--color-primary-darker); + } +} + +:where(.item--inlined-values) { + .label { + flex: 1 0 100%; + } +} + +/* It's an arbitrary choice. When there is only one meta item (like on small + * cards) removing the width can mess up the layout. However, must of the times + * when there are multiples items, we need to remove the width especially if we + * want to use `isCentered` prop. */ +:where(.item--inlined-values:not(:only-of-type)) { + .label { + /* We need to remove its width to avoid an extra space and make the + * container width fit its contents. However the label should be smaller + * than the values to avoid unexpected behavior with layout. */ + width: 0; + } +} diff --git a/src/components/molecules/meta-list/meta-item/meta-item.stories.tsx b/src/components/molecules/meta-list/meta-item/meta-item.stories.tsx new file mode 100644 index 0000000..3ddb8f1 --- /dev/null +++ b/src/components/molecules/meta-list/meta-item/meta-item.stories.tsx @@ -0,0 +1,108 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Link } from '../../../atoms'; +import { MetaItem } from './meta-item'; + +/** + * MetaItem - Storybook Meta + */ +export default { + title: 'Molecules/MetaList/Item', + component: MetaItem, + argTypes: { + label: { + control: { + type: 'text', + }, + description: 'The item label.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta<typeof MetaItem>; + +const Template: ComponentStory<typeof MetaItem> = (args) => ( + <MetaItem {...args} /> +); + +/** + * MetaItem Stories - SingleValue + */ +export const SingleValue = Template.bind({}); +SingleValue.args = { + label: 'Comments', + value: 'No comments', +}; + +/** + * MetaItem Stories - MultipleValues + */ +export const MultipleValues = Template.bind({}); +MultipleValues.args = { + label: 'Tags', + value: [ + { id: 'tag1', value: <Link href="#tag1">Tag 1</Link> }, + { id: 'tag2', value: <Link href="#tag2">Tag 2</Link> }, + ], +}; + +/** + * MetaItem Stories - SingleValueBordered + */ +export const SingleValueBordered = Template.bind({}); +SingleValueBordered.args = { + hasBorderedValues: true, + label: 'Comments', + value: 'No comments', +}; + +/** + * MetaItem Stories - MultipleValuesBordered + */ +export const MultipleValuesBordered = Template.bind({}); +MultipleValuesBordered.args = { + hasBorderedValues: true, + label: 'Tags', + value: [ + { id: 'tag1', value: <Link href="#tag1">Tag 1</Link> }, + { id: 'tag2', value: <Link href="#tag2">Tag 2</Link> }, + ], +}; + +/** + * MetaItem Stories - SingleValueInlined + */ +export const SingleValueInlined = Template.bind({}); +SingleValueInlined.args = { + isInline: true, + label: 'Comments', + value: 'No comments', +}; + +/** + * MetaItem Stories - MultipleValuesInlined + */ +export const MultipleValuesInlined = Template.bind({}); +MultipleValuesInlined.args = { + isInline: true, + label: 'Tags', + value: [ + { id: 'tag1', value: <Link href="#tag1">Tag 1</Link> }, + { id: 'tag2', value: <Link href="#tag2">Tag 2</Link> }, + ], +}; + +/** + * MetaItem Stories - InlinedValues + */ +export const InlinedValues = Template.bind({}); +InlinedValues.args = { + hasInlinedValues: true, + label: 'Tags', + value: [ + { id: 'tag1', value: <Link href="#tag1">Tag 1</Link> }, + { id: 'tag2', value: <Link href="#tag2">A long tag 2</Link> }, + { id: 'tag3', value: <Link href="#tag3">Tag 3</Link> }, + ], +}; diff --git a/src/components/molecules/meta-list/meta-item/meta-item.test.tsx b/src/components/molecules/meta-list/meta-item/meta-item.test.tsx new file mode 100644 index 0000000..629c4b2 --- /dev/null +++ b/src/components/molecules/meta-list/meta-item/meta-item.test.tsx @@ -0,0 +1,97 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { MetaItem } from './meta-item'; + +describe('MetaItem', () => { + it('renders a label and a value', () => { + const label = 'iusto'; + const value = 'autem'; + + render( + <dl> + <MetaItem label={label} value={value} /> + </dl> + ); + + expect(rtlScreen.getByRole('term')).toHaveTextContent(label); + expect(rtlScreen.getByRole('definition')).toHaveTextContent(value); + }); + + it('can render a label with multiple values', () => { + const label = 'iusto'; + const values = [ + { id: 'autem', value: 'autem' }, + { id: 'quisquam', value: 'aut' }, + { id: 'molestias', value: 'voluptatem' }, + ]; + + render( + <dl> + <MetaItem label={label} value={values} /> + </dl> + ); + + expect(rtlScreen.getByRole('term')).toHaveTextContent(label); + expect(rtlScreen.getAllByRole('definition')).toHaveLength(values.length); + }); + + it('can render a centered group of label and values', () => { + const label = 'iusto'; + const value = 'autem'; + + render( + <dl> + <MetaItem isCentered label={label} value={value} /> + </dl> + ); + + expect(rtlScreen.getByRole('term').parentElement).toHaveClass( + 'item--centered' + ); + }); + + it('can render an inlined group of label and values', () => { + const label = 'iusto'; + const value = 'autem'; + + render( + <dl> + <MetaItem isInline label={label} value={value} /> + </dl> + ); + + expect(rtlScreen.getByRole('term').parentElement).toHaveClass( + 'item--inlined' + ); + }); + + it('can render a group of label and bordered values', () => { + const label = 'iusto'; + const value = 'autem'; + + render( + <dl> + <MetaItem hasBorderedValues label={label} value={value} /> + </dl> + ); + + expect(rtlScreen.getByRole('term').parentElement).toHaveClass( + 'item--bordered-values' + ); + }); + + it('can render a group of label and inlined values', () => { + const label = 'iusto'; + const value = 'autem'; + + render( + <dl> + <MetaItem hasInlinedValues label={label} value={value} /> + </dl> + ); + + expect(rtlScreen.getByRole('term').parentElement).toHaveClass( + 'item--inlined-values' + ); + }); +}); diff --git a/src/components/molecules/meta-list/meta-item/meta-item.tsx b/src/components/molecules/meta-list/meta-item/meta-item.tsx new file mode 100644 index 0000000..c5223c2 --- /dev/null +++ b/src/components/molecules/meta-list/meta-item/meta-item.tsx @@ -0,0 +1,90 @@ +import { + type ForwardRefRenderFunction, + type ReactElement, + type ReactNode, + forwardRef, +} from 'react'; +import { Description, Group, type GroupProps, Term } from '../../../atoms'; +import styles from './meta-item.module.scss'; + +export type MetaValue = string | ReactElement; + +export type MetaValues = { + id: string; + value: MetaValue; +}; + +export type MetaItemProps = Omit<GroupProps, 'children' | 'spacing'> & { + /** + * Should the values be bordered? + * + * @default false + */ + hasBorderedValues?: boolean; + /** + * Should the values be inlined? + * + * @warning If you use it make sure the value is larger than the label. It + * could mess up your design since we are removing the label width. + * + * @default false + */ + hasInlinedValues?: boolean; + /** + * Should the label and values be centered? + * + * @default false + */ + isCentered?: boolean; + /** + * The item label. + */ + label: ReactNode; + /** + * The item value or values. + */ + value: MetaValue | MetaValues[]; +}; + +const MetaItemWithRef: ForwardRefRenderFunction< + HTMLDivElement, + MetaItemProps +> = ( + { + className = '', + hasBorderedValues = false, + hasInlinedValues = false, + isCentered = false, + isInline = false, + label, + value, + ...props + }, + ref +) => { + const itemClass = [ + styles.item, + styles[hasBorderedValues ? 'item--bordered-values' : ''], + styles[hasInlinedValues ? 'item--inlined-values' : ''], + styles[isCentered ? 'item--centered' : ''], + styles[isInline ? 'item--inlined' : 'item--stacked'], + className, + ].join(' '); + + return ( + <Group {...props} className={itemClass} isInline={isInline} ref={ref}> + <Term className={styles.label}>{label}</Term> + {Array.isArray(value) ? ( + value.map((item) => ( + <Description className={styles.value} key={item.id}> + {item.value} + </Description> + )) + ) : ( + <Description className={styles.value}>{value}</Description> + )} + </Group> + ); +}; + +export const MetaItem = forwardRef(MetaItemWithRef); |
