Skip to content

Commit a486988

Browse files
authored
refactor navigation (webpack#5171)
* refactor-navigation * add title * refactor icons * update docsearch primary color * update styles * fix style for dark mode * refactor dark mode toggler * show dark mode button for mobile * show shortcut keys * optimize styles * use grid * optimize switch button * set width to prevent unstyled content * optimize code * optimize menu * add e2e tests * add comment * customize hitComponent * use native Link * fix hitComponent * add search e2e test * fix e2e test * transform items in advance * update search box styles
1 parent 316fdc7 commit a486988

File tree

15 files changed

+439
-726
lines changed

15 files changed

+439
-726
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
describe('Detect sub navigation', () => {
2+
it('should show sub navigation', () => {
3+
cy.visit('/concepts/');
4+
5+
const selector = '[data-testid="sub-navigation"]';
6+
7+
cy.get(selector).should('exist');
8+
});
9+
10+
it('should not show sub navigation', () => {
11+
cy.visit('/');
12+
13+
const selector = '[data-testid="sub-navigation"]';
14+
15+
cy.get(selector).should('not.exist');
16+
});
17+
});

cypress/integration/search_spec.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
describe('Search', () => {
2+
it('should visit entry page', () => {
3+
cy.visit('/concepts/');
4+
cy.get('.DocSearch').click();
5+
cy.get('#docsearch-input').type('roadmap');
6+
cy.get('#docsearch-item-0').click();
7+
cy.url().should('include', '/blog/2020-12-08-roadmap-2021/');
8+
});
9+
});

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,10 @@
146146
"workbox-webpack-plugin": "^6.1.5"
147147
},
148148
"dependencies": {
149-
"docsearch.js": "^2.5.2",
149+
"@docsearch/react": "^3.0.0-alpha.37",
150150
"path-browserify": "^1.0.1",
151151
"prop-types": "^15.7.2",
152152
"react": "^17.0.2",
153-
"react-banner": "^1.0.0-rc.0",
154153
"react-dom": "^17.0.2",
155154
"react-g-analytics": "0.4.2",
156155
"react-helmet-async": "^1.0.9",

src/components/Dropdown/Dropdown.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export default class Dropdown extends Component {
8686
this.links ? this.links.push(node) : (this.links = [node])
8787
}
8888
href={item.url}
89+
className="px-5 block"
8990
>
9091
<span lang={item.lang}>{item.title}</span>
9192
</a>

src/components/HelloDarkness.jsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { THEME, THEME_LOCAL_STORAGE_KEY } from '../constants/theme';
2+
import PropTypes from 'prop-types';
3+
import { useLocalStorage } from 'react-use';
4+
import { useEffect } from 'react';
5+
6+
const { DARK, LIGHT } = THEME;
7+
8+
HelloDarkness.propTypes = {
9+
theme: PropTypes.oneOf([DARK, LIGHT]),
10+
switchTheme: PropTypes.func,
11+
};
12+
13+
HelloDarkness.defaultProps = {
14+
theme: LIGHT,
15+
};
16+
export default function HelloDarkness() {
17+
const [theme, setTheme] = useLocalStorage(
18+
THEME_LOCAL_STORAGE_KEY,
19+
THEME.LIGHT
20+
);
21+
const applyTheme = (theme) => {
22+
document.documentElement.setAttribute('data-theme', theme);
23+
if (theme === THEME.DARK) {
24+
document.documentElement.classList.add(THEME.DARK);
25+
} else {
26+
document.documentElement.classList.remove(THEME.DARK);
27+
}
28+
};
29+
useEffect(() => {
30+
applyTheme(theme);
31+
}, [theme]);
32+
33+
const switchTheme = (theme) => {
34+
setTheme(theme);
35+
};
36+
const themeSwitcher = () => switchTheme(theme === DARK ? LIGHT : DARK);
37+
return (
38+
<button
39+
aria-label={
40+
theme === DARK ? 'Switch to light theme' : 'Switch to dark theme'
41+
}
42+
className="bg-transparent border-none cursor-pointer text-[16px] p-0 inline-flex items-center"
43+
onClick={themeSwitcher}
44+
data-testid="hello-darkness"
45+
>
46+
{theme === DARK ? '🌙' : '☀️'}
47+
</button>
48+
);
49+
}

src/components/Navigation/Navigation.jsx

Lines changed: 162 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,72 @@
11
// Import External Dependencies
22
import { useEffect, useState } from 'react';
3-
import Banner from 'react-banner';
43
import PropTypes from 'prop-types';
4+
import { DocSearch } from '@docsearch/react';
5+
import { Link as ReactDOMLink } from 'react-router-dom';
56

67
// Import Components
78
import Link from '../Link/Link';
89
import Logo from '../Logo/Logo';
910
import Dropdown from '../Dropdown/Dropdown';
1011

11-
// Import helpers
12-
import isClient from '../../utilities/is-client';
13-
14-
// Import constants
15-
import { THEME } from '../../constants/theme';
16-
1712
// Load Styling
18-
import 'docsearch.js/dist/cdn/docsearch.css';
19-
import './Navigation.scss';
20-
import './Search.scss';
13+
import '@docsearch/css';
2114

2215
import GithubIcon from '../../styles/icons/github.svg';
2316
import TwitterIcon from '../../styles/icons/twitter.svg';
2417
import StackOverflowIcon from '../../styles/icons/stack-overflow.svg';
18+
import Hamburger from '../../styles/icons/hamburger.svg';
19+
import { NavLink, useLocation } from 'react-router-dom';
20+
import HelloDarkness from '../HelloDarkness';
2521

26-
const onSearch = () => {};
27-
const { DARK, LIGHT } = THEME;
22+
NavigationItem.propTypes = {
23+
children: PropTypes.node.isRequired,
24+
url: PropTypes.string.isRequired,
25+
isActive: PropTypes.func,
26+
};
27+
28+
function NavigationItem({ children, url, isActive }) {
29+
let obj = {};
30+
// decide if the link is active or not by providing a function
31+
// otherwise we'll let react-dom makes the decision for us
32+
if (isActive) {
33+
obj = {
34+
isActive,
35+
};
36+
}
37+
return (
38+
<NavLink
39+
{...obj}
40+
activeClassName="active-menu"
41+
to={url}
42+
className="text-gray-100 dark:text-gray-100 text-sm font-light uppercase hover:text-blue-200"
43+
>
44+
{children}
45+
</NavLink>
46+
);
47+
}
48+
49+
NavigationIcon.propTypes = {
50+
children: PropTypes.node.isRequired,
51+
to: PropTypes.string.isRequired,
52+
title: PropTypes.string.isRequired,
53+
};
54+
function NavigationIcon({ children, to, title }) {
55+
return (
56+
<Link
57+
to={to}
58+
className="inline-flex items-center"
59+
title={`webpack on ${title}`}
60+
>
61+
{children}
62+
</Link>
63+
);
64+
}
65+
const navigationIconProps = {
66+
'aria-hidden': true,
67+
fill: '#fff',
68+
width: 16,
69+
};
2870

2971
Navigation.propTypes = {
3072
pathname: PropTypes.string,
@@ -35,66 +77,62 @@ Navigation.propTypes = {
3577
switchTheme: PropTypes.func,
3678
};
3779

38-
function Navigation({
39-
pathname,
40-
hash = '',
41-
links,
42-
toggleSidebar,
43-
theme,
44-
switchTheme,
45-
}) {
46-
const themeSwitcher = () => switchTheme(theme === DARK ? LIGHT : DARK);
80+
function Navigation({ links, pathname, hash = '', toggleSidebar }) {
4781
const [locationHash, setLocationHash] = useState(hash);
4882

49-
useEffect(() => {
50-
if (isClient) {
51-
const DocSearch = require('docsearch.js');
52-
53-
DocSearch({
54-
apiKey: 'fac401d1a5f68bc41f01fb6261661490',
55-
indexName: 'webpack-js-org',
56-
inputSelector: '.navigation-search__input',
57-
});
58-
}
59-
}, []);
83+
const location = useLocation();
6084

6185
useEffect(() => {
6286
setLocationHash(hash);
6387
}, [hash]);
6488

6589
return (
66-
<Banner
67-
onSearch={onSearch}
68-
blockName="navigation"
69-
logo={<Logo light={true} />}
70-
url={pathname}
71-
items={[
72-
...links,
73-
{
74-
title: 'GitHub Repository',
75-
url: 'https://github.com/webpack/webpack',
76-
className: 'navigation__item--icon',
77-
content: <GithubIcon aria-hidden="true" fill="#fff" width={16} />,
78-
},
79-
{
80-
title: 'webpack on Twitter',
81-
url: 'https://twitter.com/webpack',
82-
className: 'navigation__item--icon',
83-
content: <TwitterIcon aria-hidden="true" fill="#fff" width={16} />,
84-
},
85-
{
86-
title: 'webpack on Stack Overflow',
87-
url: 'https://stackoverflow.com/questions/tagged/webpack',
88-
className: 'navigation__item--icon',
89-
content: (
90-
<StackOverflowIcon aria-hidden="true" fill="#fff" width={16} />
91-
),
92-
},
93-
{
94-
className: 'navigation__item--icon',
95-
content: (
90+
<>
91+
<header className="bg-blue-800 dark:bg-gray-900">
92+
<div className="flex items-center py-10 px-[16px] justify-between md:px-[24px] md:max-w-[1024px] md:mx-auto md:justify-start">
93+
<button
94+
className="bg-transparent border-none md:hidden"
95+
onClick={toggleSidebar}
96+
>
97+
<Hamburger
98+
width={20}
99+
height={20}
100+
className="fill-current text-white"
101+
/>
102+
</button>
103+
<Link to="/" className="md:mr-auto">
104+
<Logo />
105+
</Link>
106+
<nav className="hidden md:inline-grid md:grid-flow-col md:gap-x-[18px]">
107+
{links.map(({ content, url, isActive }) => (
108+
<NavigationItem key={url} url={url} isActive={isActive}>
109+
{content}
110+
</NavigationItem>
111+
))}
112+
{[
113+
{
114+
to: 'https://github.com/webpack/webpack',
115+
title: 'GitHub',
116+
children: <GithubIcon {...navigationIconProps} />,
117+
},
118+
{
119+
to: 'https://twitter.com/webpack',
120+
title: 'Twitter',
121+
children: <TwitterIcon {...navigationIconProps} />,
122+
},
123+
{
124+
to: 'https://stackoverflow.com/questions/tagged/webpack',
125+
title: 'StackOverflow',
126+
children: <StackOverflowIcon {...navigationIconProps} />,
127+
},
128+
].map(({ to, title, children }) => (
129+
<NavigationIcon key={to} to={to} title={title}>
130+
{children}
131+
</NavigationIcon>
132+
))}
133+
96134
<Dropdown
97-
className="navigation__languages"
135+
className=""
98136
items={[
99137
{
100138
title: 'English',
@@ -112,28 +150,68 @@ function Navigation({
112150
},
113151
]}
114152
/>
115-
),
116-
},
117-
{
118-
className: 'navigation__item--icon',
119-
content: (
120-
<button
121-
style={{
122-
background: 'transparent',
123-
border: 'none',
124-
cursor: 'pointer',
153+
</nav>
154+
<div className="inline-flex items-center ml-[18px]">
155+
<HelloDarkness />
156+
<DocSearch
157+
apiKey={'fac401d1a5f68bc41f01fb6261661490'}
158+
indexName="webpack-js-org"
159+
disableUserPersonalization={true}
160+
placeholder="Search webpack documentation"
161+
transformItems={(items) =>
162+
items.map(({ url, ...others }) => {
163+
const { origin } = new URL(url);
164+
return {
165+
...others,
166+
url: url.replace(new RegExp(`^${origin}`), ''),
167+
};
168+
})
169+
}
170+
hitComponent={({ hit, children }) => {
171+
return <ReactDOMLink to={hit.url}>{children}</ReactDOMLink>;
125172
}}
126-
onClick={themeSwitcher}
127-
data-testid="hello-darkness"
128-
>
129-
{theme === DARK ? '🌙' : '☀️'}
130-
</button>
131-
),
132-
},
133-
]}
134-
link={Link}
135-
onMenuClick={toggleSidebar}
136-
/>
173+
/>
174+
</div>
175+
</div>
176+
{/* sub navigation */}
177+
{links
178+
.filter((link) => {
179+
// only those with children are displayed
180+
return link.children;
181+
})
182+
.map((link) => {
183+
if (link.isActive) {
184+
// hide the children if the link is not active
185+
if (!link.isActive({}, location)) {
186+
return null;
187+
}
188+
}
189+
return (
190+
<div
191+
key={link.url}
192+
className="bg-gray-100 dark:bg-gray-800 hidden md:block"
193+
>
194+
<div
195+
className="md:max-w-[1024px] md:mx-auto md:grid md:grid-flow-col md:justify-end md:gap-x-[20px] md:px-[24px]"
196+
data-testid="sub-navigation"
197+
>
198+
{link.children.map((child) => (
199+
<NavLink
200+
key={child.url}
201+
to={child.url}
202+
title={child.title}
203+
className="text-blue-400 py-5 text-sm capitalize"
204+
activeClassName="active-submenu"
205+
>
206+
{child.content}
207+
</NavLink>
208+
))}
209+
</div>
210+
</div>
211+
);
212+
})}
213+
</header>
214+
</>
137215
);
138216
}
139217

0 commit comments

Comments
 (0)