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(pie-docs): DSW-2268 adds dynamic display of index page cards #1730

Merged
merged 12 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clean-houses-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"pie-docs": minor
---

[Added] - Adds the Components index page using a shortcode that will dynamically generate a card for each component in the navigation menu
19 changes: 18 additions & 1 deletion apps/pie-docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Please follow our [Wiki documentation](https://github.com/justeattakeaway/pie/wi

### Adding Index pages
An index page represents content for one of our main navigation items, for example: Foundations or Components.
When a main navigation item does not have an index page, it will navigate to the first page under it. When it has content, it will open the accordion and navigate to that content.
When a main navigation item does not have an index page, it will navigate to the first page under it. When it has content, it will navigate to that content. In both cases the accordion will be opened, as well when clicking the right caret.

To set content for a main navigation item, in the item `.md` page, please add to the eleventyNavigation section at the top `hasIndexPage` like so:

Expand All @@ -51,6 +51,23 @@ eleventyNavigation:

Content needs to be added after this. If no content is added, it will navigate to a 404.

Index pages will have the same format, as such, we developed a mechanism to render link-card items for each navigation item under the Section.
The shortcode `indexPageDisplay` will automatically render each card and look for a matching image in `assets/images/index/<section-name>`. It can be configured like this:

```njk
{% indexPageDisplay {
collection: collections.all,
itemKey: "Components",
excludedElements: ['Component Status', 'Banner', 'Checkbox Group']
} %}
```

`collections.all` is an eleventy object that is used by the `eleventy-navigation`plugin to return a list of navigation items.
`itemKey` is the section name that the navigation plugin will search for.
`excludedElements` is a list of elements we would like to exclude from the list. Both for the key name and excluded items, names must follow the `key` attribute use in each page, including casing and white spaces when is 2 or more words.

When adding new index page content, just remember to add images in the right directory and exclude any items we don't wish to link in that page. Images for mobile will be automatically selected when available. For more information on how to name images and its directory, visit the jsdocs in the indexPageDisplay shortcode.

## Drafts

When building a page that is not yet ready for production we can mark the page as a `draft` by adding `draft: true` to the page front matter. This will allow 11ty to build the page during development mode but will exclude the page from builds during production.
Expand Down
2 changes: 2 additions & 0 deletions apps/pie-docs/src/_11ty/shortcodes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const componentStatusTable = require('./componentStatusTable');
const contentPageImage = require('./contentPageImage');
const { contentLayout, contentItem, contentWrapper } = require('./contentLayout');
const globalTokensWarning = require('./notifications/globalTokensWarning');
const indexPageDisplay = require('./indexPageDisplay');
const link = require('./link');
const list = require('./list');
const mediaElement = require('./mediaElementList');
Expand Down Expand Up @@ -32,6 +33,7 @@ const addAllShortCodes = (eleventyConfig) => {
eleventyConfig.addPairedShortcode('contentLayout', (shortcodeArgs) => deindentHTML(contentLayout(shortcodeArgs)));
eleventyConfig.addPairedShortcode('contentItem', (shortcodeArgs) => deindentHTML(contentItem(shortcodeArgs)));
eleventyConfig.addShortcode('globalTokensWarning', (shortcodeArgs) => deindentHTML(globalTokensWarning(shortcodeArgs)));
eleventyConfig.addShortcode('indexPageDisplay', (shortcodeArgs) => deindentHTML(indexPageDisplay(shortcodeArgs)));
eleventyConfig.addShortcode('link', (shortcodeArgs) => deindentHTML(link(shortcodeArgs)));
eleventyConfig.addShortcode('list', (shortcodeArgs) => deindentHTML(list(shortcodeArgs)));
eleventyConfig.addShortcode('mediaElementList', (shortcodeArgs) => deindentHTML(mediaElement(shortcodeArgs)));
Expand Down
78 changes: 78 additions & 0 deletions apps/pie-docs/src/_11ty/shortcodes/indexPageDisplay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const { find } = require('@11ty/eleventy-navigation').navigation;
const fs = require('fs');
const path = require('path');

/** IndexPageDisplay Shortcode - automatic page rendering:
* Render index items in a flexible grid.
* Every main item in our menu can now render an index page. This shortcode will loop on every child menu item and create an
* interactive card with a link to that menu item. Items can be excluded if specified. Images must exist for items not excluded.
* Mobile images are optional, desktop image will be rendered if mobile image is not provided.
* A default image will be rendered if desktop image is not provided and a warning is printed to the server console.
* Images must be added:
* - within a folder with the main menu item name, for example 'components/' 'foundations/'
* - image should have the name of the sub-menu in kebab-case, for example Checkbox Group is called 'checkbox-group'
* - mobile images will use the same name with a mobile keyword, for example 'checkbox-group-mobile'
* @param {string} collection - It is the object from 11y collections.all https://www.11ty.dev/docs/collections/
* @param {string} itemKey - Is the key of the element in the main menu, and the page we will be rendering e.g. components
* @param {string} excludedElements - A list of keys from pages that have to be excluded e.g. ['Component status']
* @param {string} imageSrcDirectory - Optional when need to replace the default directory
* @returns {string} a <div> element containing a list of cards representing all elements under the itemKey category
*/
const defaultImageDirectory = '../../assets/img/index';
const toSlug = (string) => string.toLowerCase().replace(/\s+/g, '-');

const getDraftPagesList = (collection) => {
const draftPages = [];
collection.forEach((item) => {
if (item.data && item.data.draft) draftPages.push(item.data.title);
});
return draftPages;
};

// Subpage Items navigate to first child if index page does not exist
const getUrl = (element) => {
if (element.subPageDropdown) {
if (element.hasIndexPage) return element.url;
return element.children[0].url;
}
return element.url;
};

module.exports = function ({

Check warning on line 41 in apps/pie-docs/src/_11ty/shortcodes/indexPageDisplay.js

View workflow job for this annotation

GitHub Actions / lint-js

Unexpected unnamed function
collection,
itemKey,
excludedElements,
imageSrcDirectory,
}) {
const menuItems = find(collection, itemKey);
const draftPages = getDraftPagesList(collection);

const indexElements = menuItems.map((element) => {
if (!excludedElements.includes(element.title) && !draftPages.includes(element.title)) {
const menuItemSlug = toSlug(element.title);
const itemKeySlug = toSlug(itemKey);
const imageDirectory = imageSrcDirectory || defaultImageDirectory;
const imgSrc = `${imageDirectory}/${itemKeySlug}/${menuItemSlug}.svg`;
const imgMobileSrc = `${imageDirectory}/${itemKeySlug}/${menuItemSlug}-mobile.svg`;

const hasSource = fs.existsSync(path.join(__dirname, imgSrc));
const hasMobileSource = fs.existsSync(path.join(__dirname, imgMobileSrc));

const throwOnMissingImage = () => {
throw new Error(`Image not provided for ${menuItemSlug}. Please ensure image is provided or add this item to ExcludedElements.`);
};

return `
<a class="c-indexPage-link" href="${getUrl(element)}">
<picture>
${hasMobileSource ? `<source media="(max-width: 600px)" srcset="${imgMobileSrc}">` : ''}
${hasSource ? `<img src="${imgSrc}">` : throwOnMissingImage()}
</picture>
${element.title}
<div class="c-indexPage-background"></div>
</a>`;
}
return '';
});
return `<div class="c-indexPage">${indexElements.join('')}</div>`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`indexPageDisplay.js > should not render items if the page is a draft 1`] = `
"<div class=\\"c-indexPage\\">
<a class=\\"c-indexPage-link\\" href=\\"/second-sub-item/\\">
<picture>

<img src=\\"../../__tests__/_11ty/shortcodes/test-index-images/test-menu-item/second-sub-item.svg\\">
</picture>
Second Sub Item
<div class=\\"c-indexPage-background\\"></div>
</a></div>"
`;

exports[`indexPageDisplay.js > should return the expected HTML for an index page 1`] = `
"<div class=\\"c-indexPage\\">
<a class=\\"c-indexPage-link\\" href=\\"/first-sub-item/\\">
<picture>

<img src=\\"../../__tests__/_11ty/shortcodes/test-index-images/test-menu-item/first-sub-item.svg\\">
</picture>
First Sub Item
<div class=\\"c-indexPage-background\\"></div>
</a>
<a class=\\"c-indexPage-link\\" href=\\"/second-sub-item/\\">
<picture>

<img src=\\"../../__tests__/_11ty/shortcodes/test-index-images/test-menu-item/second-sub-item.svg\\">
</picture>
Second Sub Item
<div class=\\"c-indexPage-background\\"></div>
</a></div>"
`;

exports[`indexPageDisplay.js > should return the expected HTML for an index page with excluded items 1`] = `
"<div class=\\"c-indexPage\\">
<a class=\\"c-indexPage-link\\" href=\\"/first-sub-item/\\">
<picture>

<img src=\\"../../__tests__/_11ty/shortcodes/test-index-images/test-menu-item/first-sub-item.svg\\">
</picture>
First Sub Item
<div class=\\"c-indexPage-background\\"></div>
</a></div>"
`;

exports[`indexPageDisplay.js > should use child url for subpage dropdown when it does not have an index page 1`] = `
"<div class=\\"c-indexPage\\">
<a class=\\"c-indexPage-link\\" href=\\"/second-sub-item/\\">
<picture>

<img src=\\"../../__tests__/_11ty/shortcodes/test-index-images/test-menu-item/second-sub-item.svg\\">
</picture>
Second Sub Item
<div class=\\"c-indexPage-background\\"></div>
</a>
<a class=\\"c-indexPage-link\\" href=\\"subpage-dropdown-child\\">
<picture>

<img src=\\"../../__tests__/_11ty/shortcodes/test-index-images/test-menu-item/subpage-dropdown.svg\\">
</picture>
Subpage dropdown
<div class=\\"c-indexPage-background\\"></div>
</a></div>"
`;
147 changes: 147 additions & 0 deletions apps/pie-docs/src/__tests__/_11ty/shortcodes/indexPageDisplay.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
const indexPageDisplay = require('../../../_11ty/shortcodes/indexPageDisplay');

describe('indexPageDisplay.js', () => {
const testImageDirectory = '../../__tests__/_11ty/shortcodes/test-index-images';

// Sample of 11y collections.all https://www.11ty.dev/docs/collections/
const collectionAll = [
{
data: {
title: 'First Sub Item',
eleventyNavigation: {
key: 'First Sub Item',
parent: 'Test Menu Item',
},
page: {
url: '/first-sub-item/',
},
},
},
{
data: {
title: 'Second Sub Item',
eleventyNavigation: {
key: 'Second Sub Item',
parent: 'Test Menu Item',
},
page: {
url: '/second-sub-item/',
},
},
}];

it('should return the expected HTML for an index page', () => {
// act
const result = indexPageDisplay({
collection: collectionAll,
itemKey: 'Test Menu Item',
excludedElements: [],
imageSrcDirectory: testImageDirectory,
});

// assert
expect(result)
.toMatchSnapshot();
});

it('should throw when image is not provided', () => {
// setup
const collectionsWithMissingImage = [...collectionAll];
collectionsWithMissingImage.push({
data: {
title: 'Third Sub Item without image',
eleventyNavigation: {
key: 'Third Sub Item without image',
parent: 'Test Menu Item',
},
page: {
url: '/second-sub-item/',
},
},
});
// act & assert
expect(() => indexPageDisplay({
collection: collectionsWithMissingImage,
itemKey: 'Test Menu Item',
excludedElements: [],
imageSrcDirectory: testImageDirectory,
})).toThrow('Image not provided for third-sub-item-without-image. Please ensure image is provided or add this item to ExcludedElements.');
});

it('should return the expected HTML for an index page with excluded items', () => {
// act
const result = indexPageDisplay({
collection: collectionAll,
itemKey: 'Test Menu Item',
excludedElements: ['Second Sub Item'],
imageSrcDirectory: testImageDirectory,
});

// assert
expect(result)
.toMatchSnapshot();
});

it('should not render items if the page is a draft', () => {
// setup
const collectionsWithDraft = [...collectionAll];
collectionsWithDraft[0].data.draft = true;

// act
const result = indexPageDisplay({
collection: collectionsWithDraft,
itemKey: 'Test Menu Item',
excludedElements: ['Third Sub Item without image'],
imageSrcDirectory: testImageDirectory,
});

// assert
expect(result)
.toMatchSnapshot();
});

it('should use child url for subpage dropdown when it does not have an index page', () => {
// setup
const collectionsWithMissingImage = [...collectionAll];
collectionsWithMissingImage.push(
{
data: {
title: 'Subpage dropdown',
eleventyNavigation: {
key: 'Subpage dropdown',
parent: 'Test Menu Item',
subPageDropdown: true,
hasIndexPage: undefined,
},
page: {
url: 'subpage-dropdown',
},
},
},
{
data: {
title: 'Subpage dropdown child',
eleventyNavigation: {
key: 'Subpage dropdown child',
parent: 'Subpage dropdown',
},
page: {
url: 'subpage-dropdown-child',
},
},
},
);

// act
const result = indexPageDisplay({
collection: collectionsWithMissingImage,
itemKey: 'Test Menu Item',
excludedElements: [],
imageSrcDirectory: testImageDirectory,
});

// assert
expect(result)
.toMatchSnapshot();
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading