refactor(theme-classic): split theme footer into smaller components + swizzle config (#6894)

Co-authored-by: Joshua Chen <sidachen2003@gmail.com>
This commit is contained in:
Sébastien Lorber 2022-03-11 14:55:53 +01:00 committed by GitHub
parent c9ee6e467c
commit 1efc6c6091
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 416 additions and 180 deletions

View file

@ -37,11 +37,62 @@ export default function getSwizzleConfig(): SwizzleConfig {
},
Footer: {
actions: {
eject: 'unsafe', // TODO split footer into smaller parts
eject: 'safe',
wrap: 'safe',
},
description: "The footer component of you site's layout",
},
'Footer/Copyright': {
actions: {
eject: 'safe',
wrap: 'safe',
},
description: 'The footer copyright',
},
'Footer/Layout': {
actions: {
eject: 'safe',
wrap: 'safe',
},
description: 'The footer main layout component',
},
'Footer/LinkItem': {
actions: {
eject: 'safe',
wrap: 'safe',
},
description: 'The footer link item component',
},
'Footer/Links': {
actions: {
eject: 'safe',
wrap: 'safe',
},
description: 'The footer component rendering the footer links',
},
'Footer/Links/MultiColumn': {
actions: {
eject: 'safe',
wrap: 'safe',
},
description:
'The footer component rendering the footer links with a multi-column layout',
},
'Footer/Links/Simple': {
actions: {
eject: 'safe',
wrap: 'safe',
},
description:
'The footer component rendering the footer links with a simple layout (single row)',
},
'Footer/Logo': {
actions: {
eject: 'safe',
wrap: 'safe',
},
description: 'The footer logo',
},
IconArrow: {
actions: {
eject: 'safe',

View file

@ -205,7 +205,7 @@ declare module '@theme/DocSidebar/Desktop/Content' {
readonly sidebar: readonly PropSidebarItem[];
}
export default function CollapseButton(props: Props): JSX.Element;
export default function Content(props: Props): JSX.Element;
}
declare module '@theme/DocSidebar/Desktop/CollapseButton' {
@ -280,6 +280,77 @@ declare module '@theme/Footer' {
export default function Footer(): JSX.Element | null;
}
declare module '@theme/Footer/Logo' {
import type {FooterLogo} from '@docusaurus/theme-common';
export interface Props {
logo: FooterLogo;
}
export default function FooterLogo(props: Props): JSX.Element;
}
declare module '@theme/Footer/Copyright' {
export interface Props {
copyright: string;
}
export default function FooterCopyright(props: Props): JSX.Element;
}
declare module '@theme/Footer/LinkItem' {
import type {FooterLinkItem} from '@docusaurus/theme-common';
export interface Props {
item: FooterLinkItem;
}
export default function FooterLinkItem(props: Props): JSX.Element;
}
declare module '@theme/Footer/Layout' {
import type {ReactNode} from 'react';
export interface Props {
style: 'light' | 'dark';
links: ReactNode;
logo: ReactNode;
copyright: ReactNode;
}
export default function FooterLayout(props: Props): JSX.Element;
}
declare module '@theme/Footer/Links' {
import type {Footer} from '@docusaurus/theme-common';
export interface Props {
links: Footer['links'];
}
export default function FooterLinks(props: Props): JSX.Element;
}
declare module '@theme/Footer/Links/MultiColumn' {
import type {MultiColumnFooter} from '@docusaurus/theme-common';
export interface Props {
columns: MultiColumnFooter['links'];
}
export default function FooterLinksMultiColumn(props: Props): JSX.Element;
}
declare module '@theme/Footer/Links/Simple' {
import type {SimpleFooter} from '@docusaurus/theme-common';
export interface Props {
links: SimpleFooter['links'];
}
export default function FooterLinksSimple(props: Props): JSX.Element;
}
declare module '@theme/Heading' {
import type {ComponentProps} from 'react';

View file

@ -0,0 +1,22 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import type {Props} from '@theme/Footer/Copyright';
export default function FooterCopyright({copyright}: Props): JSX.Element {
return (
<div
className="footer__copyright"
// Developer provided the HTML, so assume it's safe.
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: copyright,
}}
/>
);
}

View file

@ -0,0 +1,34 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import clsx from 'clsx';
import type {Props} from '@theme/Footer/Layout';
export default function FooterLayout({
style,
links,
logo,
copyright,
}: Props): JSX.Element {
return (
<footer
className={clsx('footer', {
'footer--dark': style === 'dark',
})}>
<div className="container container-fluid">
{links}
{(logo || copyright) && (
<div className="footer__bottom text--center">
{logo && <div className="margin-bottom--sm">{logo}</div>}
{copyright}
</div>
)}
</div>
</footer>
);
}

View file

@ -0,0 +1,38 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Link from '@docusaurus/Link';
import useBaseUrl from '@docusaurus/useBaseUrl';
import isInternalUrl from '@docusaurus/isInternalUrl';
import IconExternalLink from '@theme/IconExternalLink';
import type {Props} from '@theme/Footer/LinkItem';
export default function FooterLinkItem({item}: Props): JSX.Element {
const {to, href, label, prependBaseUrlToHref, ...props} = item;
const toUrl = useBaseUrl(to);
const normalizedHref = useBaseUrl(href, {forcePrependBaseUrl: true});
return (
<Link
className="footer__link-item"
{...(href
? {
href: prependBaseUrlToHref ? normalizedHref : href,
}
: {
to: toUrl,
})}
{...props}>
<span>
{label}
{href && !isInternalUrl(href) && <IconExternalLink />}
</span>
</Link>
);
}

View file

@ -0,0 +1,53 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import type {Props} from '@theme/Footer/Links/MultiColumn';
import LinkItem from '@theme/Footer/LinkItem';
type ColumnType = Props['columns'][number];
type ColumnItemType = ColumnType['items'][number];
function ColumnLinkItem({item}: {item: ColumnItemType}) {
return item.html ? (
<li
className="footer__item"
// Developer provided the HTML, so assume it's safe.
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: item.html,
}}
/>
) : (
<li key={item.href || item.to} className="footer__item">
<LinkItem item={item} />
</li>
);
}
function Column({column}: {column: ColumnType}) {
return (
<div className="col footer__col">
<div className="footer__title">{column.title}</div>
<ul className="footer__items">
{column.items.map((item, i) => (
<ColumnLinkItem key={i} item={item} />
))}
</ul>
</div>
);
}
export default function FooterLinksMultiColumn({columns}: Props): JSX.Element {
return (
<div className="row footer__links">
{columns.map((column, i) => (
<Column key={i} column={column} />
))}
</div>
);
}

View file

@ -0,0 +1,44 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import type {Props} from '@theme/Footer/Links/Simple';
import LinkItem from '@theme/Footer/LinkItem';
function Separator() {
return <span className="footer__link-separator">·</span>;
}
function SimpleLinkItem({item}: {item: Props['links'][number]}) {
return item.html ? (
<span
className="footer__link-item"
// Developer provided the HTML, so assume it's safe.
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: item.html,
}}
/>
) : (
<LinkItem item={item} />
);
}
export default function FooterLinksSimple({links}: Props): JSX.Element {
return (
<div className="footer__links text--center">
<div className="footer__links">
{links.map((item, i) => (
<React.Fragment key={i}>
<SimpleLinkItem item={item} />
{links.length !== i + 1 && <Separator />}
</React.Fragment>
))}
</div>
</div>
);
}

View file

@ -0,0 +1,21 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import {isMultiColumnFooterLinks} from '@docusaurus/theme-common';
import type {Props} from '@theme/Footer/Links';
import FooterLinksMultiColumn from '@theme/Footer/Links/MultiColumn';
import FooterLinksSimple from '@theme/Footer/Links/Simple';
export default function FooterLinks({links}: Props): JSX.Element {
return isMultiColumnFooterLinks(links) ? (
<FooterLinksMultiColumn columns={links} />
) : (
<FooterLinksSimple links={links} />
);
}

View file

@ -0,0 +1,41 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Link from '@docusaurus/Link';
import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
import styles from './styles.module.css';
import ThemedImage from '@theme/ThemedImage';
import type {Props} from '@theme/Footer/Logo';
function LogoImage({logo}: Props) {
const {withBaseUrl} = useBaseUrlUtils();
const sources = {
light: withBaseUrl(logo.src),
dark: withBaseUrl(logo.srcDark ?? logo.src),
};
return (
<ThemedImage
className="footer__logo"
alt={logo.alt}
sources={sources}
width={logo.width}
height={logo.height}
/>
);
}
export default function FooterLogo({logo}: Props): JSX.Element {
return logo.href ? (
<Link href={logo.href} className={styles.footerLogoLink}>
<LogoImage logo={logo} />
</Link>
) : (
<LogoImage logo={logo} />
);
}

View file

@ -6,185 +6,27 @@
*/
import React from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import {
type FooterLinkItem,
useThemeConfig,
type MultiColumnFooter,
type SimpleFooter,
} from '@docusaurus/theme-common';
import useBaseUrl, {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
import isInternalUrl from '@docusaurus/isInternalUrl';
import styles from './styles.module.css';
import ThemedImage from '@theme/ThemedImage';
import IconExternalLink from '@theme/IconExternalLink';
function FooterLink({
to,
href,
label,
prependBaseUrlToHref,
...props
}: FooterLinkItem) {
const toUrl = useBaseUrl(to);
const normalizedHref = useBaseUrl(href, {forcePrependBaseUrl: true});
return (
<Link
className="footer__link-item"
{...(href
? {
href: prependBaseUrlToHref ? normalizedHref : href,
}
: {
to: toUrl,
})}
{...props}>
<span>
{label}
{href && !isInternalUrl(href) && <IconExternalLink />}
</span>
</Link>
);
}
function FooterLogo({logo}: {logo: SimpleFooter['logo']}) {
const {withBaseUrl} = useBaseUrlUtils();
if (!logo?.src) {
return null;
}
const sources = {
light: withBaseUrl(logo.src),
dark: withBaseUrl(logo.srcDark ?? logo.src),
};
const image = (
<ThemedImage
className="footer__logo"
alt={logo.alt}
sources={sources}
width={logo.width}
height={logo.height}
/>
);
return (
<div className="margin-bottom--sm">
{logo.href ? (
<Link href={logo.href} className={styles.footerLogoLink}>
{image}
</Link>
) : (
image
)}
</div>
);
}
function MultiColumnLinks({links}: {links: MultiColumnFooter['links']}) {
return (
<>
{links.map((linkItem, i) => (
<div key={i} className="col footer__col">
<div className="footer__title">{linkItem.title}</div>
<ul className="footer__items">
{linkItem.items.map((item, key) =>
item.html ? (
<li
key={key}
className="footer__item"
// Developer provided the HTML, so assume it's safe.
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: item.html,
}}
/>
) : (
<li key={item.href || item.to} className="footer__item">
<FooterLink {...item} />
</li>
),
)}
</ul>
</div>
))}
</>
);
}
function SimpleLinks({links}: {links: SimpleFooter['links']}) {
return (
<div className="footer__links">
{links.map((item, key) => (
<React.Fragment key={key}>
{item.html ? (
<span
className="footer__link-item"
// Developer provided the HTML, so assume it's safe.
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: item.html,
}}
/>
) : (
<FooterLink {...item} />
)}
{links.length !== key + 1 && (
<span className="footer__link-separator">·</span>
)}
</React.Fragment>
))}
</div>
);
}
function isMultiColumnFooterLinks(
links: MultiColumnFooter['links'] | SimpleFooter['links'],
): links is MultiColumnFooter['links'] {
return 'title' in links[0]!;
}
import {useThemeConfig} from '@docusaurus/theme-common';
import FooterLinks from '@theme/Footer/Links';
import FooterLogo from '@theme/Footer/Logo';
import FooterCopyright from '@theme/Footer/Copyright';
import FooterLayout from '@theme/Footer/Layout';
function Footer(): JSX.Element | null {
const {footer} = useThemeConfig();
if (!footer) {
return null;
}
const {copyright, links, logo} = footer;
const {copyright, links, logo, style} = footer;
return (
<footer
className={clsx('footer', {
'footer--dark': footer.style === 'dark',
})}>
<div className="container container-fluid">
{links &&
links.length > 0 &&
(isMultiColumnFooterLinks(links) ? (
<div className="row footer__links">
<MultiColumnLinks links={links} />
</div>
) : (
<div className="footer__links text--center">
<SimpleLinks links={links} />
</div>
))}
{(logo || copyright) && (
<div className="footer__bottom text--center">
<FooterLogo logo={logo} />
{copyright && (
<div
className="footer__copyright"
// Developer provided the HTML, so assume it's safe.
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: copyright,
}}
/>
)}
</div>
)}
</div>
</footer>
<FooterLayout
style={style}
links={links && links.length > 0 && <FooterLinks links={links} />}
logo={logo && <FooterLogo logo={logo} />}
copyright={copyright && <FooterCopyright copyright={copyright} />}
/>
);
}

View file

@ -20,6 +20,7 @@ export type {
MultiColumnFooter,
SimpleFooter,
Footer,
FooterLogo,
FooterLinkItem,
ColorModeConfig,
} from './utils/useThemeConfig';
@ -110,6 +111,8 @@ export {
type TOCTreeNode,
} from './utils/tocUtils';
export {isMultiColumnFooterLinks} from './utils/footerUtils';
export {
ScrollControllerProvider,
useScrollController,

View file

@ -0,0 +1,14 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type {MultiColumnFooter, SimpleFooter} from './useThemeConfig';
export function isMultiColumnFooterLinks(
links: MultiColumnFooter['links'] | SimpleFooter['links'],
): links is MultiColumnFooter['links'] {
return 'title' in links[0]!;
}

View file

@ -65,18 +65,20 @@ export type FooterLinkItem = {
href?: string;
html?: string;
prependBaseUrlToHref?: string;
} & Record<string, unknown>;
export type FooterLogo = {
alt?: string;
src: string;
srcDark?: string;
width?: string | number;
height?: string | number;
href?: string;
};
export type FooterBase = {
style: 'light' | 'dark';
logo?: {
alt?: string;
src: string;
srcDark?: string;
width?: string | number;
height?: string | number;
href?: string;
};
logo?: FooterLogo;
copyright?: string;
};