Skip to content
This repository was archived by the owner on Apr 13, 2023. It is now read-only.

Commit c81aacf

Browse files
authored
JS-less nav (#129)
* First pass at JS-less navigation * Add real data and default state * Add active state and hover styles * Move it to a single file and finish expand/collapse all
1 parent a1d1423 commit c81aacf

File tree

4 files changed

+259
-319
lines changed

4 files changed

+259
-319
lines changed
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import PropTypes from 'prop-types';
2+
import React, {useRef, useState} from 'react';
3+
import styled from '@emotion/styled';
4+
import {IconArrowUp} from '@apollo/space-kit/icons/IconArrowUp';
5+
import {IconCollapseList} from '@apollo/space-kit/icons/IconCollapseList';
6+
import {IconExpandList} from '@apollo/space-kit/icons/IconExpandList';
7+
import {IconOutlink} from '@apollo/space-kit/icons/IconOutlink';
8+
import {Link, withPrefix} from 'gatsby';
9+
import {colors} from '../utils/colors';
10+
import {size} from 'polished';
11+
import {smallCaps} from '../utils/typography';
12+
13+
const ExpandAll = styled.button(smallCaps, {
14+
display: 'flex',
15+
alignItems: 'center',
16+
marginBottom: 12,
17+
padding: '4px 0',
18+
border: 0,
19+
fontSize: 12,
20+
fontWeight: 600,
21+
lineHeight: 1,
22+
background: 'none',
23+
outline: 'none',
24+
cursor: 'pointer',
25+
color: 'inherit',
26+
':hover': {
27+
opacity: colors.hoverOpacity
28+
},
29+
svg: {
30+
...size(12),
31+
marginRight: 8
32+
}
33+
});
34+
35+
const StyledList = styled.ul({
36+
marginLeft: 0,
37+
marginBottom: 32,
38+
listStyle: 'none'
39+
});
40+
41+
const StyledListItem = styled.li({
42+
fontSize: '1rem',
43+
lineHeight: 1.5,
44+
marginBottom: '0.8125rem',
45+
a: {
46+
color: 'inherit',
47+
textDecoration: 'none',
48+
':hover': {
49+
opacity: colors.hoverOpacity
50+
},
51+
'&.active': {
52+
color: colors.primary,
53+
pointerEvents: 'none'
54+
}
55+
}
56+
});
57+
58+
const Category = styled.div({
59+
position: 'relative',
60+
zIndex: 0,
61+
[StyledList]: {
62+
position: 'relative',
63+
zIndex: 2
64+
}
65+
});
66+
67+
const categoryTitleStyles = {
68+
display: 'flex',
69+
alignItems: 'center',
70+
justifyContent: 'space-between',
71+
padding: '12px 0',
72+
color: colors.text1,
73+
fontWeight: 'bold',
74+
fontSize: 14,
75+
lineHeight: '15px',
76+
...smallCaps,
77+
svg: size(10),
78+
'&.active': {
79+
color: colors.primary
80+
}
81+
};
82+
83+
const CategoryTitle = styled.div(categoryTitleStyles);
84+
const CategoryLink = styled(Link)(categoryTitleStyles, {
85+
textDecoration: 'none',
86+
':hover': {
87+
opacity: colors.hoverOpacity
88+
}
89+
});
90+
91+
const StyledCheckbox = styled.input({
92+
...size('100%'),
93+
cursor: 'pointer',
94+
position: 'absolute',
95+
top: 0,
96+
left: 0,
97+
opacity: 0,
98+
zIndex: 1,
99+
[`:hover ~ ${CategoryTitle}`]: {
100+
opacity: colors.hoverOpacity
101+
},
102+
':not(:checked) ~': {
103+
[`${CategoryTitle} svg`]: {
104+
transform: 'scaleY(-1)'
105+
},
106+
[StyledList]: {
107+
display: 'none'
108+
}
109+
}
110+
});
111+
112+
const StyledOutlinkIcon = styled(IconOutlink)(size(14), {
113+
verticalAlign: -1,
114+
marginLeft: 8,
115+
color: colors.text3
116+
});
117+
118+
function isPageSelected(path, pathname) {
119+
const [a, b] = [withPrefix(path), pathname].map(string =>
120+
string.replace(/\/$/, '')
121+
);
122+
return a === b;
123+
}
124+
125+
function NavItems(props) {
126+
return (
127+
<StyledList>
128+
{props.pages.map((page, index) => {
129+
const pageTitle = page.sidebarTitle || page.title;
130+
return (
131+
<StyledListItem key={index}>
132+
{page.anchor ? (
133+
<a href={page.path} target="_blank" rel="noopener noreferrer">
134+
{pageTitle}
135+
<StyledOutlinkIcon />
136+
</a>
137+
) : (
138+
<Link
139+
className={
140+
isPageSelected(page.path, props.pathname) ? 'active' : null
141+
}
142+
to={page.path}
143+
title={page.description}
144+
onClick={props.onLinkClick}
145+
>
146+
{pageTitle}
147+
</Link>
148+
)}
149+
</StyledListItem>
150+
);
151+
})}
152+
</StyledList>
153+
);
154+
}
155+
156+
NavItems.propTypes = {
157+
pages: PropTypes.array.isRequired,
158+
pathname: PropTypes.string.isRequired,
159+
onLinkClick: PropTypes.func
160+
};
161+
162+
export default function SidebarNav(props) {
163+
const categoriesRef = useRef();
164+
165+
const [allExpanded, setAllExpanded] = useState(false);
166+
const categories = props.contents.filter(content => content.title);
167+
const [{pages}] = props.contents.filter(content => !content.title);
168+
169+
function toggleAll() {
170+
const checkboxes = Array.from(
171+
categoriesRef.current.querySelectorAll('input[type="checkbox"]')
172+
);
173+
174+
const expanded = !checkboxes.every(checkbox => checkbox.checked);
175+
checkboxes.forEach(checkbox => (checkbox.checked = expanded));
176+
setAllExpanded(expanded);
177+
178+
if (props.onToggleAll) {
179+
props.onToggleAll(expanded);
180+
}
181+
}
182+
183+
function toggleCategory(event) {
184+
const {value, checked, parentElement} = event.target;
185+
186+
const checkboxes = parentElement.parentElement.querySelectorAll(
187+
'input[type="checkbox"]'
188+
);
189+
const expanded = Array.from(checkboxes).every(checkbox => checkbox.checked);
190+
setAllExpanded(expanded);
191+
192+
if (props.onToggleCategory) {
193+
props.onToggleCategory(value, checked);
194+
}
195+
}
196+
197+
return (
198+
<>
199+
<NavItems
200+
pages={pages}
201+
pathname={props.pathname}
202+
onLinkClick={props.onLinkClick}
203+
/>
204+
{!props.alwaysExpanded && categories.length > 1 && (
205+
<ExpandAll onClick={toggleAll}>
206+
{React.createElement(allExpanded ? IconCollapseList : IconExpandList)}
207+
{allExpanded ? 'Collapse' : 'Expand'} all
208+
</ExpandAll>
209+
)}
210+
<div ref={categoriesRef}>
211+
{categories.map((category, index) => {
212+
const isSelected = category.pages.some(page =>
213+
isPageSelected(page.path, props.pathname)
214+
);
215+
const className = isSelected ? 'active' : null;
216+
return (
217+
<Category key={index}>
218+
{!props.alwaysExpanded && (
219+
<StyledCheckbox
220+
type="checkbox"
221+
value={category.title}
222+
onChange={toggleCategory}
223+
defaultChecked={isSelected}
224+
/>
225+
)}
226+
{props.alwaysExpanded && category.path ? (
227+
<CategoryLink className={className} to={category.path}>
228+
{category.title}
229+
</CategoryLink>
230+
) : (
231+
<CategoryTitle className={className}>
232+
{category.title}
233+
{!props.alwaysExpanded && <IconArrowUp />}
234+
</CategoryTitle>
235+
)}
236+
<NavItems
237+
pages={category.pages}
238+
pathname={props.pathname}
239+
onLinkClick={props.onLinkClick}
240+
/>
241+
</Category>
242+
);
243+
})}
244+
</div>
245+
</>
246+
);
247+
}
248+
249+
SidebarNav.propTypes = {
250+
alwaysExpanded: PropTypes.bool,
251+
contents: PropTypes.array.isRequired,
252+
pathname: PropTypes.string.isRequired,
253+
onToggleAll: PropTypes.func,
254+
onToggleCategory: PropTypes.func,
255+
onLinkClick: PropTypes.func
256+
};

packages/gatsby-theme-apollo-core/src/components/sidebar-nav/category.js

Lines changed: 0 additions & 88 deletions
This file was deleted.

0 commit comments

Comments
 (0)