aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/organisms
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-11-14 15:11:22 +0100
committerArmand Philippot <git@armandphilippot.com>2023-11-14 19:06:42 +0100
commita3a4c50f26b8750ae1c87f1f1103b84b7d2e6315 (patch)
treeae286c7c6b3ab4f556f20adf5e42b24641351296 /src/components/organisms
parent50f1c501a87ef5f5650750dbeca797e833ec7c3a (diff)
refactor(components): replace LinksListWidget with LinksWidget
* avoid List component repeat * rewrite tests and CSS * add an id to LinksWidgetItemData (previously LinksListItems) type because the label could be duplicated
Diffstat (limited to 'src/components/organisms')
-rw-r--r--src/components/organisms/widgets/index.ts2
-rw-r--r--src/components/organisms/widgets/links-list-widget.module.scss71
-rw-r--r--src/components/organisms/widgets/links-list-widget.stories.tsx91
-rw-r--r--src/components/organisms/widgets/links-list-widget.test.tsx56
-rw-r--r--src/components/organisms/widgets/links-list-widget.tsx87
-rw-r--r--src/components/organisms/widgets/links-widget/index.ts1
-rw-r--r--src/components/organisms/widgets/links-widget/links-widget.module.scss65
-rw-r--r--src/components/organisms/widgets/links-widget/links-widget.stories.tsx80
-rw-r--r--src/components/organisms/widgets/links-widget/links-widget.test.tsx83
-rw-r--r--src/components/organisms/widgets/links-widget/links-widget.tsx78
-rw-r--r--src/components/organisms/widgets/table-of-contents.tsx11
11 files changed, 314 insertions, 311 deletions
diff --git a/src/components/organisms/widgets/index.ts b/src/components/organisms/widgets/index.ts
index 2286898..972561e 100644
--- a/src/components/organisms/widgets/index.ts
+++ b/src/components/organisms/widgets/index.ts
@@ -1,5 +1,5 @@
export * from './image-widget';
-export * from './links-list-widget';
+export * from './links-widget';
export * from './sharing-widget';
export * from './social-media-widget';
export * from './table-of-contents';
diff --git a/src/components/organisms/widgets/links-list-widget.module.scss b/src/components/organisms/widgets/links-list-widget.module.scss
deleted file mode 100644
index 4efc2d4..0000000
--- a/src/components/organisms/widgets/links-list-widget.module.scss
+++ /dev/null
@@ -1,71 +0,0 @@
-@use "../../../styles/abstracts/functions" as fun;
-@use "../../../styles/abstracts/placeholders";
-
-.widget {
- .list {
- .list {
- padding: 0;
-
- > *:first-child {
- border-top: fun.convert-px(1) solid var(--color-primary);
- }
- }
-
- &__link {
- display: block;
- padding: var(--spacing-2xs) var(--spacing-xs);
- background: none;
- text-decoration: underline solid transparent 0;
-
- &:hover,
- &:focus {
- background: var(--color-bg-secondary);
- font-weight: 600;
- }
-
- &:focus {
- color: var(--color-primary);
- text-decoration-color: var(--color-primary-light);
- text-decoration-thickness: 0.25ex;
- }
-
- &:active {
- background: var(--color-bg-tertiary);
- text-decoration-color: transparent;
- text-decoration-thickness: 0;
- }
- }
-
- &--ordered {
- counter-reset: link;
-
- .list__link {
- counter-increment: link;
-
- &::before {
- padding-right: var(--spacing-2xs);
- content: counters(link, ".") ". ";
- color: var(--color-secondary);
- }
- }
- }
-
- &__item {
- &:not(:last-child) {
- border-bottom: fun.convert-px(1) solid var(--color-primary);
- }
-
- > .list {
- .list__link {
- padding-left: var(--spacing-md);
- }
-
- .list__item > .list {
- .list__link {
- padding-left: var(--spacing-xl);
- }
- }
- }
- }
- }
-}
diff --git a/src/components/organisms/widgets/links-list-widget.stories.tsx b/src/components/organisms/widgets/links-list-widget.stories.tsx
deleted file mode 100644
index 6e5f170..0000000
--- a/src/components/organisms/widgets/links-list-widget.stories.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import type { ComponentMeta, ComponentStory } from '@storybook/react';
-import { Heading } from '../../atoms';
-import { LinksListWidget } from './links-list-widget';
-
-/**
- * LinksListWidget - Storybook Meta
- */
-export default {
- title: 'Organisms/Widgets/LinksList',
- component: LinksListWidget,
- args: {
- isOrdered: false,
- },
- argTypes: {
- className: {
- control: {
- type: 'text',
- },
- description: 'Set additional classnames to the list wrapper.',
- table: {
- category: 'Styles',
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- items: {
- description: 'The widget data.',
- type: {
- name: 'object',
- required: true,
- value: {},
- },
- },
- },
-} as ComponentMeta<typeof LinksListWidget>;
-
-const Template: ComponentStory<typeof LinksListWidget> = (args) => (
- <LinksListWidget {...args} />
-);
-
-const items = [
- { name: 'Level 1: Item 1', url: '#' },
- {
- name: 'Level 1: Item 2',
- url: '#',
- child: [
- { name: 'Level 2: Item 1', url: '#' },
- { name: 'Level 2: Item 2', url: '#' },
- {
- name: 'Level 2: Item 3',
- url: '#',
- child: [
- { name: 'Level 3: Item 1', url: '#' },
- { name: 'Level 3: Item 2', url: '#' },
- ],
- },
- { name: 'Level 2: Item 4', url: '#' },
- ],
- },
- { name: 'Level 1: Item 3', url: '#' },
- { name: 'Level 1: Item 4', url: '#' },
-];
-
-/**
- * Links List Widget Stories - Unordered
- */
-export const Unordered = Template.bind({});
-Unordered.args = {
- heading: (
- <Heading isFake level={3}>
- Quo et totam
- </Heading>
- ),
- items,
-};
-
-/**
- * Links List Widget Stories - Ordered
- */
-export const Ordered = Template.bind({});
-Ordered.args = {
- heading: (
- <Heading isFake level={3}>
- Quo et totam
- </Heading>
- ),
- isOrdered: true,
- items,
-};
diff --git a/src/components/organisms/widgets/links-list-widget.test.tsx b/src/components/organisms/widgets/links-list-widget.test.tsx
deleted file mode 100644
index 2a914e7..0000000
--- a/src/components/organisms/widgets/links-list-widget.test.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { describe, expect, it } from '@jest/globals';
-import { render, screen as rtlScreen } from '@testing-library/react';
-import { Heading } from '../../atoms';
-import { LinksListWidget } from './links-list-widget';
-
-const title = 'Voluptatem minus autem';
-
-const items = [
- { name: 'Item 1', url: '/item-1' },
- { name: 'Item 2', url: '/item-2' },
- { name: 'Item 3', url: '/item-3' },
-];
-
-describe('LinksListWidget', () => {
- it('renders a widget title', () => {
- render(
- <LinksListWidget
- heading={<Heading level={3}>{title}</Heading>}
- items={items}
- />
- );
- expect(
- rtlScreen.getByRole('heading', { level: 3, name: new RegExp(title, 'i') })
- ).toBeInTheDocument();
- });
-
- it('renders the correct number of items', () => {
- render(
- <LinksListWidget
- heading={
- <Heading isFake level={3}>
- {title}
- </Heading>
- }
- items={items}
- />
- );
- expect(rtlScreen.getAllByRole('listitem')).toHaveLength(items.length);
- });
-
- it('renders some links', () => {
- render(
- <LinksListWidget
- heading={
- <Heading isFake level={3}>
- {title}
- </Heading>
- }
- items={items}
- />
- );
- expect(
- rtlScreen.getByRole('link', { name: items[0].name })
- ).toHaveAttribute('href', items[0].url);
- });
-});
diff --git a/src/components/organisms/widgets/links-list-widget.tsx b/src/components/organisms/widgets/links-list-widget.tsx
deleted file mode 100644
index 17a5884..0000000
--- a/src/components/organisms/widgets/links-list-widget.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import type { FC } from 'react';
-import { slugify } from '../../../utils/helpers';
-import { Link, List, ListItem } from '../../atoms';
-import { Collapsible, type CollapsibleProps } from '../../molecules';
-import styles from './links-list-widget.module.scss';
-
-export type LinksListItems = {
- /**
- * An array of name/url couple child of this list item.
- */
- child?: LinksListItems[];
- /**
- * The item name.
- */
- name: string;
- /**
- * The item url.
- */
- url: string;
-};
-
-export type LinksListWidgetProps = Omit<
- CollapsibleProps,
- 'children' | 'disablePadding' | 'hasBorders'
-> & {
- className?: string;
- /**
- * Should the links be ordered?
- *
- * @default false
- */
- isOrdered?: boolean;
- /**
- * An array of name/url couple.
- */
- items: LinksListItems[];
-};
-
-/**
- * LinksListWidget component
- *
- * Render a list of links inside a widget.
- */
-export const LinksListWidget: FC<LinksListWidgetProps> = ({
- className = '',
- isOrdered = false,
- items,
- ...props
-}) => {
- const listKindClass = `list--${isOrdered ? 'ordered' : 'unordered'}`;
-
- /**
- * Format the widget data to be used as List items.
- *
- * @param {LinksListItems[]} data - The widget data.
- * @returns {ListItem[]} The list items data.
- */
- const getListItems = (data: LinksListItems[]) =>
- data.map((item) => (
- <ListItem className={styles.list__item} key={slugify(item.name)}>
- <Link className={styles.list__link} href={item.url}>
- {item.name}
- </Link>
- {item.child?.length ? (
- <List
- className={`${styles.list} ${styles[listKindClass]} ${className}`}
- hideMarker
- isOrdered={isOrdered}
- >
- {getListItems(item.child)}
- </List>
- ) : null}
- </ListItem>
- ));
-
- return (
- <Collapsible {...props} className={styles.widget} disablePadding hasBorders>
- <List
- className={`${styles.list} ${styles[listKindClass]} ${className}`}
- hideMarker
- isOrdered={isOrdered}
- >
- {getListItems(items)}
- </List>
- </Collapsible>
- );
-};
diff --git a/src/components/organisms/widgets/links-widget/index.ts b/src/components/organisms/widgets/links-widget/index.ts
new file mode 100644
index 0000000..cf1de6d
--- /dev/null
+++ b/src/components/organisms/widgets/links-widget/index.ts
@@ -0,0 +1 @@
+export * from './links-widget';
diff --git a/src/components/organisms/widgets/links-widget/links-widget.module.scss b/src/components/organisms/widgets/links-widget/links-widget.module.scss
new file mode 100644
index 0000000..296d8e4
--- /dev/null
+++ b/src/components/organisms/widgets/links-widget/links-widget.module.scss
@@ -0,0 +1,65 @@
+@use "../../../../styles/abstracts/functions" as fun;
+@use "../../../../styles/abstracts/placeholders";
+
+.item {
+ &:not(:last-child) {
+ border-bottom: fun.convert-px(1) solid var(--color-primary);
+ }
+}
+
+.link {
+ display: block;
+ padding: var(--spacing-2xs) var(--spacing-sm);
+ background: none;
+ text-decoration: underline solid transparent 0;
+
+ &:hover,
+ &:focus {
+ background: var(--color-bg-secondary);
+ }
+
+ &:focus {
+ color: var(--color-primary);
+ text-decoration-color: var(--color-primary-light);
+ text-decoration-thickness: 0.25ex;
+ }
+
+ &:active {
+ background: var(--color-bg-tertiary);
+ text-decoration-color: transparent;
+ text-decoration-thickness: 0;
+ }
+}
+
+.list {
+ padding: 0;
+
+ &--ordered {
+ counter-reset: link;
+
+ .link {
+ counter-increment: link;
+
+ &::before {
+ content: counters(link, ".") ". ";
+ display: inline-block;
+ padding-inline-end: var(--spacing-2xs);
+ color: var(--color-secondary);
+ }
+ }
+ }
+
+ & & {
+ border-top: fun.convert-px(1) solid var(--color-primary);
+
+ .link {
+ padding-left: var(--spacing-lg);
+ }
+ }
+
+ & & & {
+ .link {
+ padding-left: var(--spacing-2xl);
+ }
+ }
+}
diff --git a/src/components/organisms/widgets/links-widget/links-widget.stories.tsx b/src/components/organisms/widgets/links-widget/links-widget.stories.tsx
new file mode 100644
index 0000000..3a0b027
--- /dev/null
+++ b/src/components/organisms/widgets/links-widget/links-widget.stories.tsx
@@ -0,0 +1,80 @@
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
+import { Heading } from '../../../atoms';
+import { LinksWidget, type LinksWidgetItemData } from './links-widget';
+
+/**
+ * LinksWidget - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Widgets/Links',
+ component: LinksWidget,
+ args: {
+ isOrdered: false,
+ },
+ argTypes: {
+ items: {
+ description: 'The links data.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ },
+} as ComponentMeta<typeof LinksWidget>;
+
+const Template: ComponentStory<typeof LinksWidget> = (args) => (
+ <LinksWidget {...args} />
+);
+
+const items = [
+ { id: 'item11', label: 'Level 1: Item 1', url: '#' },
+ {
+ id: 'item12',
+ label: 'Level 1: Item 2',
+ url: '#',
+ child: [
+ { id: 'item21', label: 'Level 2: Item 1', url: '#' },
+ { id: 'item22', label: 'Level 2: Item 2', url: '#' },
+ {
+ id: 'item23',
+ label: 'Level 2: Item 3',
+ url: '#',
+ child: [
+ { id: 'item31', label: 'Level 3: Item 1', url: '#' },
+ { id: 'item32', label: 'Level 3: Item 2', url: '#' },
+ ],
+ },
+ { id: 'item24', label: 'Level 2: Item 4', url: '#' },
+ ],
+ },
+ { id: 'item13', label: 'Level 1: Item 3', url: '#' },
+ { id: 'item14', label: 'Level 1: Item 4', url: '#' },
+] satisfies LinksWidgetItemData[];
+
+/**
+ * Links List Widget Stories - Unordered
+ */
+export const Unordered = Template.bind({});
+Unordered.args = {
+ heading: (
+ <Heading isFake level={3}>
+ Quo et totam
+ </Heading>
+ ),
+ items,
+};
+
+/**
+ * Links List Widget Stories - Ordered
+ */
+export const Ordered = Template.bind({});
+Ordered.args = {
+ heading: (
+ <Heading isFake level={3}>
+ Quo et totam
+ </Heading>
+ ),
+ isOrdered: true,
+ items,
+};
diff --git a/src/components/organisms/widgets/links-widget/links-widget.test.tsx b/src/components/organisms/widgets/links-widget/links-widget.test.tsx
new file mode 100644
index 0000000..f0edecc
--- /dev/null
+++ b/src/components/organisms/widgets/links-widget/links-widget.test.tsx
@@ -0,0 +1,83 @@
+import { describe, expect, it } from '@jest/globals';
+import { render, screen as rtlScreen } from '@testing-library/react';
+import { Heading } from '../../../atoms';
+import { LinksWidget, type LinksWidgetItemData } from './links-widget';
+
+describe('LinksWidget', () => {
+ it('renders the widget heading and a list of links', () => {
+ const heading = 'modi sit fugiat';
+ const headingLvl = 3;
+ const items = [
+ { id: 'item1', label: 'Link 1', url: '#link1' },
+ { id: 'item2', label: 'Link 2', url: '#link2' },
+ { id: 'item3', label: 'Link 3', url: '#link3' },
+ ] satisfies LinksWidgetItemData[];
+
+ render(
+ <LinksWidget
+ heading={<Heading level={headingLvl}>{heading}</Heading>}
+ items={items}
+ />
+ );
+
+ expect(
+ rtlScreen.getByRole('heading', { level: headingLvl })
+ ).toHaveTextContent(heading);
+ expect(rtlScreen.getAllByRole('link').length).toBe(items.length);
+ });
+
+ it('can render a nested list of links', () => {
+ const heading = 'modi sit fugiat';
+ const headingLvl = 3;
+ const items = [
+ { id: 'item1', label: 'Link 1', url: '#link1' },
+ {
+ id: 'item2',
+ label: 'Link 2',
+ url: '#link2',
+ child: [
+ { id: 'subitem1', label: 'Nested link 1', url: '#nested-link1' },
+ { id: 'subitem2', label: 'Nested link 2', url: '#nested-link2' },
+ ],
+ },
+ { id: 'item3', label: 'Link 3', url: '#link3' },
+ ] satisfies LinksWidgetItemData[];
+
+ render(
+ <LinksWidget
+ heading={<Heading level={headingLvl}>{heading}</Heading>}
+ items={items}
+ />
+ );
+
+ const totalLinks =
+ items.length +
+ items.reduce(
+ (accumulator, currentValue) =>
+ accumulator + (currentValue.child?.length ?? 0),
+ 0
+ );
+
+ expect(rtlScreen.getAllByRole('link').length).toBe(totalLinks);
+ });
+
+ it('can render an ordered list of links', () => {
+ const heading = 'modi sit fugiat';
+ const headingLvl = 3;
+ const items = [
+ { id: 'item1', label: 'Link 1', url: '#link1' },
+ { id: 'item2', label: 'Link 2', url: '#link2' },
+ { id: 'item3', label: 'Link 3', url: '#link3' },
+ ] satisfies LinksWidgetItemData[];
+
+ render(
+ <LinksWidget
+ heading={<Heading level={headingLvl}>{heading}</Heading>}
+ isOrdered
+ items={items}
+ />
+ );
+
+ expect(rtlScreen.getByRole('list')).toHaveClass(`list--ordered`);
+ });
+});
diff --git a/src/components/organisms/widgets/links-widget/links-widget.tsx b/src/components/organisms/widgets/links-widget/links-widget.tsx
new file mode 100644
index 0000000..e28e15f
--- /dev/null
+++ b/src/components/organisms/widgets/links-widget/links-widget.tsx
@@ -0,0 +1,78 @@
+import { type ForwardRefRenderFunction, forwardRef, useCallback } from 'react';
+import { Link, List, ListItem } from '../../../atoms';
+import { Collapsible, type CollapsibleProps } from '../../../molecules';
+import styles from './links-widget.module.scss';
+
+export type LinksWidgetItemData = {
+ /**
+ * An array of name/url couple child of this list item.
+ */
+ child?: LinksWidgetItemData[];
+ /**
+ * The item id.
+ */
+ id: string;
+ /**
+ * The item name.
+ */
+ label: string;
+ /**
+ * The item url.
+ */
+ url: string;
+};
+
+export type LinksWidgetProps = Omit<
+ CollapsibleProps,
+ 'children' | 'disablePadding' | 'hasBorders'
+> & {
+ /**
+ * Should the links be ordered?
+ *
+ * @default false
+ */
+ isOrdered?: boolean;
+ /**
+ * The links.
+ */
+ items: LinksWidgetItemData[];
+};
+
+const LinksWidgetWithRef: ForwardRefRenderFunction<
+ HTMLDivElement,
+ LinksWidgetProps
+> = ({ isOrdered = false, items, ...props }, ref) => {
+ const listClass = [
+ styles.list,
+ styles[isOrdered ? 'list--ordered' : 'list--unordered'],
+ ].join(' ');
+
+ const getLinksList = useCallback(
+ (data: LinksWidgetItemData[]) => (
+ <List className={listClass} hideMarker isOrdered={isOrdered}>
+ {data.map((item) => (
+ <ListItem className={styles.item} key={item.id}>
+ <Link className={styles.link} href={item.url}>
+ {item.label}
+ </Link>
+ {item.child?.length ? getLinksList(item.child) : null}
+ </ListItem>
+ ))}
+ </List>
+ ),
+ [isOrdered, listClass]
+ );
+
+ return (
+ <Collapsible {...props} disablePadding hasBorders ref={ref}>
+ {getLinksList(items)}
+ </Collapsible>
+ );
+};
+
+/**
+ * LinksWidget component
+ *
+ * Render a list of links inside a widget.
+ */
+export const LinksWidget = forwardRef(LinksWidgetWithRef);
diff --git a/src/components/organisms/widgets/table-of-contents.tsx b/src/components/organisms/widgets/table-of-contents.tsx
index 8892485..5f14415 100644
--- a/src/components/organisms/widgets/table-of-contents.tsx
+++ b/src/components/organisms/widgets/table-of-contents.tsx
@@ -1,9 +1,9 @@
import type { FC } from 'react';
import { useIntl } from 'react-intl';
import { useHeadingsTree, type Heading } from '../../../utils/hooks';
-import { type LinksListItems, LinksListWidget } from './links-list-widget';
+import { Heading as HeadingComponent } from '../../atoms';
+import { LinksWidget, type LinksWidgetItemData } from './links-widget';
import styles from './table-of-contents.module.scss';
-import { Heading as HeadingComponent } from 'src/components/atoms';
type TableOfContentsProps = {
/**
@@ -32,17 +32,18 @@ export const TableOfContents: FC<TableOfContentsProps> = ({ wrapper }) => {
* @param {Heading[]} tree - The headings tree.
* @returns {LinksListItems[]} The list items.
*/
- const getItems = (tree: Heading[]): LinksListItems[] =>
+ const getItems = (tree: Heading[]): LinksWidgetItemData[] =>
tree.map((heading) => {
return {
- name: heading.title,
+ id: heading.id,
+ label: heading.title,
url: `#${heading.id}`,
child: getItems(heading.children),
};
});
return (
- <LinksListWidget
+ <LinksWidget
className={styles.list}
heading={
<HeadingComponent isFake level={3}>