Skip to content

Commit

Permalink
tmp search
Browse files Browse the repository at this point in the history
  • Loading branch information
MariaAga committed Apr 27, 2023
1 parent a27fb9a commit 9a6c112
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ const subItemsA = [
href: '/c',
id: 'menu_item_cc',
},

{
title: 'Ac',
isDivider: false,
onClick: mockOnClick,
href: '/ac',
id: 'menu_item_ac',
},
];
const subItemsB = [
{
Expand All @@ -26,7 +34,7 @@ const subItemsB = [
},
];

const PFitems = [
export const PFitems = [
{
title: 'Monitor',
initialActive: true,
Expand All @@ -40,31 +48,6 @@ const PFitems = [
subItems: subItemsB,
},
];
// Server Hash Data
const monitorChildren = [
{
type: 'item',
name: 'Dashboard',
title: 'Dashboard',
exact: true,
url: '/',
},
{
type: 'item',
name: 'Facts',
title: 'Facts',
url: '/fact_values',
},
];

const hostsChildren = [
{
type: 'item',
name: 'All Hosts',
title: 'All Hosts',
url: '/hosts/new',
},
];

const namelessChildren = [
{
Expand All @@ -76,21 +59,6 @@ const namelessChildren = [
},
];

const hashItemsA = [
{
type: 'sub_menu',
name: 'Monitor',
icon: 'fa fa-tachometer',
children: monitorChildren,
},
{
type: 'sub_menu',
name: 'Hosts',
icon: 'fa fa-server',
children: hostsChildren,
},
];

export const hashItemNameless = [
{
type: 'sub_menu',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { snakeCase } from 'lodash';
import { noop } from '../../common/helpers';

export const selectLayout = state => state.layout;

Expand Down Expand Up @@ -37,7 +36,6 @@ const childToMenuItem = (child, currentLocation, currentOrganization) => ({
child.title === currentLocation || child.title === currentOrganization
? 'mobile-active'
: '',
href: child.url || '#',
preventHref: true,
onClick: child.onClick || noop,
href: child.url,
onClick: child.onClick,
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
NavItemSeparator,
} from '@patternfly/react-core';
import { getCurrentPath } from './LayoutHelper';
import { NavigationSearch } from './NavigationSearch';

const titleWithIcon = (title, iconClass) => (
<div>
Expand Down Expand Up @@ -43,7 +44,7 @@ const Navigation = ({

items.forEach(item => {
item.subItems.forEach(subItem => {
if (!subItem.isDivider) {
if (!subItem.isDivider && subItem.href) {
// don't keep the query parameters for the key
subItemToItemMap[pathFragment(subItem.href)] = item.title;
}
Expand All @@ -69,7 +70,7 @@ const Navigation = ({
} else {
groups[currIndex].groupItems.push({
...sub,
isActive: pathname === sub.href.split('?')[0],
isActive: pathname === sub.href?.split('?')[0],
});
}
});
Expand All @@ -80,10 +81,12 @@ const Navigation = ({
[items.length]
);

if (!items.length) return null;
const groupedItems = getGroupedItems;
return (
<Nav id="foreman-nav">
<NavList>
<NavigationSearch items={items} />
{groupedItems.map(({ title, iconClass, groups, className }, index) => (
<React.Fragment key={index}>
<NavExpandable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import {
Menu,
MenuContent,
MenuItem,
MenuList,
Popper,
SearchInput,
} from '@patternfly/react-core';
import { translate as __ } from '../../common/I18n';

export const NavigationSearch = ({ items }) => {
const navLinks = {};
let parent = null;
items.forEach(item => {
item.subItems.forEach(group => {
if (group.isDivider) {
parent = group.title;
} else {
navLinks[group.title] = {
...group,
parents: [item.title, parent].filter(Boolean),
};
}
});
parent = null;
});

const navItems = Object.keys(navLinks);
const menuNav = navItem => (
<MenuItem
to={navLinks[navItem].onClick ? null : navLinks[navItem].href}
onClick={navLinks[navItem].onClick ? navLinks[navItem].onClick : null}
itemId={navItem}
key={navItem}
description={[...navLinks[navItem].parents, navItem].join(' > ')}
>
{navItem}
</MenuItem>
);
const [value, setValue] = React.useState('');
const [isExpanded, setIsExpanded] = useState(false);
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false);
const onToggleExpand = (_event, _isExpanded) => {
setIsExpanded(!_isExpanded);
if (_isExpanded) {
setIsAutocompleteOpen(false);
}
};
const [autocompleteOptions, setAutocompleteOptions] = useState(
navItems.slice(0, 10).map(menuNav)
);

const searchInputRef = useRef(null);
const autocompleteRef = useRef(null);

const onChange = newValue => {
if (
newValue !== '' &&
searchInputRef &&
searchInputRef.current &&
searchInputRef.current.contains(document.activeElement)
) {
setIsAutocompleteOpen(true);

// When the value of the search input changes, build a list of no more than 10 autocomplete options.
// Options which start with the search input value are listed first, followed by options which contain
// the search input value.

let options = navItems
.filter(option => option.toLowerCase().includes(newValue.toLowerCase()))
.map(menuNav);
if (options.length > 10) {
options = options.slice(0, 10);
} else {
options = [
...options,
...navItems
.filter(
option =>
!option.includes(newValue.toLowerCase()) &&
option.includes(newValue.toLowerCase())
)
.map(menuNav),
].slice(0, 10);
}

// The menu is hidden if there are no options
setIsAutocompleteOpen(options.length > 0);

setAutocompleteOptions(options);
} else {
setIsAutocompleteOpen(false);
}
setValue(newValue);
};

// Whenever an autocomplete option is selected, set the search input value, close the menu, and put the browser
// focus back on the search input
const onSelect = (e, itemId) => {
e.stopPropagation();
setValue(itemId);
setIsAutocompleteOpen(false);
searchInputRef.current.focus();
};

useEffect(() => {
const handleMenuKeys = event => {
// keyboard shortcut to focus the search, will not focus if the key is typed into an input
if (
event.key === '/' &&
event.target.tagName !== 'INPUT' &&
event.target.tagName !== 'TEXTAREA'
) {
event.preventDefault();
searchInputRef.current.focus();
}
// if the autocomplete is open and the browser focus is on the search input,
else if (isAutocompleteOpen && searchInputRef?.current === event.target) {
// the escape key closes the autocomplete menu and keeps the focus on the search input.
if (event.key === 'Escape') {
setIsAutocompleteOpen(false);
searchInputRef.current.focus();
// the up and down arrow keys move browser focus into the autocomplete menu
} else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
const firstElement = autocompleteRef.current.querySelector(
'li > button:not(:disabled)'
);
firstElement && firstElement.focus();
event.preventDefault(); // by default, the up and down arrow keys scroll the window
} else if (
autocompleteRef?.current?.contains(event.target) &&
event.key === 'Tab'
) {
event.preventDefault();

setIsAutocompleteOpen(false);
searchInputRef.current.focus();
}
}
};
// The autocomplete menu should close if the user clicks outside the menu.
const handleClickOutside = event => {
if (
isAutocompleteOpen &&
autocompleteRef?.current &&
!autocompleteRef.current.contains(event.target) &&
searchInputRef?.current &&
!searchInputRef.current.contains(event.target)
) {
setIsAutocompleteOpen(false);
}
};
window.addEventListener('keydown', handleMenuKeys);
window.addEventListener('click', handleClickOutside);

return () => {
window.removeEventListener('keydown', handleMenuKeys);
window.removeEventListener('click', handleClickOutside);
};
}, [isAutocompleteOpen]);

const searchInput = (
<SearchInput
className="navigation-search"
placeholder={__('Search and go')}
value={value}
onChange={onChange}
expandableInput={{
isExpanded,
onToggleExpand,
toggleAriaLabel: 'Expandable search input toggle',
}}
ref={searchInputRef}
id="navigation-search"
onClick={e => {
// if the user clicks on the search input, open the autocomplete menu
if (e.target.type === 'text') setIsAutocompleteOpen(true);
else if (!isExpanded) setIsExpanded(true);
}}
/>
);

const autocomplete = (
<Menu
ref={autocompleteRef}
onSelect={onSelect}
className="navigation-search-menu"
>
<MenuContent>
<MenuList>{autocompleteOptions}</MenuList>
</MenuContent>
</Menu>
);

return (
<Popper
trigger={searchInput}
popper={autocomplete}
isVisible={isAutocompleteOpen}
enableFlip={false}
appendTo={() =>
document.querySelector('.pf-c-masthead.pf-m-display-inline')
}
/>
);
};
NavigationSearch.propTypes = {
items: PropTypes.array.isRequired,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import { act, render, fireEvent } from '@testing-library/react';
import { NavigationSearch } from '../NavigationSearch';
import { PFitems } from '../Layout.fixtures';

describe('NavigationSearch', () => {
const items = PFitems;
it('should display autocomplete options when input is typed', async () => {
const {
queryAllByRole,
getByPlaceholderText,
getByRole,
getByLabelText,
} = render(
<div className="pf-c-masthead pf-m-display-inline">
<NavigationSearch items={items} />
</div>
);
fireEvent.click(getByRole('button'));
const input = getByPlaceholderText('Search and go');
await act(async () => {
await fireEvent.change(input, { target: { value: 'a' } });
});
expect(queryAllByRole('menuitem')).toHaveLength(2);
fireEvent.click(getByLabelText('Expandable search input toggle'));
expect(queryAllByRole('menuitem')).toHaveLength(0);
});
});
Loading

0 comments on commit 9a6c112

Please sign in to comment.