diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-04-15 16:32:55 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-04-15 17:40:33 +0200 |
| commit | d9bf6f0d69ecb4475c06c772ef6314e5a7ee0fe8 (patch) | |
| tree | a5b962a7f41db1d1d951fab5d1f1557edef756f1 /src | |
| parent | 6ec16bc15cc78e62cb94e131699625fa5363437c (diff) | |
chore: add a LinksListWidget component
Diffstat (limited to 'src')
5 files changed, 284 insertions, 3 deletions
diff --git a/src/components/atoms/lists/list.tsx b/src/components/atoms/lists/list.tsx index 74ab8b0..d100a31 100644 --- a/src/components/atoms/lists/list.tsx +++ b/src/components/atoms/lists/list.tsx @@ -18,7 +18,7 @@ export type ListItem = { export type ListProps = { /** - * Set additional classnames to the list wrapper + * Set additional classnames to the list wrapper. */ className?: string; /** @@ -26,6 +26,10 @@ export type ListProps = { */ items: ListItem[]; /** + * Set additional classnames to the list items. + */ + itemsClassName?: string; + /** * The list kind (ordered or unordered). */ kind?: 'ordered' | 'unordered'; @@ -41,8 +45,9 @@ export type ListProps = { * Render either an ordered or an unordered list. */ const List: VFC<ListProps> = ({ - className, + className = '', items, + itemsClassName = '', kind = 'unordered', withMargin = true, }) => { @@ -57,7 +62,7 @@ const List: VFC<ListProps> = ({ */ const getItems = (array: ListItem[]): JSX.Element[] => { return array.map(({ child, id, value }) => ( - <li key={id} className={styles.list__item}> + <li key={id} className={`${styles.list__item} ${itemsClassName}`}> {value} {child && ( <ListTag diff --git a/src/components/organisms/widgets/links-list-widget.module.scss b/src/components/organisms/widgets/links-list-widget.module.scss new file mode 100644 index 0000000..cbad83e --- /dev/null +++ b/src/components/organisms/widgets/links-list-widget.module.scss @@ -0,0 +1,71 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/placeholders"; + +.widget { + .list { + &__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 { + @extend %reset-ordered-list; + + counter-reset: link; + + .list__link { + counter-increment: link; + + &::before { + padding-right: var(--spacing-2xs); + content: counters(link, ".") ". "; + color: var(--color-secondary); + } + } + } + + &--unordered { + @extend %reset-list; + } + + &__item { + &:not(:last-child) { + .list__link { + 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 new file mode 100644 index 0000000..528f6f7 --- /dev/null +++ b/src/components/organisms/widgets/links-list-widget.stories.tsx @@ -0,0 +1,92 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { IntlProvider } from 'react-intl'; +import LinksListWidget from './links-list-widget'; + +export default { + title: 'Organisms/Widgets', + component: LinksListWidget, + args: { + kind: 'unordered', + }, + argTypes: { + items: { + description: 'The widget data.', + type: { + name: 'object', + required: true, + value: {}, + }, + }, + kind: { + control: { + type: 'select', + }, + description: 'The list kind: either ordered or unordered.', + options: ['ordered', 'unordered'], + table: { + category: 'Options', + defaultValue: { summary: 'unordered' }, + }, + type: { + name: 'string', + required: false, + }, + }, + level: { + control: { + type: 'number', + }, + description: 'The heading level.', + type: { + name: 'number', + required: true, + }, + }, + title: { + control: { + type: 'text', + }, + description: 'The widget title.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta<typeof LinksListWidget>; + +const Template: ComponentStory<typeof LinksListWidget> = (args) => ( + <IntlProvider locale="en"> + <LinksListWidget {...args} /> + </IntlProvider> +); + +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: '#' }, +]; + +export const LinksList = Template.bind({}); +LinksList.args = { + items, + level: 2, + title: 'A list of links', +}; diff --git a/src/components/organisms/widgets/links-list-widget.test.tsx b/src/components/organisms/widgets/links-list-widget.test.tsx new file mode 100644 index 0000000..a8d6a35 --- /dev/null +++ b/src/components/organisms/widgets/links-list-widget.test.tsx @@ -0,0 +1,32 @@ +import { render, screen } from '@test-utils'; +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 items={items} title={title} level={2} />); + expect( + screen.getByRole('heading', { level: 2, name: new RegExp(title, 'i') }) + ).toBeInTheDocument(); + }); + + it('renders the correct number of items', () => { + render(<LinksListWidget items={items} title={title} level={2} />); + expect(screen.getAllByRole('listitem')).toHaveLength(items.length); + }); + + it('renders some links', () => { + render(<LinksListWidget items={items} title={title} level={2} />); + expect(screen.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 new file mode 100644 index 0000000..155354e --- /dev/null +++ b/src/components/organisms/widgets/links-list-widget.tsx @@ -0,0 +1,81 @@ +import Link from '@components/atoms/links/link'; +import List, { ListProps, type ListItem } from '@components/atoms/lists/list'; +import Widget, { type WidgetProps } from '@components/molecules/layout/widget'; +import { slugify } from '@utils/helpers/slugify'; +import { VFC } from 'react'; +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 = Pick<WidgetProps, 'level' | 'title'> & + Pick<ListProps, 'kind'> & { + /** + * An array of name/url couple. + */ + items: LinksListItems[]; + }; + +/** + * LinksListWidget component + * + * Render a list of links inside a widget. + */ +const LinksListWidget: VFC<LinksListWidgetProps> = ({ + items, + kind = 'unordered', + ...props +}) => { + const listKindClass = `list--${kind}`; + + /** + * 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[]): ListItem[] => { + return data.map((item) => { + return { + id: slugify(item.name), + child: item.child && getListItems(item.child), + value: ( + <Link href={item.url} className={styles.list__link}> + {item.name} + </Link> + ), + }; + }); + }; + + return ( + <Widget + expanded={true} + withBorders={true} + className={styles.widget} + {...props} + > + <List + items={getListItems(items)} + kind={kind} + withMargin={false} + className={`${styles.list} ${styles[listKindClass]}`} + itemsClassName={styles.list__item} + /> + </Widget> + ); +}; + +export default LinksListWidget; |
