Skip to content

Commit

Permalink
feat(AppFrame): mobile view and MobileNavigation component (#1319)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcinsawicki authored Sep 11, 2024
1 parent 8492c97 commit 81ce453
Show file tree
Hide file tree
Showing 25 changed files with 625 additions and 205 deletions.
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

0 comments on commit 81ce453

Please sign in to comment.