diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-09-27 18:43:25 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-10-24 12:25:00 +0200 |
| commit | d17d894f398650209c0ddd29502308de8c07bd93 (patch) | |
| tree | 858402dfd362e74686d25fec155f247ad3217635 | |
| parent | 7255d25f6834a208c0ed44636356cc260f6ab6ba (diff) | |
feat(components): add Article, Aside, Footer, Header, Main & Nav
Some components have been renamed to be able to create Footer, Header
and Nav.
55 files changed, 580 insertions, 216 deletions
diff --git a/src/components/atoms/index.ts b/src/components/atoms/index.ts index 672440c..e0041cc 100644 --- a/src/components/atoms/index.ts +++ b/src/components/atoms/index.ts @@ -8,3 +8,4 @@ export * from './links'; export * from './lists'; export * from './loaders'; export * from './modal'; +export * from './sidebar'; diff --git a/src/components/atoms/layout/article/article.stories.tsx b/src/components/atoms/layout/article/article.stories.tsx new file mode 100644 index 0000000..4c5b158 --- /dev/null +++ b/src/components/atoms/layout/article/article.stories.tsx @@ -0,0 +1,34 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Article as ArticleComponent } from './article'; + +/** + * Article - Storybook Meta + */ +export default { + title: 'Atoms/Layout', + component: ArticleComponent, + argTypes: { + children: { + control: { + type: 'text', + }, + description: 'The contents.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta<typeof ArticleComponent>; + +const Template: ComponentStory<typeof ArticleComponent> = (args) => ( + <ArticleComponent {...args} /> +); + +/** + * Layout Stories - Article + */ +export const Article = Template.bind({}); +Article.args = { + children: 'The article content.', +}; diff --git a/src/components/atoms/layout/article/article.test.tsx b/src/components/atoms/layout/article/article.test.tsx new file mode 100644 index 0000000..2412fe3 --- /dev/null +++ b/src/components/atoms/layout/article/article.test.tsx @@ -0,0 +1,13 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { Article } from './article'; + +describe('Article', () => { + it('renders the contents of an article', () => { + const children = 'The article content.'; + + render(<Article>{children}</Article>); + + expect(rtlScreen.getByRole('article')).toHaveTextContent(children); + }); +}); diff --git a/src/components/atoms/layout/article/article.tsx b/src/components/atoms/layout/article/article.tsx new file mode 100644 index 0000000..8611e20 --- /dev/null +++ b/src/components/atoms/layout/article/article.tsx @@ -0,0 +1,23 @@ +import { + type ForwardRefRenderFunction, + type HTMLAttributes, + type ReactNode, + forwardRef, +} from 'react'; + +export type ArticleProps = HTMLAttributes<HTMLElement> & { + /** + * The article contents. + */ + children: ReactNode; +}; + +const ArticleWithRef: ForwardRefRenderFunction<HTMLElement, ArticleProps> = ( + props, + ref +) => <article {...props} ref={ref} />; + +/** + * Article component. + */ +export const Article = forwardRef(ArticleWithRef); diff --git a/src/components/atoms/layout/article/index.ts b/src/components/atoms/layout/article/index.ts new file mode 100644 index 0000000..a8e7c14 --- /dev/null +++ b/src/components/atoms/layout/article/index.ts @@ -0,0 +1 @@ +export * from './article'; diff --git a/src/components/atoms/layout/aside/aside.stories.tsx b/src/components/atoms/layout/aside/aside.stories.tsx new file mode 100644 index 0000000..a394d1b --- /dev/null +++ b/src/components/atoms/layout/aside/aside.stories.tsx @@ -0,0 +1,34 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Aside as AsideComponent } from './aside'; + +/** + * Aside - Storybook Meta + */ +export default { + title: 'Atoms/Layout', + component: AsideComponent, + argTypes: { + children: { + control: { + type: 'text', + }, + description: 'The contents.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta<typeof AsideComponent>; + +const Template: ComponentStory<typeof AsideComponent> = (args) => ( + <AsideComponent {...args} /> +); + +/** + * Layout Stories - Aside + */ +export const Aside = Template.bind({}); +Aside.args = { + children: 'The aside content.', +}; diff --git a/src/components/atoms/layout/aside/aside.test.tsx b/src/components/atoms/layout/aside/aside.test.tsx new file mode 100644 index 0000000..5c55589 --- /dev/null +++ b/src/components/atoms/layout/aside/aside.test.tsx @@ -0,0 +1,13 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { Aside } from './aside'; + +describe('Aside', () => { + it('renders the contents of an aside', () => { + const children = 'The aside content.'; + + render(<Aside>{children}</Aside>); + + expect(rtlScreen.getByRole('complementary')).toHaveTextContent(children); + }); +}); diff --git a/src/components/atoms/layout/aside/aside.tsx b/src/components/atoms/layout/aside/aside.tsx new file mode 100644 index 0000000..92a9bf2 --- /dev/null +++ b/src/components/atoms/layout/aside/aside.tsx @@ -0,0 +1,23 @@ +import { + type ForwardRefRenderFunction, + type HTMLAttributes, + type ReactNode, + forwardRef, +} from 'react'; + +export type AsideProps = HTMLAttributes<HTMLElement> & { + /** + * The aside contents. + */ + children: ReactNode; +}; + +const AsideWithRef: ForwardRefRenderFunction<HTMLElement, AsideProps> = ( + props, + ref +) => <aside {...props} ref={ref} />; + +/** + * Aside component. + */ +export const Aside = forwardRef(AsideWithRef); diff --git a/src/components/atoms/layout/aside/index.ts b/src/components/atoms/layout/aside/index.ts new file mode 100644 index 0000000..dc3bc0b --- /dev/null +++ b/src/components/atoms/layout/aside/index.ts @@ -0,0 +1 @@ +export * from './aside'; diff --git a/src/components/atoms/layout/footer/footer.stories.tsx b/src/components/atoms/layout/footer/footer.stories.tsx new file mode 100644 index 0000000..0df1647 --- /dev/null +++ b/src/components/atoms/layout/footer/footer.stories.tsx @@ -0,0 +1,34 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Footer as FooterComponent } from './footer'; + +/** + * Footer - Storybook Meta + */ +export default { + title: 'Atoms/Layout', + component: FooterComponent, + argTypes: { + children: { + control: { + type: 'text', + }, + description: 'The contents.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta<typeof FooterComponent>; + +const Template: ComponentStory<typeof FooterComponent> = (args) => ( + <FooterComponent {...args} /> +); + +/** + * Layout Stories - Footer + */ +export const Footer = Template.bind({}); +Footer.args = { + children: 'The footer content.', +}; diff --git a/src/components/atoms/layout/footer/footer.test.tsx b/src/components/atoms/layout/footer/footer.test.tsx new file mode 100644 index 0000000..15c6e6d --- /dev/null +++ b/src/components/atoms/layout/footer/footer.test.tsx @@ -0,0 +1,13 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { Footer } from './footer'; + +describe('Footer', () => { + it('renders the contents of a footer', () => { + const children = 'The footer content.'; + + render(<Footer>{children}</Footer>); + + expect(rtlScreen.getByRole('contentinfo')).toHaveTextContent(children); + }); +}); diff --git a/src/components/atoms/layout/footer/footer.tsx b/src/components/atoms/layout/footer/footer.tsx new file mode 100644 index 0000000..deb3956 --- /dev/null +++ b/src/components/atoms/layout/footer/footer.tsx @@ -0,0 +1,23 @@ +import { + type ForwardRefRenderFunction, + type HTMLAttributes, + type ReactNode, + forwardRef, +} from 'react'; + +export type FooterProps = HTMLAttributes<HTMLElement> & { + /** + * The footer contents. + */ + children: ReactNode; +}; + +const FooterWithRef: ForwardRefRenderFunction<HTMLElement, FooterProps> = ( + props, + ref +) => <footer {...props} ref={ref} />; + +/** + * Footer component. + */ +export const Footer = forwardRef(FooterWithRef); diff --git a/src/components/atoms/layout/footer/index.ts b/src/components/atoms/layout/footer/index.ts new file mode 100644 index 0000000..a058eae --- /dev/null +++ b/src/components/atoms/layout/footer/index.ts @@ -0,0 +1 @@ +export * from './footer'; diff --git a/src/components/atoms/layout/header/header.stories.tsx b/src/components/atoms/layout/header/header.stories.tsx new file mode 100644 index 0000000..3d98263 --- /dev/null +++ b/src/components/atoms/layout/header/header.stories.tsx @@ -0,0 +1,34 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Header as HeaderComponent } from './header'; + +/** + * Header - Storybook Meta + */ +export default { + title: 'Atoms/Layout', + component: HeaderComponent, + argTypes: { + children: { + control: { + type: 'text', + }, + description: 'The contents.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta<typeof HeaderComponent>; + +const Template: ComponentStory<typeof HeaderComponent> = (args) => ( + <HeaderComponent {...args} /> +); + +/** + * Layout Stories - Header + */ +export const Header = Template.bind({}); +Header.args = { + children: 'The header content.', +}; diff --git a/src/components/atoms/layout/header/header.test.tsx b/src/components/atoms/layout/header/header.test.tsx new file mode 100644 index 0000000..412073f --- /dev/null +++ b/src/components/atoms/layout/header/header.test.tsx @@ -0,0 +1,13 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { Header } from './header'; + +describe('Header', () => { + it('renders the contents of a header', () => { + const children = 'The header content.'; + + render(<Header>{children}</Header>); + + expect(rtlScreen.getByRole('banner')).toHaveTextContent(children); + }); +}); diff --git a/src/components/atoms/layout/header/header.tsx b/src/components/atoms/layout/header/header.tsx new file mode 100644 index 0000000..e4837a6 --- /dev/null +++ b/src/components/atoms/layout/header/header.tsx @@ -0,0 +1,23 @@ +import { + type ForwardRefRenderFunction, + type HTMLAttributes, + type ReactNode, + forwardRef, +} from 'react'; + +export type HeaderProps = HTMLAttributes<HTMLElement> & { + /** + * The header contents. + */ + children: ReactNode; +}; + +const HeaderWithRef: ForwardRefRenderFunction<HTMLElement, HeaderProps> = ( + props, + ref +) => <header {...props} ref={ref} />; + +/** + * Header component. + */ +export const Header = forwardRef(HeaderWithRef); diff --git a/src/components/atoms/layout/header/index.ts b/src/components/atoms/layout/header/index.ts new file mode 100644 index 0000000..677ca79 --- /dev/null +++ b/src/components/atoms/layout/header/index.ts @@ -0,0 +1 @@ +export * from './header'; diff --git a/src/components/atoms/layout/index.ts b/src/components/atoms/layout/index.ts index 8cbc597..c53671a 100644 --- a/src/components/atoms/layout/index.ts +++ b/src/components/atoms/layout/index.ts @@ -1,7 +1,11 @@ +export * from './article'; +export * from './aside'; export * from './column'; export * from './copyright'; +export * from './footer'; +export * from './header'; export * from './main'; +export * from './nav'; export * from './no-script'; export * from './notice'; export * from './section'; -export * from './sidebar'; diff --git a/src/components/atoms/layout/main.test.tsx b/src/components/atoms/layout/main.test.tsx deleted file mode 100644 index 58dc660..0000000 --- a/src/components/atoms/layout/main.test.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; -import { Main } from './main'; - -const id = 'main'; -const children = 'The main content.'; - -describe('Main', () => { - it('renders the content of main element', () => { - render(<Main id={id}>{children}</Main>); - expect(screen.getByRole('main')).toHaveTextContent(children); - }); -}); diff --git a/src/components/atoms/layout/main.tsx b/src/components/atoms/layout/main.tsx deleted file mode 100644 index 919b25a..0000000 --- a/src/components/atoms/layout/main.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { FC, HTMLAttributes, ReactNode } from 'react'; - -export type MainProps = HTMLAttributes<HTMLElement> & { - /** - * The main body. - */ - children: ReactNode; -}; - -/** - * Main component - * - * Render a main element. - */ -export const Main: FC<MainProps> = ({ children, ...props }) => { - return <main {...props}>{children}</main>; -}; diff --git a/src/components/atoms/layout/main/index.ts b/src/components/atoms/layout/main/index.ts new file mode 100644 index 0000000..aad1ca8 --- /dev/null +++ b/src/components/atoms/layout/main/index.ts @@ -0,0 +1 @@ +export * from './main'; diff --git a/src/components/atoms/layout/main.stories.tsx b/src/components/atoms/layout/main/main.stories.tsx index ef4d728..6a22ed5 100644 --- a/src/components/atoms/layout/main.stories.tsx +++ b/src/components/atoms/layout/main/main.stories.tsx @@ -1,4 +1,4 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; import { Main as MainComponent } from './main'; /** @@ -54,5 +54,4 @@ const Template: ComponentStory<typeof MainComponent> = (args) => ( export const Main = Template.bind({}); Main.args = { children: 'The main content.', - id: '#main', }; diff --git a/src/components/atoms/layout/main/main.test.tsx b/src/components/atoms/layout/main/main.test.tsx new file mode 100644 index 0000000..ffd4e2d --- /dev/null +++ b/src/components/atoms/layout/main/main.test.tsx @@ -0,0 +1,13 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { Main } from './main'; + +describe('Main', () => { + it('renders the contents of the main element', () => { + const children = 'The main content.'; + + render(<Main>{children}</Main>); + + expect(rtlScreen.getByRole('main')).toHaveTextContent(children); + }); +}); diff --git a/src/components/atoms/layout/main/main.tsx b/src/components/atoms/layout/main/main.tsx new file mode 100644 index 0000000..5854900 --- /dev/null +++ b/src/components/atoms/layout/main/main.tsx @@ -0,0 +1,29 @@ +import { + forwardRef, + type HTMLAttributes, + type ReactNode, + type ForwardRefRenderFunction, +} from 'react'; + +export type MainProps = HTMLAttributes<HTMLElement> & { + /** + * The main body. + */ + children: ReactNode; +}; + +const MainWithRef: ForwardRefRenderFunction<HTMLElement, MainProps> = ( + { children, ...props }, + ref +) => ( + <main {...props} ref={ref}> + {children} + </main> +); + +/** + * Main component + * + * Render a main element. + */ +export const Main = forwardRef(MainWithRef); diff --git a/src/components/atoms/layout/nav/index.ts b/src/components/atoms/layout/nav/index.ts new file mode 100644 index 0000000..38d6745 --- /dev/null +++ b/src/components/atoms/layout/nav/index.ts @@ -0,0 +1 @@ +export * from './nav'; diff --git a/src/components/atoms/layout/nav/nav.stories.tsx b/src/components/atoms/layout/nav/nav.stories.tsx new file mode 100644 index 0000000..5649abf --- /dev/null +++ b/src/components/atoms/layout/nav/nav.stories.tsx @@ -0,0 +1,34 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Nav as NavComponent } from './nav'; + +/** + * Nav - Storybook Meta + */ +export default { + title: 'Atoms/Layout', + component: NavComponent, + argTypes: { + children: { + control: { + type: 'text', + }, + description: 'The contents.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta<typeof NavComponent>; + +const Template: ComponentStory<typeof NavComponent> = (args) => ( + <NavComponent {...args} /> +); + +/** + * Layout Stories - Nav + */ +export const Nav = Template.bind({}); +Nav.args = { + children: 'The nav content.', +}; diff --git a/src/components/atoms/layout/nav/nav.test.tsx b/src/components/atoms/layout/nav/nav.test.tsx new file mode 100644 index 0000000..4d8c942 --- /dev/null +++ b/src/components/atoms/layout/nav/nav.test.tsx @@ -0,0 +1,13 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { Nav } from './nav'; + +describe('Nav', () => { + it('renders the contents of a nav', () => { + const children = 'The nav content.'; + + render(<Nav>{children}</Nav>); + + expect(rtlScreen.getByRole('navigation')).toHaveTextContent(children); + }); +}); diff --git a/src/components/atoms/layout/nav/nav.tsx b/src/components/atoms/layout/nav/nav.tsx new file mode 100644 index 0000000..38969de --- /dev/null +++ b/src/components/atoms/layout/nav/nav.tsx @@ -0,0 +1,23 @@ +import { + type ForwardRefRenderFunction, + type HTMLAttributes, + type ReactNode, + forwardRef, +} from 'react'; + +export type NavProps = HTMLAttributes<HTMLElement> & { + /** + * The nav contents. + */ + children: ReactNode; +}; + +const NavWithRef: ForwardRefRenderFunction<HTMLElement, NavProps> = ( + props, + ref +) => <nav {...props} ref={ref} />; + +/** + * Nav component. + */ +export const Nav = forwardRef(NavWithRef); diff --git a/src/components/atoms/layout/sidebar.tsx b/src/components/atoms/layout/sidebar.tsx deleted file mode 100644 index d290069..0000000 --- a/src/components/atoms/layout/sidebar.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { FC, HTMLAttributes, ReactNode } from 'react'; -import styles from './sidebar.module.scss'; - -export type SidebarProps = HTMLAttributes<HTMLElement> & { - /** - * The sidebar body. - */ - children: ReactNode; -}; - -/** - * Sidebar component - * - * Render an aside element. - */ -export const Sidebar: FC<SidebarProps> = ({ - children, - className = '', - ...props -}) => { - return ( - <aside {...props} className={`${styles.wrapper} ${className}`}> - <div className={styles.body}>{children}</div> - </aside> - ); -}; diff --git a/src/components/atoms/sidebar/index.ts b/src/components/atoms/sidebar/index.ts new file mode 100644 index 0000000..b2ba9a4 --- /dev/null +++ b/src/components/atoms/sidebar/index.ts @@ -0,0 +1 @@ +export * from './sidebar'; diff --git a/src/components/atoms/layout/sidebar.module.scss b/src/components/atoms/sidebar/sidebar.module.scss index 31adb6f..31adb6f 100644 --- a/src/components/atoms/layout/sidebar.module.scss +++ b/src/components/atoms/sidebar/sidebar.module.scss diff --git a/src/components/atoms/layout/sidebar.stories.tsx b/src/components/atoms/sidebar/sidebar.stories.tsx index f85e468..4debb41 100644 --- a/src/components/atoms/layout/sidebar.stories.tsx +++ b/src/components/atoms/sidebar/sidebar.stories.tsx @@ -1,4 +1,4 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; import { Sidebar as SidebarComponent } from './sidebar'; /** diff --git a/src/components/atoms/layout/sidebar.test.tsx b/src/components/atoms/sidebar/sidebar.test.tsx index 12b2f61..13ee03a 100644 --- a/src/components/atoms/layout/sidebar.test.tsx +++ b/src/components/atoms/sidebar/sidebar.test.tsx @@ -1,5 +1,5 @@ import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; +import { render, screen as rtlScreen } from '../../../../tests/utils'; import { Sidebar } from './sidebar'; const children = 'A widget'; @@ -7,6 +7,6 @@ const children = 'A widget'; describe('Sidebar', () => { it('renders an aside element', () => { render(<Sidebar>{children}</Sidebar>); - expect(screen.getByRole('complementary')).toHaveTextContent(children); + expect(rtlScreen.getByRole('complementary')).toHaveTextContent(children); }); }); diff --git a/src/components/atoms/sidebar/sidebar.tsx b/src/components/atoms/sidebar/sidebar.tsx new file mode 100644 index 0000000..2ee53c6 --- /dev/null +++ b/src/components/atoms/sidebar/sidebar.tsx @@ -0,0 +1,22 @@ +import type { FC } from 'react'; +import { Aside, type AsideProps } from '../layout'; +import styles from './sidebar.module.scss'; + +export type SidebarProps = AsideProps; + +/** + * Sidebar component + */ +export const Sidebar: FC<SidebarProps> = ({ + children, + className = '', + ...props +}) => { + const sidebarClass = `${styles.wrapper} ${className}`; + + return ( + <Aside {...props} className={sidebarClass}> + <div className={styles.body}>{children}</div> + </Aside> + ); +}; diff --git a/src/components/molecules/layout/page-footer.tsx b/src/components/molecules/layout/page-footer.tsx index 786fca0..5f3b176 100644 --- a/src/components/molecules/layout/page-footer.tsx +++ b/src/components/molecules/layout/page-footer.tsx @@ -1,11 +1,8 @@ -import { FC } from 'react'; -import { Meta, MetaData } from './meta'; +import type { FC } from 'react'; +import { Footer, type FooterProps } from '../../atoms'; +import { Meta, type MetaData } from './meta'; -export type PageFooterProps = { - /** - * Set additional classnames to the footer element. - */ - className?: string; +export type PageFooterProps = Omit<FooterProps, 'children'> & { /** * The footer metadata. */ @@ -15,12 +12,10 @@ export type PageFooterProps = { /** * PageFooter component * - * Render a footer element to display page meta. + * Render a footer to display page meta. */ -export const PageFooter: FC<PageFooterProps> = ({ meta, ...props }) => { - return ( - <footer {...props}> - {meta && <Meta data={meta} withSeparator={false} />} - </footer> - ); -}; +export const PageFooter: FC<PageFooterProps> = ({ meta, ...props }) => ( + <Footer {...props}> + {meta ? <Meta data={meta} withSeparator={false} /> : null} + </Footer> +); diff --git a/src/components/molecules/layout/page-header.tsx b/src/components/molecules/layout/page-header.tsx index 04f2966..92650c5 100644 --- a/src/components/molecules/layout/page-header.tsx +++ b/src/components/molecules/layout/page-header.tsx @@ -1,5 +1,5 @@ import type { FC, ReactNode } from 'react'; -import { Heading } from '../../atoms'; +import { Header, Heading } from '../../atoms'; import { Meta, type MetaData } from './meta'; import styles from './page-header.module.scss'; @@ -50,7 +50,7 @@ export const PageHeader: FC<PageHeaderProps> = ({ }; return ( - <header className={headerClass}> + <Header className={headerClass}> <div className={styles.body}> <Heading className={styles.title} level={1}> {title} @@ -67,6 +67,6 @@ export const PageHeader: FC<PageHeaderProps> = ({ ) : null} {intro ? getIntro() : null} </div> - </header> + </Header> ); }; diff --git a/src/components/molecules/nav/index.ts b/src/components/molecules/nav/index.ts index 68efe4e..9c46050 100644 --- a/src/components/molecules/nav/index.ts +++ b/src/components/molecules/nav/index.ts @@ -1,3 +1,3 @@ export * from './breadcrumb'; -export * from './nav'; +export * from './nav-list'; export * from './pagination'; diff --git a/src/components/molecules/nav/nav.module.scss b/src/components/molecules/nav/nav-list.module.scss index a6d43bc..a6d43bc 100644 --- a/src/components/molecules/nav/nav.module.scss +++ b/src/components/molecules/nav/nav-list.module.scss diff --git a/src/components/molecules/nav/nav.stories.tsx b/src/components/molecules/nav/nav-list.stories.tsx index d343528..110a6ca 100644 --- a/src/components/molecules/nav/nav.stories.tsx +++ b/src/components/molecules/nav/nav-list.stories.tsx @@ -1,13 +1,13 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; import { Envelop, Home } from '../../atoms'; -import { Nav, type NavItem } from './nav'; +import { NavList, type NavItem } from './nav-list'; /** * Nav - Storybook Meta */ export default { title: 'Molecules/Navigation/Nav', - component: Nav, + component: NavList, argTypes: { 'aria-label': { control: { @@ -71,9 +71,11 @@ export default { }, }, }, -} as ComponentMeta<typeof Nav>; +} as ComponentMeta<typeof NavList>; -const Template: ComponentStory<typeof Nav> = (args) => <Nav {...args} />; +const Template: ComponentStory<typeof NavList> = (args) => ( + <NavList {...args} /> +); const MainNavItems: NavItem[] = [ { id: 'homeLink', href: '/', label: 'Home', logo: <Home /> }, diff --git a/src/components/molecules/nav/nav.test.tsx b/src/components/molecules/nav/nav-list.test.tsx index 2a6dc84..58437cb 100644 --- a/src/components/molecules/nav/nav.test.tsx +++ b/src/components/molecules/nav/nav-list.test.tsx @@ -1,7 +1,7 @@ import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; +import { render, screen as rtlScreen } from '../../../../tests/utils'; import { Envelop, Home } from '../../atoms'; -import { Nav, type NavItem } from './nav'; +import { NavList, type NavItem } from './nav-list'; const navItems: NavItem[] = [ { id: 'homeLink', href: '/', label: 'Home', logo: <Home /> }, @@ -10,19 +10,19 @@ const navItems: NavItem[] = [ describe('Nav', () => { it('renders a main navigation', () => { - render(<Nav kind="main" items={navItems} />); - expect(screen.getByRole('navigation')).toHaveClass('nav--main'); + render(<NavList kind="main" items={navItems} />); + expect(rtlScreen.getByRole('navigation')).toHaveClass('nav--main'); }); it('renders a footer navigation', () => { - render(<Nav kind="footer" items={navItems} />); - expect(screen.getByRole('navigation')).toHaveClass('nav--footer'); + render(<NavList kind="footer" items={navItems} />); + expect(rtlScreen.getByRole('navigation')).toHaveClass('nav--footer'); }); it('renders navigation links', () => { - render(<Nav kind="main" items={navItems} />); + render(<NavList kind="main" items={navItems} />); expect( - screen.getByRole('link', { name: navItems[0].label }) + rtlScreen.getByRole('link', { name: navItems[0].label }) ).toHaveAttribute('href', navItems[0].href); }); }); diff --git a/src/components/molecules/nav/nav.tsx b/src/components/molecules/nav/nav-list.tsx index 8214d94..59556ce 100644 --- a/src/components/molecules/nav/nav.tsx +++ b/src/components/molecules/nav/nav-list.tsx @@ -1,6 +1,6 @@ -import { FC, ReactNode } from 'react'; -import { Link, NavLink } from '../../atoms'; -import styles from './nav.module.scss'; +import type { FC, ReactNode } from 'react'; +import { Link, Nav, NavLink, type NavProps } from '../../atoms'; +import styles from './nav-list.module.scss'; export type NavItem = { /** @@ -21,15 +21,7 @@ export type NavItem = { logo?: ReactNode; }; -export type NavProps = { - /** - * An accessible name. - */ - 'aria-label'?: string; - /** - * Set additional classnames to the navigation wrapper. - */ - className?: string; +export type NavListProps = Omit<NavProps, 'children'> & { /** * The navigation items. */ @@ -49,7 +41,7 @@ export type NavProps = { * * Render the nav links. */ -export const Nav: FC<NavProps> = ({ +export const NavList: FC<NavListProps> = ({ className = '', items, kind, @@ -57,13 +49,15 @@ export const Nav: FC<NavProps> = ({ ...props }) => { const kindClass = `nav--${kind}`; + const navClass = `${styles[kindClass]} ${className}`; + const listClass = `${styles.nav__list} ${listClassName}`; /** * Get the nav items. * @returns {JSX.Element[]} An array of nav items. */ - const getItems = (): JSX.Element[] => { - return items.map(({ id, href, label, logo }) => ( + const getItems = (): JSX.Element[] => + items.map(({ id, href, label, logo }) => ( <li key={id} className={styles.nav__item}> {kind === 'main' ? ( <NavLink href={href} label={label} logo={logo} /> @@ -72,11 +66,10 @@ export const Nav: FC<NavProps> = ({ )} </li> )); - }; return ( - <nav {...props} className={`${styles[kindClass]} ${className}`}> - <ul className={`${styles.nav__list} ${listClassName}`}>{getItems()}</ul> - </nav> + <Nav {...props} className={navClass}> + <ul className={listClass}>{getItems()}</ul> + </Nav> ); }; diff --git a/src/components/organisms/layout/footer.test.tsx b/src/components/organisms/layout/footer.test.tsx deleted file mode 100644 index 51f21fb..0000000 --- a/src/components/organisms/layout/footer.test.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; -import { Footer, type FooterProps } from './footer'; - -const copyright: FooterProps['copyright'] = { - dates: { start: '2017', end: '2022' }, - owner: 'Lorem ipsum', - icon: 'CC', -}; - -const navItems: FooterProps['navItems'] = [ - { id: 'legal-notice', href: '#', label: 'Legal notice' }, -]; - -describe('Footer', () => { - it('renders the website copyright', () => { - render(<Footer copyright={copyright} topId="top" />); - expect(screen.getByText(copyright.owner)).toBeInTheDocument(); - }); - - it('renders a back to top link', () => { - render(<Footer copyright={copyright} topId="top" />); - expect( - screen.getByRole('link', { name: 'Back to top' }) - ).toBeInTheDocument(); - }); - - it('renders some nav items', () => { - render(<Footer copyright={copyright} navItems={navItems} topId="top" />); - expect( - screen.getByRole('link', { name: navItems[0].label }) - ).toBeInTheDocument(); - }); -}); diff --git a/src/components/organisms/layout/index.ts b/src/components/organisms/layout/index.ts index 35061cb..1351537 100644 --- a/src/components/organisms/layout/index.ts +++ b/src/components/organisms/layout/index.ts @@ -1,9 +1,9 @@ export * from './cards-list'; export * from './comment'; export * from './comments-list'; -export * from './footer'; -export * from './header'; export * from './no-results'; export * from './overview'; export * from './posts-list'; +export * from './site-footer'; +export * from './site-header'; export * from './summary'; diff --git a/src/components/organisms/layout/footer.module.scss b/src/components/organisms/layout/site-footer.module.scss index a8bcd61..a8bcd61 100644 --- a/src/components/organisms/layout/footer.module.scss +++ b/src/components/organisms/layout/site-footer.module.scss diff --git a/src/components/organisms/layout/footer.stories.tsx b/src/components/organisms/layout/site-footer.stories.tsx index b5097dd..59a378f 100644 --- a/src/components/organisms/layout/footer.stories.tsx +++ b/src/components/organisms/layout/site-footer.stories.tsx @@ -1,12 +1,12 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { Footer as FooterComponent } from './footer'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { SiteFooter as SiteFooterComponent } from './site-footer'; /** - * Footer - Storybook Meta + * SiteFooter - Storybook Meta */ export default { title: 'Organisms/Layout', - component: FooterComponent, + component: SiteFooterComponent, argTypes: { backToTopClassName: { control: { @@ -65,10 +65,10 @@ export default { }, }, }, -} as ComponentMeta<typeof FooterComponent>; +} as ComponentMeta<typeof SiteFooterComponent>; -const Template: ComponentStory<typeof FooterComponent> = (args) => ( - <FooterComponent {...args} /> +const Template: ComponentStory<typeof SiteFooterComponent> = (args) => ( + <SiteFooterComponent {...args} /> ); const copyright = { @@ -80,10 +80,10 @@ const copyright = { const navItems = [{ id: 'legal-notice', href: '#', label: 'Legal notice' }]; /** - * Layout Stories - Footer + * Layout Stories - SiteFooter */ -export const Footer = Template.bind({}); -Footer.args = { +export const SiteFooter = Template.bind({}); +SiteFooter.args = { copyright, navItems, topId: 'top', diff --git a/src/components/organisms/layout/site-footer.test.tsx b/src/components/organisms/layout/site-footer.test.tsx new file mode 100644 index 0000000..ffdeb7a --- /dev/null +++ b/src/components/organisms/layout/site-footer.test.tsx @@ -0,0 +1,36 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '../../../../tests/utils'; +import { SiteFooter, type SiteFooterProps } from './site-footer'; + +const copyright: SiteFooterProps['copyright'] = { + dates: { start: '2017', end: '2022' }, + owner: 'Lorem ipsum', + icon: 'CC', +}; + +const navItems: SiteFooterProps['navItems'] = [ + { id: 'legal-notice', href: '#', label: 'Legal notice' }, +]; + +describe('SiteFooter', () => { + it('renders the website copyright', () => { + render(<SiteFooter copyright={copyright} topId="top" />); + expect(rtlScreen.getByText(copyright.owner)).toBeInTheDocument(); + }); + + it('renders a back to top link', () => { + render(<SiteFooter copyright={copyright} topId="top" />); + expect( + rtlScreen.getByRole('link', { name: 'Back to top' }) + ).toBeInTheDocument(); + }); + + it('renders some nav items', () => { + render( + <SiteFooter copyright={copyright} navItems={navItems} topId="top" /> + ); + expect( + rtlScreen.getByRole('link', { name: navItems[0].label }) + ).toBeInTheDocument(); + }); +}); diff --git a/src/components/organisms/layout/footer.tsx b/src/components/organisms/layout/site-footer.tsx index 36e85a7..c1fe9d0 100644 --- a/src/components/organisms/layout/footer.tsx +++ b/src/components/organisms/layout/site-footer.tsx @@ -1,15 +1,15 @@ import type { FC } from 'react'; import { useIntl } from 'react-intl'; -import { Copyright, type CopyrightProps } from '../../atoms'; +import { Copyright, Footer, type CopyrightProps } from '../../atoms'; import { BackToTop, type BackToTopProps, - Nav, + NavList, type NavItem, } from '../../molecules'; -import styles from './footer.module.scss'; +import styles from './site-footer.module.scss'; -export type FooterProps = { +export type SiteFooterProps = { /** * Set additional classnames to the back to top button. */ @@ -33,11 +33,11 @@ export type FooterProps = { }; /** - * Footer component + * SiteFooter component * * Renders a footer with copyright and nav; */ -export const Footer: FC<FooterProps> = ({ +export const SiteFooter: FC<SiteFooterProps> = ({ backToTopClassName, className = '', copyright, @@ -47,29 +47,29 @@ export const Footer: FC<FooterProps> = ({ const intl = useIntl(); const ariaLabel = intl.formatMessage({ defaultMessage: 'Footer', - description: 'Footer: an accessible name for footer nav', - id: 'd4N8nD', + description: 'SiteFooter: an accessible name for the footer nav', + id: 'pRzkFR', }); const footerClass = `${styles.wrapper} ${className}`; const btnClass = `${styles['back-to-top']} ${backToTopClassName}`; return ( - <footer className={footerClass}> + <Footer className={footerClass}> <Copyright dates={copyright.dates} icon={copyright.icon} owner={copyright.owner} /> {navItems ? ( - <Nav + <NavList aria-label={ariaLabel} className={styles.nav} items={navItems} - // eslint-disable-next-line react/jsx-no-literals -- Hardcoded config + // eslint-disable-next-line react/jsx-no-literals -- Kind allowed kind="footer" /> ) : null} <BackToTop className={btnClass} to={topId} /> - </footer> + </Footer> ); }; diff --git a/src/components/organisms/layout/header.module.scss b/src/components/organisms/layout/site-header.module.scss index 573d455..573d455 100644 --- a/src/components/organisms/layout/header.module.scss +++ b/src/components/organisms/layout/site-header.module.scss diff --git a/src/components/organisms/layout/header.stories.tsx b/src/components/organisms/layout/site-header.stories.tsx index 31fb0ca..56f1689 100644 --- a/src/components/organisms/layout/header.stories.tsx +++ b/src/components/organisms/layout/site-header.stories.tsx @@ -1,12 +1,12 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { Header as HeaderComponent } from './header'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { SiteHeader as SiteHeaderComponent } from './site-header'; /** - * Header - Storybook Meta + * SiteHeader - Storybook Meta */ export default { title: 'Organisms/Layout', - component: HeaderComponent, + component: SiteHeaderComponent, args: { ackeeStorageKey: 'ackee-tracking', isHome: false, @@ -129,10 +129,10 @@ export default { parameters: { layout: 'fullscreen', }, -} as ComponentMeta<typeof HeaderComponent>; +} as ComponentMeta<typeof SiteHeaderComponent>; -const Template: ComponentStory<typeof HeaderComponent> = (args) => ( - <HeaderComponent {...args} /> +const Template: ComponentStory<typeof SiteHeaderComponent> = (args) => ( + <SiteHeaderComponent {...args} /> ); const nav = [ @@ -143,10 +143,10 @@ const nav = [ ]; /** - * Layout Stories - Header + * Layout Stories - SiteHeader */ -export const Header = Template.bind({}); -Header.args = { +export const SiteHeader = Template.bind({}); +SiteHeader.args = { nav, photo: 'http://placeimg.com/640/480/people', title: 'Website title', diff --git a/src/components/organisms/layout/header.test.tsx b/src/components/organisms/layout/site-header.test.tsx index 7f72f24..e75f99f 100644 --- a/src/components/organisms/layout/header.test.tsx +++ b/src/components/organisms/layout/site-header.test.tsx @@ -1,6 +1,6 @@ import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; -import { Header } from './header'; +import { render, screen as rtlScreen } from '../../../../tests/utils'; +import { SiteHeader } from './site-header'; const nav = [ { id: 'home-link', href: '#', label: 'Home' }, @@ -13,10 +13,10 @@ const photo = 'http://placeimg.com/640/480/nightlife'; const title = 'Assumenda quis quod'; -describe('Header', () => { +describe('SiteHeader', () => { it('renders the website title', () => { render( - <Header + <SiteHeader ackeeStorageKey="ackee-tracking" isHome={true} motionStorageKey="reduced-motion" @@ -27,13 +27,13 @@ describe('Header', () => { /> ); expect( - screen.getByRole('heading', { level: 1, name: title }) + rtlScreen.getByRole('heading', { level: 1, name: title }) ).toBeInTheDocument(); }); it('renders the main nav', () => { render( - <Header + <SiteHeader ackeeStorageKey="ackee-tracking" motionStorageKey="reduced-motion" nav={nav} @@ -42,6 +42,6 @@ describe('Header', () => { title={title} /> ); - expect(screen.getByRole('navigation')).toBeInTheDocument(); + expect(rtlScreen.getByRole('navigation')).toBeInTheDocument(); }); }); diff --git a/src/components/organisms/layout/header.tsx b/src/components/organisms/layout/site-header.tsx index d2f7620..e8953c0 100644 --- a/src/components/organisms/layout/header.tsx +++ b/src/components/organisms/layout/site-header.tsx @@ -1,9 +1,10 @@ -import { FC } from 'react'; +import type { FC } from 'react'; +import { Header } from '../../atoms'; import { Branding, type BrandingProps } from '../../molecules'; import { Toolbar, type ToolbarProps } from '../toolbar'; -import styles from './header.module.scss'; +import styles from './site-header.module.scss'; -export type HeaderProps = BrandingProps & +export type SiteHeaderProps = BrandingProps & Pick< ToolbarProps, 'ackeeStorageKey' | 'motionStorageKey' | 'nav' | 'searchPage' @@ -15,11 +16,11 @@ export type HeaderProps = BrandingProps & }; /** - * Header component + * SiteHeader component * * Render the website header. */ -export const Header: FC<HeaderProps> = ({ +export const SiteHeader: FC<SiteHeaderProps> = ({ ackeeStorageKey, className, motionStorageKey, @@ -27,8 +28,10 @@ export const Header: FC<HeaderProps> = ({ searchPage, ...props }) => { + const headerClass = `${styles.wrapper} ${className}`; + return ( - <header className={`${styles.wrapper} ${className}`}> + <Header className={headerClass}> <div className={styles.body}> <Branding {...props} /> <Toolbar @@ -39,6 +42,6 @@ export const Header: FC<HeaderProps> = ({ searchPage={searchPage} /> </div> - </header> + </Header> ); }; diff --git a/src/components/organisms/toolbar/main-nav.tsx b/src/components/organisms/toolbar/main-nav.tsx index 4182b4c..6933c44 100644 --- a/src/components/organisms/toolbar/main-nav.tsx +++ b/src/components/organisms/toolbar/main-nav.tsx @@ -1,4 +1,4 @@ -import { forwardRef, ForwardRefRenderFunction } from 'react'; +import { forwardRef, type ForwardRefRenderFunction } from 'react'; import { useIntl } from 'react-intl'; import { BooleanField, @@ -6,7 +6,7 @@ import { Hamburger, Label, } from '../../atoms'; -import { Nav, type NavProps, type NavItem } from '../../molecules'; +import { NavList, type NavListProps, type NavItem } from '../../molecules'; import mainNavStyles from './main-nav.module.scss'; import sharedStyles from './toolbar-items.module.scss'; @@ -14,7 +14,7 @@ export type MainNavProps = { /** * Set additional classnames to the nav element. */ - className?: NavProps['className']; + className?: NavListProps['className']; /** * The button state. */ @@ -64,7 +64,7 @@ const MainNavWithRef: ForwardRefRenderFunction<HTMLDivElement, MainNavProps> = ( > <Hamburger iconClassName={mainNavStyles.icon} /> </Label> - <Nav + <NavList className={`${sharedStyles.modal} ${mainNavStyles.modal} ${className}`} items={items} kind="main" diff --git a/src/components/templates/layout/layout.tsx b/src/components/templates/layout/layout.tsx index b284e29..810a019 100644 --- a/src/components/templates/layout/layout.tsx +++ b/src/components/templates/layout/layout.tsx @@ -28,10 +28,10 @@ import { PostsStack, } from '../../atoms'; import { - Footer, - type FooterProps, - Header, - type HeaderProps, + SiteFooter, + type SiteFooterProps, + SiteHeader, + type SiteHeaderProps, } from '../../organisms'; import styles from './layout.module.scss'; @@ -39,7 +39,7 @@ export type QueryAction = SearchAction & { 'query-input': string; }; -export type LayoutProps = Pick<HeaderProps, 'isHome'> & { +export type LayoutProps = Pick<SiteHeaderProps, 'isHome'> & { /** * The layout main content. */ @@ -118,7 +118,7 @@ export const Layout: FC<LayoutProps> = ({ id: 'AE4kCD', }); - const mainNav: HeaderProps['nav'] = [ + const mainNav: SiteHeaderProps['nav'] = [ { id: 'home', label: homeLabel, @@ -157,7 +157,7 @@ export const Layout: FC<LayoutProps> = ({ id: 'nwbzKm', }); - const footerNav: FooterProps['navItems'] = [ + const footerNav: SiteFooterProps['navItems'] = [ { id: 'legal-notice', label: legalNoticeLabel, href: ROUTES.LEGAL_NOTICE }, ]; @@ -239,7 +239,7 @@ export const Layout: FC<LayoutProps> = ({ <ButtonLink className="screen-reader-text" to="#main"> {skipToContent} </ButtonLink> - <Header + <SiteHeader // eslint-disable-next-line react/jsx-no-literals -- Storage key allowed ackeeStorageKey="ackee-tracking" baseline={baseline} @@ -261,7 +261,7 @@ export const Layout: FC<LayoutProps> = ({ {children} </article> </Main> - <Footer + <SiteFooter backToTopClassName={backToTopClassName} className={styles.footer} copyright={copyrightData} diff --git a/src/i18n/en.json b/src/i18n/en.json index 277ed23..fd46201 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -415,10 +415,6 @@ "defaultMessage": "CC BY SA", "description": "CCBySA: icon title" }, - "d4N8nD": { - "defaultMessage": "Footer", - "description": "Footer: an accessible name for footer nav" - }, "dDK5oc": { "defaultMessage": "{website} picture", "description": "Branding: photo alternative text" @@ -523,6 +519,10 @@ "defaultMessage": "Dark theme", "description": "PrismThemeToggle: dark theme label" }, + "pRzkFR": { + "defaultMessage": "Footer", + "description": "SiteFooter: an accessible name for the footer nav" + }, "pWKyyR": { "defaultMessage": "Off", "description": "MotionToggle: deactivate reduce motion label" diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 69f6b42..f951413 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -415,10 +415,6 @@ "defaultMessage": "CC BY SA", "description": "CCBySA: icon title" }, - "d4N8nD": { - "defaultMessage": "Pied de page", - "description": "Footer: an accessible name for footer nav" - }, "dDK5oc": { "defaultMessage": "Photo d’{website}", "description": "Branding: photo alternative text" @@ -523,6 +519,10 @@ "defaultMessage": "Thème sombre", "description": "PrismThemeToggle: dark theme label" }, + "pRzkFR": { + "defaultMessage": "Pied de page", + "description": "SiteFooter: an accessible name for the footer nav" + }, "pWKyyR": { "defaultMessage": "Arrêt", "description": "MotionToggle: deactivate reduce motion label" |
