Skip to content

Commit dceafe9

Browse files
authored
feat(Nav): performance improvements (#55)
* feat(Nav): WIP performance * clean up code, move sorting to codefence * update tests to reflect removal of sorting logic * update test, snapshot, fix active detection
1 parent 90f7fc2 commit dceafe9

File tree

9 files changed

+218
-351
lines changed

9 files changed

+218
-351
lines changed

src/components/Masthead.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { PageToggle } from './PageToggle'
1616
<PFMasthead>
1717
<MastheadMain>
1818
<MastheadToggle>
19-
<PageToggle client:idle />
19+
<PageToggle client:idle transition:persist />
2020
</MastheadToggle>
2121
<MastheadBrand>
2222
<MastheadLogo component="a" href="/">

src/components/NavEntry.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ export interface TextContentEntry {
66
data: {
77
id: string
88
section: string
9-
tab?: string
109
}
11-
collection: string
1210
}
1311

1412
interface NavEntryProps {

src/components/NavSection.tsx

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,10 @@ export const NavSection = ({
1414
sectionId,
1515
activeItem,
1616
}: NavSectionProps) => {
17-
const isExpanded = window.location.pathname.includes(sectionId)
17+
const isExpanded = window.location.pathname.includes(kebabCase(sectionId))
18+
const isActive = entries.some((entry) => entry.id === activeItem)
1819

19-
const sortedNavEntries = entries.sort((a, b) =>
20-
a.data.id.localeCompare(b.data.id),
21-
)
22-
23-
const isActive = sortedNavEntries.some((entry) => entry.id === activeItem)
24-
25-
let navItems = sortedNavEntries
26-
if (sectionId === 'components' || sectionId === 'layouts') {
27-
// only display unique entry.data.id in the nav list if the section is components
28-
navItems = [
29-
...sortedNavEntries
30-
.reduce((map, entry) => {
31-
if (!map.has(entry.data.id)) {
32-
map.set(entry.data.id, entry)
33-
}
34-
return map
35-
}, new Map())
36-
.values(),
37-
]
38-
}
39-
40-
const items = navItems.map((entry) => (
20+
const items = entries.map((entry) => (
4121
<NavEntry
4222
key={entry.id}
4323
entry={entry}

src/components/Navigation.astro

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { getCollection } from 'astro:content'
33
44
import { Navigation as ReactNav } from './Navigation.tsx'
5+
import { TextContentEntry } from './NavEntry.tsx'
56
67
import { content } from '../content'
78
@@ -13,12 +14,56 @@ const collections = await Promise.all(
1314
),
1415
)
1516
16-
const navEntries = collections.flat()
17+
const navDataRaw = collections.flat();
18+
const uniqueSections = new Set(navDataRaw.map((entry) => entry.data.section));
19+
const navData: Record<string, TextContentEntry[]> = {};
20+
21+
const [orderedSections, unorderedSections] = Array.from(uniqueSections).reduce(
22+
(acc, section) => {
23+
if (!config.navSectionOrder) {
24+
acc[1].push(section)
25+
return acc
26+
}
27+
28+
const index = config.navSectionOrder.indexOf(section)
29+
if (index > -1) {
30+
acc[0][index] = section
31+
} else {
32+
acc[1].push(section)
33+
}
34+
return acc
35+
},
36+
[[], []] as [string[], string[]],
37+
)
38+
39+
const sortedSections = [...orderedSections, ...unorderedSections.sort()]
40+
sortedSections.map((section) => {
41+
const entries = navDataRaw
42+
.filter((entry) => entry.data.section === section)
43+
.map(entry => ({ id: entry.id, data: { id: entry.data.id, section }} as TextContentEntry))
44+
45+
const sortedEntries = entries.sort((a, b) =>
46+
a.data.id.localeCompare(b.data.id),
47+
)
48+
49+
let navEntries = sortedEntries
50+
if (section === 'components' || section === 'layouts') {
51+
// only display unique entry.data.id in the nav list if the section is components
52+
navEntries = [
53+
...sortedEntries
54+
.reduce((map, entry) => {
55+
if (!map.has(entry.data.id)) {
56+
map.set(entry.data.id, entry)
57+
}
58+
return map
59+
}, new Map())
60+
.values(),
61+
]
62+
}
63+
64+
navData[section] = navEntries;
65+
})
66+
1767
---
1868

19-
<ReactNav
20-
client:only="react"
21-
navEntries={navEntries}
22-
navSectionOrder={config.navSectionOrder}
23-
transition:animate="fade"
24-
/>
69+
<ReactNav client:only="react" navData={navData} transition:animate="fade" />

src/components/Navigation.tsx

Lines changed: 13 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,17 @@ import { NavSection } from './NavSection'
44
import { type TextContentEntry } from './NavEntry'
55

66
interface NavigationProps {
7-
navEntries: TextContentEntry[]
8-
navSectionOrder?: string[]
7+
navData: Record<string, TextContentEntry[]>
98
}
109

1110
export const Navigation: React.FunctionComponent<NavigationProps> = ({
12-
navEntries,
13-
navSectionOrder,
11+
navData,
1412
}: NavigationProps) => {
1513
const [activeItem, setActiveItem] = useState('')
1614

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

2220
const onNavSelect = (
@@ -26,48 +24,20 @@ export const Navigation: React.FunctionComponent<NavigationProps> = ({
2624
setActiveItem(selectedItem.itemId.toString())
2725
}
2826

29-
const uniqueSections = Array.from(
30-
new Set(navEntries.map((entry) => entry.data.section)),
31-
)
32-
33-
// We want to list any ordered sections first, followed by any unordered sections sorted alphabetically
34-
const [orderedSections, unorderedSections] = uniqueSections.reduce(
35-
(acc, section) => {
36-
if (!navSectionOrder) {
37-
acc[1].push(section)
38-
return acc
39-
}
40-
41-
const index = navSectionOrder.indexOf(section)
42-
if (index > -1) {
43-
acc[0][index] = section
44-
} else {
45-
acc[1].push(section)
46-
}
47-
return acc
48-
},
49-
[[], []] as [string[], string[]],
50-
)
51-
const sortedSections = [...orderedSections, ...unorderedSections.sort()]
52-
53-
const navSections = sortedSections.map((section) => {
54-
const entries = navEntries.filter((entry) => entry.data.section === section)
55-
56-
return (
57-
<NavSection
58-
key={section}
59-
entries={entries}
60-
sectionId={section}
61-
activeItem={activeItem}
62-
/>
63-
)
64-
})
65-
6627
return (
6728
// Can possibly add back PageSidebar wrapper when https://github.com/patternfly/patternfly/issues/7377 goes in
6829
<PageSidebarBody id="page-sidebar-body">
6930
<Nav onSelect={onNavSelect}>
70-
<NavList>{navSections}</NavList>
31+
<NavList>
32+
{Object.entries(navData).map(([key, value], index) => (
33+
<NavSection
34+
key={index}
35+
entries={value}
36+
sectionId={key}
37+
activeItem={activeItem}
38+
/>
39+
))}
40+
</NavList>
7141
</Nav>
7242
</PageSidebarBody>
7343
)
Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,36 @@
1-
2-
import { render, screen } from '@testing-library/react';
3-
import { NavEntry, TextContentEntry } from '../NavEntry';
1+
import { render, screen } from '@testing-library/react'
2+
import { NavEntry, TextContentEntry } from '../NavEntry'
43

54
const mockEntry: TextContentEntry = {
65
id: 'entry1',
76
data: { id: 'Entry1', section: 'section1' },
8-
collection: 'textContent',
9-
};
7+
}
108

119
describe('NavEntry', () => {
1210
it('renders without crashing', () => {
13-
render(<NavEntry entry={mockEntry} isActive={false} />);
14-
expect(screen.getByText('Entry1')).toBeInTheDocument();
15-
});
11+
render(<NavEntry entry={mockEntry} isActive={false} />)
12+
expect(screen.getByText('Entry1')).toBeInTheDocument()
13+
})
1614

1715
it('renders the correct link', () => {
18-
render(<NavEntry entry={mockEntry} isActive={false} />);
19-
expect(screen.getByRole('link')).toHaveAttribute('href', '/section1/entry1');
20-
});
16+
render(<NavEntry entry={mockEntry} isActive={false} />)
17+
expect(screen.getByRole('link')).toHaveAttribute('href', '/section1/entry1')
18+
})
2119

2220
it('marks the entry as active if isActive is true', () => {
23-
render(<NavEntry entry={mockEntry} isActive={true} />);
24-
expect(screen.getByRole('link')).toHaveClass('pf-m-current');
25-
});
21+
render(<NavEntry entry={mockEntry} isActive={true} />)
22+
expect(screen.getByRole('link')).toHaveClass('pf-m-current')
23+
})
2624

2725
it('does not mark the entry as active if isActive is false', () => {
28-
render(<NavEntry entry={mockEntry} isActive={false} />);
29-
expect(screen.getByRole('link')).not.toHaveClass('pf-m-current');
30-
});
26+
render(<NavEntry entry={mockEntry} isActive={false} />)
27+
expect(screen.getByRole('link')).not.toHaveClass('pf-m-current')
28+
})
3129

3230
it('matches snapshot', () => {
33-
const { asFragment } = render(<NavEntry entry={mockEntry} isActive={false} />);
34-
expect(asFragment()).toMatchSnapshot();
35-
});
36-
});
31+
const { asFragment } = render(
32+
<NavEntry entry={mockEntry} isActive={false} />,
33+
)
34+
expect(asFragment()).toMatchSnapshot()
35+
})
36+
})

src/components/__tests__/NavSection.test.tsx

Lines changed: 0 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,14 @@ const mockEntries: TextContentEntry[] = [
66
{
77
id: 'entry1',
88
data: { id: 'Entry1', section: 'section1' },
9-
collection: 'textContent',
109
},
1110
{
1211
id: 'entry2',
1312
data: { id: 'Entry2', section: 'Section1' },
14-
collection: 'textContent',
1513
},
1614
{
1715
id: 'entry3',
1816
data: { id: 'Entry3', section: 'Section1' },
19-
collection: 'textContent',
20-
},
21-
]
22-
23-
const dupedEntries: TextContentEntry[] = [
24-
{
25-
id: 'entry1',
26-
data: { id: 'Entry1', section: 'components' },
27-
collection: 'react',
28-
},
29-
{
30-
id: 'entry2',
31-
data: { id: 'Entry1', section: 'components' },
32-
collection: 'react',
33-
},
34-
{
35-
id: 'entry3',
36-
data: { id: 'Entry2', section: 'components' },
37-
collection: 'react',
3817
},
3918
]
4019

@@ -159,45 +138,3 @@ it('matches snapshot', () => {
159138
)
160139
expect(asFragment()).toMatchSnapshot()
161140
})
162-
163-
it('dedupes and renders correct number of entries for components section', () => {
164-
Object.defineProperty(window, 'location', {
165-
value: {
166-
pathname: '/foo/components',
167-
},
168-
writable: true,
169-
})
170-
171-
render(
172-
<NavSection
173-
entries={dupedEntries}
174-
sectionId="components"
175-
activeItem="entry1"
176-
/>,
177-
)
178-
179-
expect(screen.getAllByRole('listitem')).toHaveLength(3)
180-
expect(screen.getByRole('link', { name: 'Entry1' })).toBeInTheDocument()
181-
expect(screen.getByRole('link', { name: 'Entry2' })).toBeInTheDocument()
182-
})
183-
184-
it('dedupes and renders correct number of entries for layouts section', () => {
185-
Object.defineProperty(window, 'location', {
186-
value: {
187-
pathname: '/foo/layouts',
188-
},
189-
writable: true,
190-
})
191-
192-
render(
193-
<NavSection
194-
entries={dupedEntries}
195-
sectionId="layouts"
196-
activeItem="entry1"
197-
/>,
198-
)
199-
200-
expect(screen.getAllByRole('listitem')).toHaveLength(3)
201-
expect(screen.getByRole('link', { name: 'Entry1' })).toBeInTheDocument()
202-
expect(screen.getByRole('link', { name: 'Entry2' })).toBeInTheDocument()
203-
})

0 commit comments

Comments
 (0)