Skip to content
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
13 changes: 12 additions & 1 deletion src/components/NavEntry.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { NavItem } from '@patternfly/react-core'
import { kebabCase } from 'change-case'

export interface TextContentEntry {
id: string
data: {
id: string
section: string
tab?: string
}
collection: string
}
Expand All @@ -18,8 +20,17 @@ export const NavEntry = ({ entry, isActive }: NavEntryProps) => {
const { id } = entry
const { id: entryTitle, section } = entry.data

const _id =
section === 'components' || section === 'layouts'
? kebabCase(entryTitle)
: id
return (
<NavItem itemId={id} to={`/${section}/${id}`} isActive={isActive} id={`nav-entry-${id}`}>
<NavItem
itemId={_id}
to={`/${section}/${_id}`}
isActive={isActive}
id={`nav-entry-${_id}`}
>
{entryTitle}
</NavItem>
)
Expand Down
27 changes: 25 additions & 2 deletions src/components/NavSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NavExpandable } from '@patternfly/react-core'
import { sentenceCase } from 'change-case'
import { NavEntry, type TextContentEntry } from './NavEntry'
import { kebabCase } from 'change-case'

interface NavSectionProps {
entries: TextContentEntry[]
Expand All @@ -21,8 +22,30 @@ export const NavSection = ({

const isActive = sortedNavEntries.some((entry) => entry.id === activeItem)

const items = sortedNavEntries.map((entry) => (
<NavEntry key={entry.id} entry={entry} isActive={activeItem === entry.id} />
let navItems = sortedNavEntries
if (sectionId === 'components' || sectionId === 'layouts') {
// only display unique entry.data.id in the nav list if the section is components
navItems = [
...sortedNavEntries
.reduce((map, entry) => {
if (!map.has(entry.data.id)) {
map.set(entry.data.id, entry)
}
return map
}, new Map())
.values(),
]
}

const items = navItems.map((entry) => (
<NavEntry
key={entry.id}
entry={entry}
isActive={
activeItem === entry.id ||
window.location.pathname.includes(kebabCase(entry.data.id))
}
/>
))

return (
Expand Down
3 changes: 2 additions & 1 deletion src/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export const Navigation: React.FunctionComponent<NavigationProps> = ({
const [activeItem, setActiveItem] = useState('')

useEffect(() => {
setActiveItem(window.location.pathname.split('/').reverse()[0])
// TODO: Needs an alternate solution because of /tab in the path
setActiveItem(window.location.pathname.split('/').reverse()[0])
}, [])

const onNavSelect = (
Expand Down
6 changes: 2 additions & 4 deletions src/components/Page.astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
import styles from '@patternfly/react-styles/css/components/Page/page'
import { Content, PageSection } from '@patternfly/react-core'
import { PageSection } from '@patternfly/react-core'
---

<div class={styles.page}>
Expand All @@ -10,9 +10,7 @@ import { Content, PageSection } from '@patternfly/react-core'
<div class={styles.pageMainContainer}>
<main class={styles.pageMain}>
<PageSection transition:animate="none">
<Content>
<slot />
</Content>
<slot />
</PageSection>
</main>
</div>
Expand Down
60 changes: 60 additions & 0 deletions src/components/__tests__/NavSection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ const mockEntries: TextContentEntry[] = [
},
]

const dupedEntries: TextContentEntry[] = [
{
id: 'entry1',
data: { id: 'Entry1', section: 'components' },
collection: 'react',
},
{
id: 'entry2',
data: { id: 'Entry1', section: 'components' },
collection: 'react',
},
{
id: 'entry3',
data: { id: 'Entry2', section: 'components' },
collection: 'react',
},
]

it('renders without crashing', () => {
render(
<NavSection
Expand Down Expand Up @@ -141,3 +159,45 @@ it('matches snapshot', () => {
)
expect(asFragment()).toMatchSnapshot()
})

it('dedupes and renders correct number of entries for components section', () => {
Object.defineProperty(window, 'location', {
value: {
pathname: '/foo/components',
},
writable: true,
})

render(
<NavSection
entries={dupedEntries}
sectionId="components"
activeItem="entry1"
/>,
)

expect(screen.getAllByRole('listitem')).toHaveLength(3)
expect(screen.getByRole('link', { name: 'Entry1' })).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'Entry2' })).toBeInTheDocument()
})

it('dedupes and renders correct number of entries for layouts section', () => {
Object.defineProperty(window, 'location', {
value: {
pathname: '/foo/layouts',
},
writable: true,
})

render(
<NavSection
entries={dupedEntries}
sectionId="layouts"
activeItem="entry1"
/>,
)

expect(screen.getAllByRole('listitem')).toHaveLength(3)
expect(screen.getByRole('link', { name: 'Entry1' })).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'Entry2' })).toBeInTheDocument()
})
8 changes: 8 additions & 0 deletions src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,20 @@ function defineContent(contentObj: CollectionDefinition) {
return
}

// TODO: Expand for other packages that remain under the react umbrella (Table, CodeEditor, etc)
const tabMap: any = {
'react-component-docs': 'react',
'core-component-docs': 'html'
};

return defineCollection({
loader: glob({ base: dir, pattern }),
schema: z.object({
id: z.string(),
section: z.string(),
subsection: z.string().optional(),
title: z.string().optional(),
tab: z.string().optional().default(tabMap[name])
}),
})
}
Expand Down
15 changes: 14 additions & 1 deletion src/content.ts
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
export const content = [{ base: 'textContent', pattern: '*.md', name: "textContent" }, { base: 'dir', pattern: '*.md', name: "dir" }]
export const content = [
{ base: 'textContent', pattern: '*.md', name: "textContent" }
// TODO: Remove. Uncomment for local testing.
// {
// "packageName":"@patternfly/react-core",
// "pattern":"**/examples/**/*.md", // had to update this pattern to bring in demos docs
// "name":"react-component-docs"
// },
// {
// "packageName":"@patternfly/patternfly",
// "pattern":"**/examples/**/*.md", // had to update this pattern to bring in demos docs
// "name":"core-component-docs"
// }
]
53 changes: 53 additions & 0 deletions src/globals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
export const componentTabs: any = {};

export const tabNames: any = {
'react': 'React',
'react-demos': 'React demos',
'react-deprecated': 'React deprecated',
'html': 'HTML',
'html-demos': 'HTML demos'
};

export const buildTab = (entry: any, tab: string) => {
const tabEntry = componentTabs[entry.data.id]

// if no dictionary entry exists, and tab data exists
if(tabEntry === undefined && tab) {
componentTabs[entry.data.id] = [tab]
// if dictionary entry & tab data exists, and entry does not include tab
} else if (tabEntry && tab && !tabEntry.includes(tab)) {
componentTabs[entry.data.id] = [...tabEntry, tab];
}
}

export const sortTabs = () => {
const defaultOrder = 50;
const sourceOrder: any = {
react: 1,
'react-next': 1.1,
'react-demos': 2,
'react-deprecated': 2.1,
html: 3,
'html-demos': 4,
'design-guidelines': 99,
'accessibility': 100,
'upgrade-guide': 101,
'release-notes': 102,
};

const sortSources = (s1: string, s2: string) => {
const s1Index = sourceOrder[s1] || defaultOrder;
const s2Index = sourceOrder[s2] || defaultOrder;
if (s1Index === defaultOrder && s2Index === defaultOrder) {
return s1.localeCompare(s2);
}

return s1Index > s2Index ? 1 : -1;
}

// Sort tabs entries based on above sort order
// Ensures all tabs are displayed in a consistent order & which tab gets displayed for a component route without a tab
Object.values(componentTabs).map((tabs: any) => {
tabs.sort(sortSources)
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,34 @@ import { getCollection, render } from 'astro:content'
import { Title } from '@patternfly/react-core'
import MainLayout from '../../layouts/Main.astro'
import { content } from "../../content"
import { kebabCase } from 'change-case'
import { componentTabs } from '../../globals'

export async function getStaticPaths() {
const collections = await Promise.all(content.map(async (entry) => await getCollection(entry.name as 'textContent')))

return collections.flat().map((entry) => ({
params: { id: entry.id, section: entry.data.section },
params: { page: kebabCase(entry.data.id), section: entry.data.section },
props: { entry, title: entry.data.title },
})
)
}


const { entry } = Astro.props
const { title } = entry.data
const { title, id, section } = entry.data
const { Content } = await render(entry)

if(section === 'components') { // if section is components, rewrite to first tab content
return Astro.rewrite(`/components/${kebabCase(id)}/${componentTabs[id][0]}`);
}
---

<MainLayout>
{
title && (
<Title headingLevel="h1" size="4xl">
{title}
{title}
</Title>
)
}
Expand Down
71 changes: 71 additions & 0 deletions src/pages/[section]/[page]/[...tab].astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
import { getCollection, render } from 'astro:content'
import { Title, PageSection, Content as PFContent } from '@patternfly/react-core'
import MainLayout from '../../../layouts/Main.astro'
import { content } from "../../../content"
import { kebabCase } from 'change-case'
import { componentTabs, tabNames, buildTab, sortTabs } from '../../../globals';

export async function getStaticPaths() {
const collections = await Promise.all(content.map(async (entry) => await getCollection(entry.name as 'textContent')))

const flatCol = collections.flat().map((entry) => {
// Build tabs dictionary
let tab = entry.data.tab;
if(tab) { // check for demos/deprecated
if(entry.id.includes('demos')) {
tab = `${tab}-demos`;
} else if (entry.id.includes('deprecated')) {
tab = `${tab}-deprecated`;
}
}
buildTab(entry, tab);

return {
params: { page: kebabCase(entry.data.id), section: entry.data.section, tab },
props: { entry, ...entry.data },
}
})

sortTabs()
return flatCol;
}

const { entry } = Astro.props
const { title, id, section } = entry.data
const { Content } = await render(entry)
const currentPath = Astro.url.pathname;
---

<MainLayout>
{
title && (
<Title headingLevel="h1" size="4xl">
{title}
</Title>
)
}
{componentTabs[id] && (
<PageSection id="ws-sticky-nav-tabs" stickyOnBreakpoint={{ default: 'top' }} type="tabs">
<div class="pf-v6-c-tabs pf-m-page-insets pf-m-no-border-bottom">
<ul class="pf-v6-c-tabs__list">
{componentTabs[id].map((tab: string) => (
// eslint-disable-next-line react/jsx-key
<li
class={`pf-v6-c-tabs__item${currentPath === `/${section}/${kebabCase(id)}/${tab}` ? ' pf-m-current' : ''}`}
>
<a class="pf-v6-c-tabs__link" href={`/${section}/${kebabCase(id)}/${tab}`}>
{tabNames[tab]}
</a>
</li>
))}
</ul>
</div>
</PageSection>
)}
<PageSection id="main-content" isFilled>
<PFContent>
<Content />
</PFContent>
</PageSection>
</MainLayout>