Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(theme-classic): new configuration syntax for a simple footer #6132

Merged
merged 14 commits into from
Dec 20, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,68 @@ describe('themeConfig', () => {
});
});

test('should allow simple links in footer', () => {
const partialConfig = {
footer: {
links: [
{
label: 'Privacy',
href: 'https://opensource.facebook.com/legal/privacy/',
},
{
label: 'Terms',
href: 'https://opensource.facebook.com/legal/terms/',
},
{
label: 'Data Policy',
href: 'https://opensource.facebook.com/legal/data-policy/',
},
{
label: 'Cookie Policy',
href: 'https://opensource.facebook.com/legal/cookie-policy/',
},
],
},
};
const normalizedConfig = testValidateThemeConfig(partialConfig);

expect(normalizedConfig).toEqual({
...normalizedConfig,
footer: {
...normalizedConfig.footer,
...partialConfig.footer,
},
});
});

test('should reject mix of simple and multi-column links in footer', () => {
const partialConfig = {
footer: {
links: [
{
title: 'Learn',
items: [
{
label: 'Introduction',
to: 'docs',
},
],
},
{
label: 'Privacy',
href: 'https://opensource.facebook.com/legal/privacy/',
},
],
},
};

expect(() =>
testValidateThemeConfig(partialConfig),
).toThrowErrorMatchingInlineSnapshot(
`"\\"footer.links\\" does not match any of the allowed types"`,
christopherklint97 marked this conversation as resolved.
Show resolved Hide resolved
);
});

test('should allow width and height specification for logo ', () => {
const altTagConfig = {
navbar: {
Expand Down
116 changes: 82 additions & 34 deletions packages/docusaurus-theme-classic/src/theme/Footer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import React from 'react';
import clsx from 'clsx';

import Link from '@docusaurus/Link';
import {FooterLinkItem, useThemeConfig} from '@docusaurus/theme-common';
import {
FooterItem,
MultiColumnFooter,
SimpleFooter,
useThemeConfig,
} from '@docusaurus/theme-common';
import useBaseUrl from '@docusaurus/useBaseUrl';
import isInternalUrl from '@docusaurus/isInternalUrl';
import styles from './styles.module.css';
Expand All @@ -22,7 +27,7 @@ function FooterLink({
label,
prependBaseUrlToHref,
...props
}: FooterLinkItem) {
}: FooterItem) {
const toUrl = useBaseUrl(to);
const normalizedHref = useBaseUrl(href, {forcePrependBaseUrl: true});

Expand Down Expand Up @@ -66,6 +71,74 @@ function FooterLogo({
);
}

function MultiColumnLinks({
links,
}: {
links: Array<{title: string; items: FooterItem[]}>;
}) {
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: FooterItem[]}) {
return (
<div className="footer__links">
{links.map((item, key) => (
<>
{item.html ? (
<span
key={key}
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>
)}
</>
))}
</div>
);
}

function isMultiColumnFooterLinks(
links: MultiColumnFooter['links'] | SimpleFooter['links'],
): links is MultiColumnFooter['links'] {
return 'title' in links[0];
}

function Footer(): JSX.Element | null {
const {footer} = useThemeConfig();

Expand All @@ -84,39 +157,14 @@ function Footer(): JSX.Element | null {
className={clsx('footer', {
'footer--dark': footer.style === 'dark',
})}>
<div className="container">
<div className="container container-fluid">
{links && links.length > 0 && (
<div className="row footer__links">
{links.map((linkItem, i) => (
<div key={i} className="col footer__col">
{linkItem.title != null ? (
<div className="footer__title">{linkItem.title}</div>
) : null}
{linkItem.items != null &&
Array.isArray(linkItem.items) &&
linkItem.items.length > 0 ? (
<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>
) : null}
</div>
))}
<div className={clsx({row: 'title' in links[0]}, 'footer__links')}>
{isMultiColumnFooterLinks(links) ? (
<MultiColumnLinks links={links} />
) : (
<SimpleLinks links={links} />
)}
</div>
)}
{(logo || copyright) && (
Expand Down
59 changes: 41 additions & 18 deletions packages/docusaurus-theme-classic/src/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
ThemeConfig,
Navbar,
NavbarItem,
Footer,
MultiColumnFooter,
SimpleFooter,
FooterItem,
} from '@docusaurus/theme-common';

import {keyBy, chain} from 'lodash';
Expand Down Expand Up @@ -69,10 +71,12 @@ function translateNavbar(
};
}

function getFooterTranslationFile(footer: Footer): TranslationFileContent {
function getFooterTranslationFile(
footer: MultiColumnFooter | SimpleFooter,
): TranslationFileContent {
// TODO POC code
const footerLinkTitles: TranslationFileContent = chain(
footer.links.filter((link) => !!link.title),
(footer as MultiColumnFooter).links.filter((link) => !!link.title),
)
.keyBy((link) => `link.title.${link.title}`)
.mapValues((link) => ({
Expand All @@ -82,7 +86,9 @@ function getFooterTranslationFile(footer: Footer): TranslationFileContent {
.value();

const footerLinkLabels: TranslationFileContent = chain(
footer.links.flatMap((link) => link.items).filter((link) => !!link.label),
footer.links
.flatMap((link) => ('items' in link ? link.items : link))
.filter((link) => !!link.label),
)
.keyBy((linkItem) => `link.item.label.${linkItem.label}`)
.mapValues((linkItem) => ({
Expand All @@ -105,26 +111,43 @@ function getFooterTranslationFile(footer: Footer): TranslationFileContent {
return mergeTranslations([footerLinkTitles, footerLinkLabels, copyright]);
}
function translateFooter(
footer: Footer,
footer: MultiColumnFooter | SimpleFooter,
footerTranslations: TranslationFileContent,
): Footer {
const links = footer.links.map((link) => ({
...link,
title:
footerTranslations[`link.title.${link.title}`]?.message ?? link.title,
items: link.items.map((linkItem) => ({
...linkItem,
label:
footerTranslations[`link.item.label.${linkItem.label}`]?.message ??
linkItem.label,
})),
}));
): MultiColumnFooter | SimpleFooter {
const links = footer.links.map((link) => {
if ('title' in link) {
return {
...link,
title:
footerTranslations[`link.title.${link.title}`]?.message ?? link.title,
items: link.items.map((linkItem) => ({
...linkItem,
label:
footerTranslations[`link.item.label.${linkItem.label}`]?.message ??
linkItem.label,
})),
};
} else {
return {
...link,
label:
footerTranslations[`link.item.label.${link.label}`]?.message ??
link.label,
};
}
});

const copyright = footerTranslations.copyright?.message ?? footer.copyright;

return {
...footer,
links,
links:
'title' in links[0]
? (links as Array<{
title: string;
items: FooterItem[];
}>)
: (links as FooterItem[]),
copyright,
};
}
Expand Down
19 changes: 11 additions & 8 deletions packages/docusaurus-theme-classic/src/validateThemeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,14 +311,17 @@ const ThemeConfigSchema = Joi.object({
href: Joi.string(),
}),
copyright: Joi.string(),
links: Joi.array()
.items(
Joi.object({
title: Joi.string().allow(null),
items: Joi.array().items(FooterLinkItemSchema).default([]),
}),
)
.default([]),
links: [
Joi.array()
.items(
Joi.object({
title: Joi.string().allow(null),
items: Joi.array().items(FooterLinkItemSchema).default([]),
}),
)
.default([]),
Joi.array().items(FooterLinkItemSchema).default([]),
],
}).optional(),
prism: Joi.object({
theme: Joi.object({
Expand Down
6 changes: 3 additions & 3 deletions packages/docusaurus-theme-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ export type {
Navbar,
NavbarItem,
NavbarLogo,
Footer,
FooterLinks,
FooterLinkItem,
MultiColumnFooter,
SimpleFooter,
FooterItem,
ColorModeConfig,
} from './utils/useThemeConfig';

Expand Down
21 changes: 14 additions & 7 deletions packages/docusaurus-theme-common/src/utils/useThemeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,14 @@ export type PrismConfig = {
additionalLanguages?: string[];
};

export type FooterLinkItem = {
export type FooterItem = {
label?: string;
to?: string;
href?: string;
html?: string;
prependBaseUrlToHref?: string;
};
export type FooterLinks = {
title?: string;
items: FooterLinkItem[];
};

export type Footer = {
style: 'light' | 'dark';
logo?: {
Expand All @@ -88,7 +85,17 @@ export type Footer = {
href?: string;
};
copyright?: string;
links: FooterLinks[];
};

export type MultiColumnFooter = Footer & {
links: Array<{
title: string;
Copy link
Collaborator

@slorber slorber Dec 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The config validation allows undefined/null.

Undefined can lead to type guards thinking it's a simple footer despite being a multi column one:

image

null can lead to a weird rendering alignment:

image

Not sure who's using null, it may make sense to disable column titles and the alignment may be good enough if none of the columns have titles...


Edit: see use-case for null: #4267

Just adding default(null) to title validation + this type might be good enough.

items: FooterItem[];
}>;
};

export type SimpleFooter = Footer & {
links: FooterItem[];
};

export type TableOfContents = {
Expand All @@ -111,7 +118,7 @@ export type ThemeConfig = {
colorMode: ColorModeConfig;
announcementBar?: AnnouncementBarConfig;
prism: PrismConfig;
footer?: Footer;
footer?: MultiColumnFooter | SimpleFooter;
hideableSidebar: boolean;
image?: string;
metadata: Array<Record<string, string>>;
Expand Down
Loading