Skip to content

Commit

Permalink
Navigation component + Page/Layout concept (#42)
Browse files Browse the repository at this point in the history
This PR adds the navigation component, and also reworks the react-router usage into a Page and Layout system.

For the Navigation resizing, I tried to use resize observer, first with https://www.npmjs.com/package/@react-hook/resize-observer and then with ResizeObserver directly. The third party hook did not seem to work well for our use case,
and for using ResizeObserver directly, it turns out the TypeScript types for ResizeObserver are not included by default,
and I felt a little apprehensive to include third party types for it, though it would probably be fine.
microsoft/TypeScript#28502

J=SLAP-1558
TEST=manual

test resizing the page and seeing tabs appear and disappear
test that switching tabs will run searches
  • Loading branch information
oshi97 committed Oct 18, 2021
1 parent 0bd77b4 commit c85e3de
Show file tree
Hide file tree
Showing 15 changed files with 450 additions and 181 deletions.
6 changes: 3 additions & 3 deletions THIRD-PARTY-NOTICES
Expand Up @@ -32,7 +32,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

The following NPM package may be included in this product:

- @reduxjs/toolkit@1.6.1
- @reduxjs/toolkit@1.6.2

This package contains the following license and notice below:

Expand Down Expand Up @@ -96,7 +96,7 @@ The following NPM packages may be included in this product:

- @yext/answers-core@1.3.2
- @yext/answers-headless-react@0.3.0-beta.0
- @yext/answers-headless@0.0.4
- @yext/answers-headless@0.0.5

These packages each contain the following license and notice below:

Expand Down Expand Up @@ -220,7 +220,7 @@ SOFTWARE.

The following NPM package may be included in this product:

- immer@9.0.5
- immer@9.0.6

This package contains the following license and notice below:

Expand Down
195 changes: 25 additions & 170 deletions sample-app/src/App.tsx
@@ -1,184 +1,39 @@
import './sass/App.scss';
import VerticalSearchPage from './pages/VerticalSearchPage';
import UniversalSearchPage from './pages/UniversalSearchPage';
import PageRouter from './PageRouter';
import StandardLayout from './pages/StandardLayout';
import { AnswersActionsProvider } from '@yext/answers-headless-react';
import AlternativeVerticals from './components/AlternativeVerticals';
import DecoratedAppliedFilters from './components/DecoratedAppliedFilters';
import { StandardCard } from './components/cards/StandardCard';
import ResultsCount from './components/ResultsCount';
import SearchBar from './components/SearchBar';
import StaticFilters from './components/StaticFilters';
import VerticalResults from './components/VerticalResults';
import SpellCheck from './components/SpellCheck';
import LocationBias from './components/LocationBias';
import UniversalResults from './components/UniversalResults';
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Facets from './components/Facets';

function App() {
const staticFilterOptions = [
{
label: 'canada',
fieldId: 'c_employeeCountry',
value: 'Canada',
},
{
label: 'remote',
fieldId: 'c_employeeCountry',
value: 'Remote'
},
{
label: 'usa',
fieldId: 'c_employeeCountry',
value: 'United States',
},
{
label: 'tech',
fieldId: 'c_employeeDepartment',
value: 'Technology'
},
{
label: 'consult',
fieldId: 'c_employeeDepartment',
value: 'Consulting',
},
{
label: 'fin',
fieldId: 'c_employeeDepartment',
value: 'Finance',
}
]

const universalResultsConfig = {
people: {
label: "People",
viewMore: true,
cardConfig: {
CardComponent: StandardCard,
showOrdinal: true
}
},
events: {
label: "events",
cardConfig: {
CardComponent: StandardCard,
showOrdinal: true
}
},
links: {
label: "links",
viewMore: true,
cardConfig: {
CardComponent: StandardCard,
showOrdinal: true
}
},
financial_professionals: {
label: "Financial Professionals",
},
healthcare_professionals: {
label: "Healthcare Professionals",
import { universalResultsConfig } from './universalResultsConfig';

const routes = [
{
path: '/',
exact: true,
page: <UniversalSearchPage universalResultsConfig={universalResultsConfig} />
},
...Object.keys(universalResultsConfig).map(key => {
return {
path: `/${key}`,
page: <VerticalSearchPage verticalKey={key} />
}
}

const universalResultsFilterConfig = {
show: true
};

const facetConfigs = {
c_employeeDepartment: {
label: 'Employee Department!'
}
}
})
];

export default function App() {
return (
<AnswersActionsProvider
apiKey='2d8c550071a64ea23e263118a2b0680b'
experienceKey='slanswers'
locale='en'
verticalKey='people'
>
{/*
TODO: use Navigation component for routing when that's added to repo.
current setup is for testing purposes.
*/}
<Router>
<Switch>
{/* universal search */}
<Route exact path='/'>
<div className='start'>
test
</div>
<div className='end'>
<SearchBar
placeholder='Search...'
isVertical={false}
/>
<div>
<UniversalResults
appliedFiltersConfig={universalResultsFilterConfig}
verticalConfigs={universalResultsConfig}
/>
</div>
</div>
</Route>

{/* vertical page */}
<Route path={Object.keys(universalResultsConfig).map(key => `/${key}`)}>
<div>
A VERTICAL PAGE!
</div>
</Route>

{/* vertical search */}
<Route exact path='/vertical'>
<div className='start'>
test
<StaticFilters
title='~Country and Employee Departments~'
options={staticFilterOptions}
/>
<Facets
searchOnChange={true}
searchable={true}
collapsible={true}
defaultExpanded={true}
facetConfigs={facetConfigs}
/>
<SpellCheck
isVertical={true}
/>
</div>
<div className='end'>
<SearchBar
placeholder='Search...'
isVertical={true}
/>
<div>
<ResultsCount />
<DecoratedAppliedFilters
showFieldNames={true}
hiddenFields={['builtin.entityType']}
delimiter='|'
/>
<AlternativeVerticals
currentVerticalLabel='People'
verticalsConfig={[
{ label: 'Locations', verticalKey: 'KM' },
{ label: 'FAQs', verticalKey: 'faq' }
]}
/>
<VerticalResults
CardComponent={StandardCard}
cardConfig={{ showOrdinal: true }}
displayAllResults={true}
/>
<LocationBias isVertical={false} />
</div>
</div>
</Route>
</Switch>
</Router>
<div className='App'>
<PageRouter
Layout={StandardLayout}
routes={routes}
/>
</div>
</AnswersActionsProvider>
);
}

export default App;
41 changes: 41 additions & 0 deletions sample-app/src/PageRouter.tsx
@@ -0,0 +1,41 @@
import { ComponentType } from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';

interface RouteData {
path: string
page: JSX.Element
exact?: boolean
}

export type LayoutComponent = ComponentType<{ page: JSX.Element }>

interface PageProps {
Layout?: LayoutComponent
routes: RouteData[]
}

/**
* PageRouter abstracts away logic surrounding react-router, and provides an easy way
* to specify a {@link LayoutComponent} for a page.
*/
export default function PageRouter({ Layout, routes }: PageProps) {
const pages = routes.map(routeData => {
const { path, page, exact } = routeData;
if (Layout) {
return (
<Route key={path} path={path} exact={exact}>
<Layout page={page}/>
</Route>
);
}
return <Route key={path} path={path} exact={exact}>{page}</Route>;
});

return (
<Router>
<Switch>
{pages}
</Switch>
</Router>
);
}
99 changes: 99 additions & 0 deletions sample-app/src/components/Navigation.tsx
@@ -0,0 +1,99 @@
import classNames from 'classnames';
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { NavLink, useLocation } from 'react-router-dom';
import { ReactComponent as KebabIcon } from '../icons/kebab.svg';
import '../sass/Navigation.scss';

interface LinkData {
to: string
label: string
}

interface NavigationProps {
links: LinkData[]
}

export default function Navigation({ links }: NavigationProps) {
// Close the menu when clicking the document
const [menuOpen, setMenuOpen] = useState<boolean>(false);
const menuRef = useRef<HTMLButtonElement>(null);
const handleDocumentClick = (e: MouseEvent) => {
if (e.target !== menuRef.current) {
setMenuOpen(false);
}
};
useLayoutEffect(() => {
document.addEventListener('click', handleDocumentClick)
return () => document.removeEventListener('click', handleDocumentClick);
}, []);

// Responsive tabs
const [numOverflowLinks, setNumOverflowLinks] = useState<number>(0);
const navigationRef = useRef<HTMLDivElement>(null);
const handleResize = useCallback(() => {
const navEl = navigationRef.current;
if (!navEl) {
return;
}
const isOverflowing = navEl.scrollWidth > navEl.offsetWidth;
if (isOverflowing && numOverflowLinks < links.length) {
setNumOverflowLinks(numOverflowLinks + 1);
}
}, [links.length, numOverflowLinks])
useLayoutEffect(handleResize, [handleResize]);
useEffect(() => {
let timeoutId: NodeJS.Timeout;
function resizeListener() {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
setNumOverflowLinks(0);
handleResize()
}, 50)
};
window.addEventListener('resize', resizeListener);
return () => window.removeEventListener('resize', resizeListener);
}, [handleResize]);

const { search } = useLocation();
const visibleLinks = links.slice(0, links.length - numOverflowLinks);
const overflowLinks = links.slice(-numOverflowLinks);
const menuButtonClassNames = classNames('Navigation__menuButton', {
'Navigation__menuButton--open': menuOpen
});
return (
<nav className='Navigation' ref={navigationRef}>
<div className='Navigation__links'>
{visibleLinks.map(l => renderLink(l, search))}
</div>
{numOverflowLinks > 0 &&
<div className='Navigation__menuWrapper'>
<button
className={menuButtonClassNames}
ref={menuRef}
onClick={() => setMenuOpen(!menuOpen)}
>
<KebabIcon /> More
</button>
<div className='Navigation__menuLinks'>
{menuOpen && overflowLinks.map(l => renderLink(l, search))}
</div>
</div>
}
</nav>
)
}

function renderLink(linkData: LinkData, queryParams: string) {
const { to, label } = linkData;
return (
<NavLink
key={to}
className='Navigation__link'
activeClassName='Navigation__link--currentRoute'
to={`${to}${queryParams}`}
exact={true}
>
{label}
</NavLink>
)
}
2 changes: 1 addition & 1 deletion sample-app/src/components/UniversalResults.tsx
Expand Up @@ -8,7 +8,7 @@ import { CardConfig } from '../models/cardComponent';
import classNames from "classnames";
import '../sass/UniversalResults.scss';

interface VerticalConfig {
export interface VerticalConfig {
SectionComponent?: SectionComponent,
cardConfig?: CardConfig,
label?: string,
Expand Down

0 comments on commit c85e3de

Please sign in to comment.