Skip to content
Merged
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
26 changes: 9 additions & 17 deletions apps/meteor/client/NavBarV2/NavBar.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useToolbar } from '@react-aria/toolbar';
import { NavBar as NavBarComponent, NavBarSection, NavBarGroup, NavBarDivider } from '@rocket.chat/fuselage';
import { usePermission, useTranslation, useUser } from '@rocket.chat/ui-contexts';
import { usePermission, useUser } from '@rocket.chat/ui-contexts';
import { useVoipState } from '@rocket.chat/ui-voip';
import { useRef } from 'react';
import { useTranslation } from 'react-i18next';

import NavbarNavigation from './NavBarNavigation';
import {
NavBarItemOmniChannelCallDialPad,
NavBarItemOmnichannelContact,
Expand All @@ -25,7 +25,7 @@ import { useOmnichannelEnabled } from '../hooks/omnichannel/useOmnichannelEnable
import { useOmnichannelShowQueueLink } from '../hooks/omnichannel/useOmnichannelShowQueueLink';

const NavBar = () => {
const t = useTranslation();
const { t } = useTranslation();
const user = useUser();

const showOmnichannel = useOmnichannelEnabled();
Expand All @@ -38,30 +38,22 @@ const NavBar = () => {
const isCallReady = useIsCallReady();
const { isEnabled: showVoip } = useVoipState();

const pagesToolbarRef = useRef(null);
const { toolbarProps: pagesToolbarProps } = useToolbar({ 'aria-label': t('Pages') }, pagesToolbarRef);

const omnichannelToolbarRef = useRef(null);
const { toolbarProps: omnichannelToolbarProps } = useToolbar({ 'aria-label': t('Omnichannel') }, omnichannelToolbarRef);

const voipToolbarRef = useRef(null);
const { toolbarProps: voipToolbarProps } = useToolbar({ 'aria-label': t('Voice_Call') }, voipToolbarRef);

return (
<NavBarComponent aria-label='header'>
<NavBarSection>
<NavBarGroup role='toolbar' ref={pagesToolbarRef} {...pagesToolbarProps}>
<NavBarGroup aria-label={t('Pages_and_actions')}>
<NavBarItemHomePage title={t('Home')} />
<NavBarItemDirectoryPage title={t('Directory')} />
{showMarketplace && <NavBarItemMarketPlaceMenu />}
<NavBarItemCreateNew />
<NavBarItemSort />
</NavBarGroup>
</NavBarSection>
<NavbarNavigation />
<NavBarSection>
{showVoip && (
<>
<NavBarGroup role='toolbar' ref={voipToolbarRef} {...voipToolbarProps}>
<NavBarGroup aria-label={t('Voice_Call')}>
<NavBarItemVoipDialer primary={isCallEnabled} />
<NavBarItemVoipToggler />
</NavBarGroup>
Expand All @@ -70,7 +62,7 @@ const NavBar = () => {
)}
{showOmnichannel && (
<>
<NavBarGroup role='toolbar' ref={omnichannelToolbarRef} {...omnichannelToolbarProps}>
<NavBarGroup aria-label={t('Omnichannel')}>
{showOmnichannelQueueLink && <NavBarItemOmnichannelQueue title={t('Queue')} />}
{isCallReady && <NavBarItemOmniChannelCallDialPad />}
<NavBarItemOmnichannelContact title={t('Contact_Center')} />
Expand All @@ -80,7 +72,7 @@ const NavBar = () => {
<NavBarDivider />
</>
)}
<NavBarGroup aria-label={t('Workspace_and_user_settings')}>
<NavBarGroup aria-label={t('Workspace_and_user_preferences')}>
<NavBarItemAdministrationMenu />
{user ? <UserMenu user={user} /> : <NavBarItemLoginPage />}
</NavBarGroup>
Expand Down
25 changes: 25 additions & 0 deletions apps/meteor/client/NavBarV2/NavBarNavigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { NavBarGroup, NavBarSection, NavBarItem } from '@rocket.chat/fuselage';
import { useRouter } from '@rocket.chat/ui-contexts';
import { FocusScope } from 'react-aria';
import { useTranslation } from 'react-i18next';

import NavBarSearch from './NavBarSearch';

const NavbarNavigation = () => {
const { t } = useTranslation();
const { navigate } = useRouter();

return (
<NavBarSection>
<FocusScope>
<NavBarSearch />
</FocusScope>
<NavBarGroup aria-label={t('History_navigation')}>
<NavBarItem title={t('Back_in_history')} onClick={() => navigate(-1)} icon='chevron-right' small />
<NavBarItem title={t('Forward_in_history')} onClick={() => navigate(1)} icon='chevron-left' small />
</NavBarGroup>
</NavBarSection>
);
};

export default NavbarNavigation;
176 changes: 176 additions & 0 deletions apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { Box, Icon, TextInput, Tile } from '@rocket.chat/fuselage';
import { useDebouncedValue, useEffectEvent, useMergedRefs, useOutsideClick } from '@rocket.chat/fuselage-hooks';
import type { KeyboardEvent } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { useFocusManager, useOverlayTrigger } from 'react-aria';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useOverlayTriggerState } from 'react-stately';
import tinykeys from 'tinykeys';

import NavBarSearchNoResults from './NavBarSearchNoResults';
import NavBarSearchRow from './NavBarSearchRow';
import { getShortcut } from './getShortcut';
import { useSearchItems } from './hooks/useSearchItems';
import { CustomScrollbars } from '../../components/CustomScrollbars';

const isOption = (node: Element) => node.getAttribute('role') === 'option';

const NavBarSearch = () => {
const { t } = useTranslation();
const focusManager = useFocusManager();
const shortcut = getShortcut();

const placeholder = [t('Search_rooms'), shortcut].filter(Boolean).join(' ');

const {
formState: { isDirty },
register,
watch,
resetField,
setFocus,
} = useForm({ defaultValues: { filterText: '' } });
const { ref: filterRef, ...rest } = register('filterText');
const debouncedFilter = useDebouncedValue(watch('filterText'), 200);

const { filterText } = watch();

const { data: items = [], isLoading } = useSearchItems(debouncedFilter);

const triggerRef = useRef(null);

const state = useOverlayTriggerState({});
const { triggerProps, overlayProps } = useOverlayTrigger({ type: 'listbox' }, state, triggerRef);
delete triggerProps.onPress;

const handleEscSearch = useCallback(() => {
resetField('filterText');
state.close();
}, [resetField, state]);

const handleClearText = useEffectEvent(() => {
resetField('filterText');
setFocus('filterText');
});

const handleSelect = useEffectEvent(() => {
state.close();
resetField('filterText');
});

const containerRef = useRef<HTMLElement>(null);
const mergedRefs = useMergedRefs(filterRef, triggerRef);

const handleKeyDown = (e: KeyboardEvent) => {
if (e.code === 'Tab') {
state.close();
}

if (e.code === 'ArrowUp' || e.code === 'ArrowDown') {
e.preventDefault();

if (e.code === 'ArrowUp') {
return focusManager?.focusPrevious({
wrap: true,
accept: (node) => isOption(node),
});
}

if (e.code === 'ArrowDown') {
focusManager?.focusNext({
wrap: true,
accept: (node) => isOption(node),
});
}
}
};

useOutsideClick([containerRef], state.close);

useEffect(() => {
const unsubscribe = tinykeys(window, {
'$mod+K': (event) => {
event.preventDefault();
setFocus('filterText');
},
'$mod+P': (event) => {
event.preventDefault();
setFocus('filterText');
},
'Escape': (event) => {
event.preventDefault();
handleEscSearch();
},
});

return (): void => {
unsubscribe();
};
}, [focusManager, handleEscSearch, setFocus]);

return (
<Box role='search' mie={8} width='x622' position='relative'>
<TextInput
{...rest}
{...triggerProps}
onFocus={() => state.setOpen(true)}
onKeyDown={(e) => {
state.setOpen(true);

if ((e.code === 'Tab' && e.shiftKey) || e.key === 'Escape') {
state.close();
}

if (e.code === 'ArrowUp' || e.code === 'ArrowDown') {
e.preventDefault();

focusManager?.focusNext({
accept: (node) => isOption(node),
});
}
}}
autoComplete='off'
placeholder={placeholder}
ref={mergedRefs}
role='combobox'
small
addon={<Icon name={isDirty ? 'cross' : 'magnifier'} size='x20' onClick={handleClearText} />}
/>
{state.isOpen && (
<Tile
ref={containerRef}
position='absolute'
zIndex={99}
padding={0}
pb={16}
minHeight='x52'
maxHeight='50vh'
display='flex'
width='100%'
flexDirection='column'
aria-live='polite'
aria-atomic='true'
aria-busy={isLoading}
>
<CustomScrollbars>
<div {...overlayProps} role='listbox' aria-label={t('Channels')} tabIndex={-1} onKeyDown={handleKeyDown}>
{items.length === 0 && !isLoading && <NavBarSearchNoResults />}
{items.length > 0 && (
<Box color='title-labels' fontScale='c1' fontWeight='bold' pi={12} mbe={4}>
{filterText ? t('Results') : t('Recent')}
</Box>
)}
{items.map((item) => (
<div key={item._id}>
<NavBarSearchRow room={item} onClick={handleSelect} />
</div>
))}
</div>
</CustomScrollbars>
</Tile>
)}
</Box>
);
};

export default NavBarSearch;
28 changes: 28 additions & 0 deletions apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { SidebarV2Item, SidebarV2ItemAvatarWrapper, SidebarV2ItemTitle } from '@rocket.chat/fuselage';
import type { HTMLAttributes, ReactElement, ReactNode } from 'react';

type NavBarSearchItemProps = {
title: string;
avatar: ReactElement;
icon: ReactNode;
actions?: ReactElement;
href?: string;
unread?: boolean;
selected?: boolean;
badges?: ReactElement;
clickable?: boolean;
} & Omit<HTMLAttributes<HTMLAnchorElement>, 'is'>;

const NavBarSearchItem = ({ icon, title, avatar, actions, unread, badges, ...props }: NavBarSearchItemProps) => {
return (
<SidebarV2Item role='option' {...props}>
{avatar && <SidebarV2ItemAvatarWrapper>{avatar}</SidebarV2ItemAvatarWrapper>}
{icon && icon}
<SidebarV2ItemTitle unread={unread}>{title}</SidebarV2ItemTitle>
{badges && badges}
{actions && actions}
</SidebarV2Item>
);
};

export default NavBarSearchItem;
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { SidebarV2ItemBadge, SidebarV2ItemIcon } from '@rocket.chat/fuselage';
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
import type { ComponentProps, ReactElement } from 'react';
import { useTranslation } from 'react-i18next';

import NavBarSearchItem from './NavBarSearchItem';
import { RoomIcon } from '../../components/RoomIcon';
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
import { OmnichannelBadges } from '../../sidebarv2/badges/OmnichannelBadges';
import { useUnreadDisplay } from '../../sidebarv2/hooks/useUnreadDisplay';

type NavBarSearchItemWithDataProps = {
room: SubscriptionWithRoom;
id: string;
AvatarTemplate: ReactElement;
} & Partial<ComponentProps<typeof NavBarSearchItem>>;

const NavBarSearchItemWithData = ({ room, AvatarTemplate, ...props }: NavBarSearchItemWithDataProps) => {
const { t } = useTranslation();

const href = roomCoordinator.getRouteLink(room.t, room) || '';
const title = roomCoordinator.getRoomName(room.t, room) || '';

const { unreadTitle, unreadVariant, showUnread, unreadCount, highlightUnread: highlighted } = useUnreadDisplay(room);

const icon = <SidebarV2ItemIcon highlighted={highlighted} icon={<RoomIcon room={room} placement='sidebar' size='x20' />} />;

const badges = (
<>
{showUnread && (
<SidebarV2ItemBadge
variant={unreadVariant}
title={unreadTitle}
role='status'
aria-label={t('__unreadTitle__from__roomTitle__', { unreadTitle, roomTitle: title })}
>
<span aria-hidden>{unreadCount.total}</span>
</SidebarV2ItemBadge>
)}
{isOmnichannelRoom(room) && <OmnichannelBadges room={room} />}
</>
);

return (
<NavBarSearchItem
{...props}
unread={highlighted}
href={href}
aria-label={showUnread ? t('__unreadTitle__from__roomTitle__', { unreadTitle, roomTitle: title }) : title}
title={title}
icon={icon}
badges={badges}
avatar={AvatarTemplate}
/>
);
};

export default NavBarSearchItemWithData;
10 changes: 10 additions & 0 deletions apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchNoResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useTranslation } from 'react-i18next';

import GenericNoResults from '../../components/GenericNoResults';

const NavBarSearchNoResults = () => {
const { t } = useTranslation();
return <GenericNoResults description={t('Try_entering_a_different_search_term')} />;
};

export default NavBarSearchNoResults;
Loading
Loading