Skip to content

Commit

Permalink
DOP-4536: Breadcrumbs! πŸš‚πŸžπŸ₯πŸ₯–πŸ«“πŸž (#1085)
Browse files Browse the repository at this point in the history
Co-authored-by: Maya Raman <[email protected]>
Co-authored-by: Matt Meigs <[email protected]>
Co-authored-by: anabellabuckvar <[email protected]>
  • Loading branch information
4 people authored May 1, 2024
1 parent 1c94ff2 commit 7d84d5f
Show file tree
Hide file tree
Showing 31 changed files with 1,574 additions and 882 deletions.
372 changes: 317 additions & 55 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
"@leafygreen-ui/text-area": "^6.1.0",
"@leafygreen-ui/text-input": "^10.1.0",
"@leafygreen-ui/toast": "^6.1.4",
"@leafygreen-ui/tooltip": "^7.1.0",
"@leafygreen-ui/tooltip": "^11.0.4",
"@leafygreen-ui/typography": "^18.3.0",
"@loadable/component": "^5.14.1",
"@mdb/consistent-nav": "^2.0.7",
Expand Down
7 changes: 4 additions & 3 deletions plugins/gatsby-source-snooty-preview/gatsby-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,11 @@ exports.createSchemaCustomization = async ({ actions }) => {
productName: String
}
type ProjectParent implements Node @dontInfer {
parents: JSON
project: String!
type Breadcrumb implements Node @dontInfer {
breadcrumbs: JSON
propertyUrl: String
}
`;
createTypes(typeDefs);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { realmDocumentDatabase } = require('../../src/init/DocumentDatabase.js');
const { createOpenAPIChangelogNode } = require('../utils/openapi');
const { createProductNodes } = require('../utils/products');
const { createDocsetNodes } = require('../utils/docsets');
const { createProjectParentNodes } = require('../utils/project-parents');
const { createBreadcrumbNodes } = require('../utils/breadcrumbs');

// Sources nodes for the preview plugin that are not directly related to data
// from the Snooty Data API
Expand All @@ -18,7 +18,7 @@ exports.sourceNodes = async ({
await db.connect();
await createProductNodes({ db, createNode, createNodeId, createContentDigest });
await createDocsetNodes({ db, createNode, createNodeId, createContentDigest });
await createProjectParentNodes({ db, createNode, createNodeId, createContentDigest, getNodesByType });
await createBreadcrumbNodes({ db, createNode, createNodeId, createContentDigest });
if (hasOpenAPIChangelog)
await createOpenAPIChangelogNode({ createNode, createNodeId, createContentDigest, siteMetadata, db });
};
11 changes: 6 additions & 5 deletions plugins/gatsby-source-snooty-prod/gatsby-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const { manifestDocumentDatabase, realmDocumentDatabase } = require('../../src/i
const { createOpenAPIChangelogNode } = require('../utils/openapi.js');
const { createProductNodes } = require('../utils/products.js');
const { createDocsetNodes } = require('../utils/docsets.js');
const { createProjectParentNodes } = require('../utils/project-parents.js');
const { createBreadcrumbNodes } = require('../utils/breadcrumbs.js');

const assets = new Map();
const projectComponents = new Set();
Expand Down Expand Up @@ -192,7 +192,7 @@ exports.sourceNodes = async ({ actions, createContentDigest, createNodeId, getNo

await createProductNodes({ db, createNode, createNodeId, createContentDigest });

await createProjectParentNodes({ db, createNode, createNodeId, createContentDigest, getNodesByType });
await createBreadcrumbNodes({ db, createNode, createNodeId, createContentDigest });

const umbrellaProduct = await db.realmInterface.getMetadata(
{
Expand Down Expand Up @@ -413,9 +413,10 @@ exports.createSchemaCustomization = ({ actions }) => {
productName: String
}
type ProjectParent implements Node @dontInfer {
parents: JSON
project: String!
type Breadcrumb implements Node @dontInfer {
breadcrumbs: JSON
propertyUrl: String
}
`);
};
32 changes: 32 additions & 0 deletions plugins/utils/breadcrumbs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const { siteMetadata } = require('../../src/utils/site-metadata');

const breadcrumbType = `Breadcrumb`;

const createBreadcrumbNodes = async ({ db, createNode, createNodeId, createContentDigest }) => {
const { database, project } = siteMetadata;
let breadcrumbData;
try {
breadcrumbData = await db.fetchBreadcrumbs(database, project);
} catch (e) {
console.error(`Error while fetching breadcrumb data from Atlas: ${e}`);
}
const [breadcrumbs, propertyUrl] = breadcrumbData
? [breadcrumbData.breadcrumbs, breadcrumbData.propertyUrl]
: [null, ''];

return createNode({
children: [],
id: createNodeId(`Breadcrumbs-${project}`),
internal: {
contentDigest: createContentDigest(breadcrumbs),
type: breadcrumbType,
},
breadcrumbs: breadcrumbs,
propertyUrl: propertyUrl,
});
};

module.exports = {
createBreadcrumbNodes,
breadcrumbType,
};
31 changes: 0 additions & 31 deletions plugins/utils/project-parents.js

This file was deleted.

111 changes: 59 additions & 52 deletions src/components/Breadcrumbs/BreadcrumbContainer.js
Original file line number Diff line number Diff line change
@@ -1,73 +1,81 @@
import React from 'react';
import PropTypes from 'prop-types';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { css as LeafyCss, cx } from '@leafygreen-ui/emotion';
import { palette } from '@leafygreen-ui/palette';
import Link from '../Link';
import { formatText } from '../../utils/format-text';
import { theme } from '../../theme/docsTheme';
import { reportAnalytics } from '../../utils/report-analytics';
import { useNavigationParents } from '../../hooks/use-navigation-parents';
import useSnootyMetadata from '../../utils/use-snooty-metadata';

const activeColor = css`
color: ${palette.gray.dark3};
`;
import { theme } from '../../theme/docsTheme';
import IndividualBreadcrumb from './IndividualBreadcrumb';
import CollapsedBreadcrumbs from './CollapsedBreadcrumbs';

const StyledArrow = styled('span')`
const StyledSlash = styled('span')`
cursor: default;
padding-left: ${theme.size.small};
padding-right: ${theme.size.small};
`;

:last-of-type {
${activeColor}
}
const Flexbox = styled('div')`
display: flex;
align-items: center;
`;

const linkStyling = LeafyCss`
font-size: ${theme.fontSize.small};
:last-of-type {
${activeColor}
}
const MIN_BREADCRUMBS = 3;
const initialMaxCrumbs = (breadcrumbs) => breadcrumbs.length + 1;

:hover,
:focus {
text-decoration: none;
const BreadcrumbContainer = ({ breadcrumbs }) => {
const [maxCrumbs, setMaxCrumbs] = React.useState(initialMaxCrumbs(breadcrumbs));

:not(:last-of-type) {
${activeColor}
React.useEffect(() => {
const handleResize = () => {
setMaxCrumbs(initialMaxCrumbs(breadcrumbs));
};

window.addEventListener('resize', handleResize);

return () => window.removeEventListener('resize', handleResize);
}, [breadcrumbs]);

// Our breadcrumbs representation is an array of crumbObjectShape || (array of crumbObjectShape)
// The latter indicates a collapsed series of breadcrumbs.
const processedBreadcrumbs = React.useMemo(() => {
const crumbsCopy = Array.from(breadcrumbs);
if (crumbsCopy.length >= maxCrumbs && crumbsCopy.length > 2) {
// A maximum of maxCrumbs breadcrumbs may be shown, so we collapse the first run of internal
// crumbs into a single "…" crumb
const collapsedCrumbs = crumbsCopy.splice(1, breadcrumbs.length - maxCrumbs + 1, []);
crumbsCopy[1] = collapsedCrumbs;
}
}
`;
return crumbsCopy;
}, [maxCrumbs, breadcrumbs]);

const BreadcrumbContainer = ({ homeCrumb, lastCrumb }) => {
const { project } = useSnootyMetadata();
const parents = useNavigationParents(project);
const breadcrumbs = React.useMemo(() => [homeCrumb, ...parents, lastCrumb], [homeCrumb, parents, lastCrumb]);
const collapseBreadcrumbs = () => {
const newMaxCrumbs = Math.max(maxCrumbs - 1, MIN_BREADCRUMBS);
setMaxCrumbs(newMaxCrumbs);
};

return (
<>
{breadcrumbs.map(({ title, url }, index) => {
<Flexbox>
{processedBreadcrumbs.map((crumb, index) => {
const isFirst = index === 0;
const renderKey = typeof title === 'string' ? title : title[0]?.value; // could return undefined which is fine, we would still get a unique key
return (
<React.Fragment key={`${renderKey}-${index}`}>
{!isFirst && <StyledArrow> &#8594; </StyledArrow>}
<Link
className={cx(linkStyling)}
to={url}
onClick={() => {
reportAnalytics('BreadcrumbClick', {
parentPaths: breadcrumbs,
breadcrumbClicked: url,
});
}}
>
{formatText(title)}
</Link>
<React.Fragment key={index}>
{!isFirst && <StyledSlash> / </StyledSlash>}
{Array.isArray(crumb) ? (
<CollapsedBreadcrumbs crumbs={crumb}></CollapsedBreadcrumbs>
) : (
<IndividualBreadcrumb
key={crumb.title}
crumb={crumb}
setIsExcessivelyTruncated={collapseBreadcrumbs}
onClick={() =>
reportAnalytics('BreadcrumbClick', {
breadcrumbClicked: crumb.url,
})
}
></IndividualBreadcrumb>
)}
</React.Fragment>
);
})}
</>
</Flexbox>
);
};

Expand All @@ -77,8 +85,7 @@ const crumbObjectShape = {
};

BreadcrumbContainer.propTypes = {
homeCrumb: PropTypes.shape(crumbObjectShape).isRequired,
lastCrumb: PropTypes.shape(crumbObjectShape).isRequired,
breadcrumbs: PropTypes.shape(crumbObjectShape).isRequired,
};

export default BreadcrumbContainer;
39 changes: 39 additions & 0 deletions src/components/Breadcrumbs/CollapsedBreadcrumbs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Menu, MenuItem } from '@leafygreen-ui/menu';
import IconButton from '@leafygreen-ui/icon-button';
import Icon from '@leafygreen-ui/icon';
import { formatText } from '../../utils/format-text';

const CollapsedBreadcrumbs = ({ crumbs }) => {
return (
<React.Fragment>
<Menu
align="bottom"
justify="start"
trigger={
<IconButton aria-label="Show all breadcrumbs">
<Icon glyph="Ellipsis" />
</IconButton>
}
>
{crumbs.map((crumb, index) => (
<MenuItem key={index} href={crumb.url}>
{formatText(crumb.title)}
</MenuItem>
))}
</Menu>
</React.Fragment>
);
};

const crumbObjectShape = {
title: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
};

CollapsedBreadcrumbs.propTypes = {
crumbs: PropTypes.arrayOf(PropTypes.shape(crumbObjectShape)).isRequired,
};

export default CollapsedBreadcrumbs;
Loading

0 comments on commit 7d84d5f

Please sign in to comment.