Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat): new app switcher #1149

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Search } from '@carbon/react';
import styles from './app-search-bar.scss';

import { type AssignedExtension, Extension, useConnectedExtensions } from '@openmrs/esm-framework';
import { ComponentContext } from '@openmrs/esm-framework/src/internal';

const appMenuItemSlot = 'app-menu-item-slot';

interface AppSearchBarProps {
onChange?: (searchTerm: string) => void;
onClear: () => void;
onSubmit: (searchTerm: string) => void;
small?: boolean;
}

const AppSearchBar = React.forwardRef<HTMLInputElement, AppSearchBarProps>(
({ onChange, onClear, onSubmit, small }, ref) => {
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState('');
const menuItemExtensions = useConnectedExtensions(appMenuItemSlot) as AssignedExtension[];

const handleChange = (val: string) => {
setSearchTerm(val);
if (onChange) {
onChange(val);
}
};

const handleSubmit = (evt: React.FormEvent) => {
evt.preventDefault();
if (onSubmit) {
onSubmit(searchTerm);
}
};

const filteredExtensions = menuItemExtensions
.filter((extension) => {
const itemName = extension?.name ?? '';
return itemName.toLowerCase().includes(searchTerm.toLowerCase());
})
.map((extension) => (
<ComponentContext.Provider
key={extension?.id}
value={{
featureName: 'app-menu',
moduleName: extension?.moduleName,
extension: {
extensionId: extension?.id,
extensionSlotName: appMenuItemSlot,
extensionSlotModuleName: extension?.moduleName,
},
}}
>
<Extension />
</ComponentContext.Provider>
));

return (
<>
<form onSubmit={handleSubmit} className={styles.searchArea}>
<Search
autoFocus
className={styles.appSearchInput}
closeButtonLabelText={t('clearSearch', 'Clear')}
labelText=""
onChange={(event) => handleChange(event.target.value)}
onClear={onClear}
placeholder={t('searchForModule', 'Search for module')}
size={small ? 'sm' : 'lg'}
value={searchTerm}
ref={ref}
data-testid="appSearchBar"
/>
</form>
<div className={styles.searchItems}>
{searchTerm
? filteredExtensions
: menuItemExtensions.map((extension) => (
<ComponentContext.Provider
key={extension?.id}
value={{
featureName: 'app-menu',
moduleName: extension?.moduleName,
extension: {
extensionId: extension?.id,
extensionSlotName: appMenuItemSlot,
extensionSlotModuleName: extension?.moduleName,
},
}}
>
<Extension />
</ComponentContext.Provider>
))}
</div>
</>
);
},
);

export default AppSearchBar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@use '@carbon/styles/scss/colors';
@use '@carbon/styles/scss/spacing';
@import '~@openmrs/esm-styleguide/src/vars';
.appSearchInput {
border: none;
}

.searchItems {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin: 0.5rem 0 0 0.5rem;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { HeaderGlobalAction } from '@carbon/react';
import { Close, Switcher } from '@carbon/react/icons';
import { isDesktop, navigate, useLayoutType, useOnClickOutside } from '@openmrs/esm-framework';
import AppSearchOverlay from '../app-search-overlay/app-search-overlay.component';
import styles from './app-search-icon.scss';
import { useParams, useSearchParams } from 'react-router-dom';

interface AppSearchLaunchProps {}

const AppSearchLaunch: React.FC<AppSearchLaunchProps> = () => {
const { t } = useTranslation();
const layout = useLayoutType();
const { page } = useParams();
const isSearchPage = useMemo(() => page === 'search', [page]);
const [searchParams] = useSearchParams();
const initialSearchTerm = isSearchPage ? searchParams.get('query') : '';

const [showSearchInput, setShowSearchInput] = useState(false);
const [canClickOutside, setCanClickOutside] = useState(false);

const handleCloseSearchInput = useCallback(() => {
if (isDesktop(layout) && !isSearchPage) {
setShowSearchInput(false);
}
}, [setShowSearchInput, isSearchPage, layout]);

const ref = useOnClickOutside<HTMLDivElement>(handleCloseSearchInput, canClickOutside);

const handleGlobalAction = useCallback(() => {
if (showSearchInput) {
if (isSearchPage) {
navigate({
to: window.sessionStorage.getItem('searchReturnUrl') ?? '${openmrsSpaBase}/',
});
window.sessionStorage.removeItem('searchReturnUrl');
}
setShowSearchInput(false);
} else {
setShowSearchInput(true);
}
}, [isSearchPage, setShowSearchInput, showSearchInput]);

useEffect(() => {
// Search input should always be open when we direct to the search page.
setShowSearchInput(isSearchPage);
if (isSearchPage) {
setCanClickOutside(false);
}
}, [isSearchPage]);

useEffect(() => {
showSearchInput ? setCanClickOutside(true) : setCanClickOutside(false);
}, [showSearchInput]);

return (
<div className={styles.appSearchIconWrapper} ref={ref}>
{showSearchInput && <AppSearchOverlay onClose={handleGlobalAction} query={initialSearchTerm} />}

<div className={`${showSearchInput && styles.closeButton}`}>
<HeaderGlobalAction
aria-label={t('searchModule', 'Search Module')}
aria-labelledby="Search Module"
className={`${showSearchInput ? styles.activeSearchIconButton : styles.searchIconButton}`}
enterDelayMs={500}
name="SearchModuleIcon"
data-testid="searchModuleIcon"
onClick={handleGlobalAction}
>
{showSearchInput ? <Close size={20} /> : <Switcher size={20} />}
</HeaderGlobalAction>
</div>
</div>
);
};

export default AppSearchLaunch;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@import '~@openmrs/esm-styleguide/src/vars';

.appSearchIconWrapper {
display: flex;
justify-content: flex-end;
align-items: center;
}

.searchIconButton {
@include brand-01(background-color);
}

.activeSearchIconButton {
@include brand-02(background-color);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import AppSearchBar from '../app-search-bar/app-search-bar.component';
import debounce from 'lodash-es/debounce';

interface AppSearchOverlayProps {
onClose: () => void;
query?: string;
header?: string;
}

const AppSearchOverlay: React.FC<AppSearchOverlayProps> = ({ onClose, query = '', header }) => {
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState(query);
const handleClear = useCallback(() => setSearchTerm(''), [setSearchTerm]);

useEffect(() => {
if (query) {
setSearchTerm(query);
}
}, [query]);

const onSearchQueryChange = debounce((val) => {
setSearchTerm(val);
}, 300);

return <AppSearchBar onSubmit={onSearchQueryChange} onChange={onSearchQueryChange} onClear={handleClear} />;
};

export default AppSearchOverlay;
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import OfflineBanner from '../offline-banner/offline-banner.component';
import UserMenuPanel from '../navbar-header-panels/user-menu-panel.component';
import SideMenuPanel from '../navbar-header-panels/side-menu-panel.component';
import styles from './navbar.scss';
import AppSearchLaunch from '../app-menu/app-search-icon/app-search-icon.component';

const HeaderItems: React.FC = () => {
const { t } = useTranslation();
Expand Down Expand Up @@ -115,7 +116,7 @@ const HeaderItems: React.FC = () => {
)}
</HeaderGlobalBar>
{!isDesktop(layout) && <SideMenuPanel hidePanel={hidePanel('sideMenu')} expanded={isActivePanel('sideMenu')} />}
{showAppMenu && <AppMenuPanel expanded={isActivePanel('appMenu')} hidePanel={hidePanel('appMenu')} />}
{showAppMenu && <AppSearchLaunch />}
<NotificationsMenuPanel expanded={isActivePanel('notificationsMenu')} />
{showUserMenu && <UserMenuPanel expanded={isActivePanel('userMenu')} hidePanel={hidePanel('userMenu')} />}
</Header>
Expand Down
3 changes: 3 additions & 0 deletions packages/apps/esm-primary-navigation-app/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"change": "Change",
"changeLanguage": "Change language",
"changingLanguage": "Changing language",
"clearSearch": "Clear",
"notifications": "Notifications",
"searchApp": "Search App",
"searchForModule": "Search for module",
"userMenuTooltip": "My Account"
}
Loading