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(AppFrame): mobile view and MobileNavigation component (#1319) #1331

Merged
merged 1 commit into from
Sep 11, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const Accordion: React.FC<IAccordionProps> = ({
},
className
);
const { size, handleResize } = useHeightResizer();
const { size, handleResizeRef } = useHeightResizer();

const handleExpandChange = (isExpanded: boolean) => {
if (isExpanded) {
Expand Down Expand Up @@ -102,7 +102,7 @@ export const Accordion: React.FC<IAccordionProps> = ({
style={{ maxHeight: isExpanded ? size : 0 }}
ref={contentRef}
>
<div ref={handleResize}>
<div ref={handleResizeRef}>
{isMounted && (
<Text
as="div"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ export const AccordionMultilineElement: React.FC<
isVisible: !isExpanded,
elementRef: multilineRef,
});
const { size, handleResize } = useHeightResizer();
const { size, handleResizeRef } = useHeightResizer();

return (
<div
className={styles[`${baseClass}`]}
style={{ maxHeight: isVisible ? size : 0 }}
ref={multilineRef}
>
<div ref={handleResize}>
<div ref={handleResizeRef}>
{isMounted && (
<div className={styles[`${baseClass}__inner`]}>{children}</div>
)}
Expand Down
43 changes: 42 additions & 1 deletion packages/react-components/src/components/AppFrame/AppFrame.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The most important functionality of this component, apart from the UI layout, is

## How to build the navigation <a id="BuildNav" />

[Main navigation](#MainNav) | [Side navigation](#SideNav) | [AppFrameProvider](#Provider) | [Expiration Counter](#ExpirationCounter)
[Main navigation](#MainNav) | [Side navigation](#SideNav) | [MobileNavigation](#MobileNav) | [AppFrameProvider](#Provider) | [Expiration Counter](#ExpirationCounter)

The component allows you to place navigation available on the left side, consisting of icons, and also allows you to display additional navigation in the opening panel.

Expand Down Expand Up @@ -137,6 +137,47 @@ import { Home, Settings } from '@livechat/design-system-icons';
}
```

### Mobile navigation <a id="MobileNav" />

To build the mobile navigation, use the available components:
- `MobileNavigation`
- `NavigationItem`

#### `MobileNavigation` component

The `MobileNavigation` component serves as the main wrapper for navigation items passed as children.

#### `NavigationItem` component

The `NavigationItem` is the same component used in the `Navigation`. You must remember to set the `isMobile` prop to `true` to display items in the mobile mode (label is visible in the button instead of tooltip).

#### Mobile navigation implementation example

```jsx
import { AppFrame, Navigation, NavigationItem, MobileNavigation } from '@livechat/design-system-react-components';
import { Home, Settings } from '@livechat/design-system-icons';

<AppFrame
navigation={<Navigation>...</Navigation>}
mobileNavigation={
<MobileNavigation>
...
<NavigationItem
isMobile
key="home"
id="home"
label="Home"
icon={<Icon source={Home} />}
onClick={(e, id) => {}}
isActive={activeItem === 'home'}
badge={5}
url="#"
/>
...
</MobileNavigation>
}
```

#### Side navigation visibility with `AppFrameProvider` <a id="Provider" />

Side navigation passed by the `sideNavigation` prop will be displayed in an appropriate container, located next to the application frame. By default, the navigation will be visible, but the component allows you to control its visibility.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ $base-class: 'app-frame';
width: 100%;
height: 100%;

&--mobile {
padding: 0;
}

&__top-bar {
display: flex;
flex-shrink: 0;
Expand All @@ -39,6 +43,10 @@ $base-class: 'app-frame';
height: 100%;
overflow: hidden;

&--mobile {
flex-direction: column;
}

&__nav-bar-wrapper {
flex-shrink: 0;
transition: all var(--transition-duration-fast-2) ease-in-out;
Expand All @@ -60,6 +68,17 @@ $base-class: 'app-frame';
height: 100%;
overflow: hidden;
color: var(--content-default);

&--mobile {
border-radius: 0;
}
}

&__mobile-top-bar {
position: absolute;
top: 0;
right: 0;
left: 0;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { render, vi } from 'test-utils';

import { AppFrame } from './AppFrame';
import {
MobileNavigation,
Navigation,
NavigationItem,
SideNavigation,
Expand All @@ -25,11 +26,20 @@ const defaultProps = {
id="item-1"
label="Item 1"
icon={<div>Icon</div>}
url="#"
onClick={vi.fn()}
/>
</Navigation>
),
mobileNavigation: (
<MobileNavigation data-testid="mobile-navigation">
<NavigationItem
id="item-1"
label="Item 1"
icon={<div>Icon</div>}
onClick={vi.fn()}
/>
</MobileNavigation>
),
};

const renderComponent = (props: IAppFrameProps) => {
Expand All @@ -46,10 +56,11 @@ describe('<AppFrame> component', () => {
expect(container.firstChild).toHaveClass('custom-class');
});

it('should render with given navigation', () => {
const { getByTestId } = renderComponent(defaultProps);
it('should render navigation and not render mobile navigation', () => {
const { getByTestId, queryByTestId } = renderComponent(defaultProps);

expect(getByTestId('navigation')).toBeInTheDocument();
expect(queryByTestId('mobile-navigation')).not.toBeInTheDocument();
});

it('should render with top bar if provided', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,50 @@
font-size: 14px;
font-weight: 600;
}

.app-container {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
overflow: auto;
}

.app-content-1 {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
justify-content: center;
width: 100%;
}

.page-title {
text-align: center;
}

.switchers {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
max-width: 270px;
}

.switch {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;

p {
margin: 0;
}
}

.app-content-2 {
padding: 16px;
width: 100%;
max-width: 800px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
NavigationTopBarAlert,
NavigationTopBarTitle,
ExpirationCounter,
MobileNavigation,
} from './components';
import {
ExampleAppContent,
Expand Down Expand Up @@ -207,6 +208,24 @@ export const Default = (): React.ReactElement => {
</NavigationGroup>
</Navigation>
}
mobileNavigation={
<MobileNavigation>
{navigationItems.slice(0, 5).map((item, index) => (
<NavigationItem
key={item}
id={item}
label={item.charAt(0).toUpperCase() + item.slice(1)}
icon={<Icon source={navigationItemsIcons[index]} />}
onClick={(e, id) => {
e.preventDefault();
setActiveItem(id);
}}
isActive={activeItem === item}
badge={getBadgeContent(item)}
/>
))}
</MobileNavigation>
}
sideNavigation={getSubNav()}
topBar={
topBarVisible || visibleAlerts.some((alert) => alert) ? (
Expand Down
80 changes: 59 additions & 21 deletions packages/react-components/src/components/AppFrame/AppFrame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';

import cx from 'clsx';

import { useAnimations } from '../../hooks';
import { useAnimations, useMobileViewDetector } from '../../hooks';
import { AppFrameProvider, useAppFrame } from '../../providers';

import { IAppFrameProps } from './types';
Expand All @@ -17,43 +17,62 @@ const Frame = (props: IAppFrameProps) => {
children,
className,
navigation,
mobileNavigation,
sideNavigation,
topBar,
topBarClassName,
sideNavigationContainerClassName,
contentClassName,
mobileViewBreakpoint = 705,
} = props;
const mergedClassNames = cx(styles[baseClass], className);
const { isSideNavigationVisible } = useAppFrame();
const {
isSideNavigationVisible,
setIsMobileViewEnabled,
isMobileViewEnabled,
} = useAppFrame();
const sideNavWrapperRef = React.useRef<HTMLDivElement>(null);
const { isOpen, isMounted } = useAnimations({
isVisible: isSideNavigationVisible,
elementRef: sideNavWrapperRef,
});
const { isMobile, handleResizeRef } = useMobileViewDetector({
mobileBreakpoint: mobileViewBreakpoint,
});

React.useEffect(() => {
setIsMobileViewEnabled(isMobile);
}, [isMobile]);

return (
<div className={mergedClassNames}>
{navigation}
<div className={styles[pageContainerClass]}>
<div
className={cx(
styles[`${pageContainerClass}__top-bar`],
{
[styles[`${pageContainerClass}__top-bar--visible`]]: topBar,
},
'lc-dark-theme',
topBarClassName
)}
>
{topBar}
</div>
<div className={mergedClassNames} ref={handleResizeRef}>
{!isMobileViewEnabled && navigation}
<div
className={cx(styles[pageContainerClass], {
[styles[`${pageContainerClass}--mobile`]]: isMobileViewEnabled,
})}
>
{!isMobileViewEnabled && (
<div
className={cx(
styles[`${pageContainerClass}__top-bar`],
{
[styles[`${pageContainerClass}__top-bar--visible`]]: topBar,
},
'lc-dark-theme',
topBarClassName
)}
>
{topBar}
</div>
)}
<div
className={cx(styles[`${pageContainerClass}__content-wrapper`], {
[styles[`${pageContainerClass}__content-wrapper--with-top-bar`]]:
topBar,
[styles[`${pageContainerClass}__content-wrapper--mobile`]]:
isMobileViewEnabled,
})}
>
{sideNavigation && (
{!isMobileViewEnabled && sideNavigation && (
<div
ref={sideNavWrapperRef}
className={cx(
Expand All @@ -75,11 +94,30 @@ const Frame = (props: IAppFrameProps) => {
<div
className={cx(
styles[`${pageContainerClass}__content-wrapper__content`],
contentClassName
contentClassName,
{
[styles[
`${pageContainerClass}__content-wrapper__content--mobile`
]]: isMobileViewEnabled,
}
)}
>
{children}
</div>
{isMobileViewEnabled && (
<>
<div
className={
styles[
`${pageContainerClass}__content-wrapper__mobile-top-bar`
]
}
>
{topBar}
</div>
<div>{mobileNavigation}</div>
</>
)}
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
$base-class: 'mobile-navigation';

.#{$base-class} {
display: flex;
flex-direction: row;
flex-shrink: 0;
align-items: center;
justify-content: space-around;
z-index: 1;
background-color: var(--navbar-background);
padding: 6px;
}
Loading
Loading