aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/molecules/meta-list/meta-item
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/molecules/meta-list/meta-item')
-rw-r--r--src/components/molecules/meta-list/meta-item/index.ts1
-rw-r--r--src/components/molecules/meta-list/meta-item/meta-item.module.scss62
-rw-r--r--src/components/molecules/meta-list/meta-item/meta-item.stories.tsx108
-rw-r--r--src/components/molecules/meta-list/meta-item/meta-item.test.tsx97
-rw-r--r--src/components/molecules/meta-list/meta-item/meta-item.tsx90
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);