Skip to content

Commit

Permalink
feat(layout): added support for mini layouts
Browse files Browse the repository at this point in the history
  • Loading branch information
mlaursen committed Feb 28, 2021
1 parent 172ee40 commit 36b3cbc
Show file tree
Hide file tree
Showing 12 changed files with 666 additions and 123 deletions.
128 changes: 26 additions & 102 deletions packages/layout/src/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { ReactNode, ReactElement } from "react";
import { SkipToMainContent, SkipToMainContentProps } from "@react-md/link";
import { BaseTreeItem } from "@react-md/tree";
import { PropsWithRef } from "@react-md/utils";

Expand All @@ -9,9 +8,9 @@ import {
DEFAULT_PHONE_LAYOUT,
DEFAULT_TABLET_LAYOUT,
} from "./constants";
import { LayoutAppBar, LayoutAppBarProps } from "./LayoutAppBar";
import { LayoutMain, LayoutMainProps } from "./LayoutMain";
import { LayoutNavigation, LayoutNavigationProps } from "./LayoutNavigation";
import { LayoutChildren, LayoutChildrenProps } from "./LayoutChildren";
import { LayoutAppBarProps } from "./LayoutAppBar";
import { LayoutNavigationProps } from "./LayoutNavigation";
import { LayoutWithNavToggle } from "./LayoutNavToggle";
import { LayoutProvider } from "./LayoutProvider";
import { LayoutTreeProps } from "./LayoutTree";
Expand Down Expand Up @@ -105,6 +104,26 @@ export interface FlattenedLayoutComponentConfiguration<
*/
nav?: ReactNode;

/**
* A custom implementation for the main mini navigation component within the
* `Layout`. If this is not `undefined`, it will be used instead of the
* default implementation.
*
* Using this prop will make the following props do nothing for the mini nav:
*
* - `navProps`
* - `navHeader`
* - `navHeaderProps`
* - `navHeaderTitle`
* - `navHeaderTitleProps`
* - `closeNav`
* - `closeNavProps`
* - `treeProps`
*
* @remarks \@since.2.7.0
*/
miniNav?: ReactNode;

/**
* Any additional props to provide to the default `LayoutNavigation`.
*/
Expand Down Expand Up @@ -205,44 +224,7 @@ export interface FlattenedLayoutComponentConfiguration<

export interface LayoutProps<T extends BaseTreeItem = LayoutNavigationItem>
extends LayoutConfiguration,
FlattenedLayoutComponentConfiguration<T> {
/**
* The base id to use for everything within the layout component. The `id`
* will be applied to:
*
* - the `LayoutAppBar` as `${id}-header`
* - the `AppBarTitle` as `${id}-title`
* - the `LayoutNavToggle` as `${id}-nav-toggle`
* - the `LayoutMain` element as `${id}-main`
*/
id?: string;

/**
* The children to display within the layout. This is pretty much required
* since you'll have an empty app otherwise, but it's left as optional just
* for prototyping purposes.
*/
children?: ReactNode;

/**
* Any additional props to provide to the `<SkipToMainContent />` link that is
* automatically rendered in the layout.
*/
skipProps?: Omit<SkipToMainContentProps, "mainId">;

/**
* Any optional props to provide to the `<main>` element of the page.
*/
mainProps?: PropsWithRef<LayoutMainProps, HTMLDivElement>;

/**
* Boolean if the main app bar should appear after the navigation component.
* It is generally recommended to enable this prop if the navigation component
* as a focusable element in the header since it will have a better tab focus
* order.
*/
navAfterAppBar?: boolean;
}
LayoutChildrenProps<T> {}

/**
* The layout to use for your app. There are 9 different types of layouts
Expand All @@ -256,66 +238,14 @@ export interface LayoutProps<T extends BaseTreeItem = LayoutNavigationItem>
*/
export function Layout({
id = "layout",
appBar: propAppBar,
appBarProps,
navAfterAppBar = false,
children,
skipProps,
mainProps,
phoneLayout = DEFAULT_PHONE_LAYOUT,
tabletLayout = DEFAULT_TABLET_LAYOUT,
landscapeTabletLayout = DEFAULT_LANDSCAPE_TABLET_LAYOUT,
desktopLayout = DEFAULT_DESKTOP_LAYOUT,
largeDesktopLayout,
defaultToggleableVisible = false,
customTitle,
title,
titleProps,
navToggle,
navToggleProps,
nav: propNav,
navProps,
navHeader,
navHeaderProps,
navHeaderTitle,
navHeaderTitleProps,
closeNav,
closeNavProps,
treeProps,
...props
}: LayoutProps): ReactElement {
const fixedAppBar = appBarProps?.fixed ?? typeof propAppBar === "undefined";
const mainId = mainProps?.id || `${id}-main`;

let appBar = propAppBar;
if (typeof appBar === "undefined") {
appBar = (
<LayoutAppBar
{...appBarProps}
customTitle={customTitle}
title={title}
titleProps={titleProps}
navToggle={navToggle}
navToggleProps={navToggleProps}
/>
);
}

let nav = propNav;
if (typeof nav === "undefined") {
nav = (
<LayoutNavigation
header={navHeader}
headerProps={navHeaderProps}
headerTitle={navHeaderTitle}
headerTitleProps={navHeaderTitleProps}
closeNav={closeNav}
closeNavProps={closeNavProps}
treeProps={treeProps}
{...navProps}
/>
);
}

return (
<LayoutProvider
baseId={id}
Expand All @@ -326,13 +256,7 @@ export function Layout({
largeDesktopLayout={largeDesktopLayout}
defaultToggleableVisible={defaultToggleableVisible}
>
<SkipToMainContent {...skipProps} mainId={mainId} />
{navAfterAppBar && appBar}
{nav}
{!navAfterAppBar && appBar}
<LayoutMain headerOffset={fixedAppBar} {...mainProps} id={mainId}>
{children}
</LayoutMain>
<LayoutChildren id={id} {...props} />
</LayoutProvider>
);
}
Expand Down
199 changes: 199 additions & 0 deletions packages/layout/src/LayoutChildren.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import React, { ReactElement, ReactNode, useEffect, useState } from "react";
import { SkipToMainContent, SkipToMainContentProps } from "@react-md/link";
import { BaseTreeItem, TreeData } from "@react-md/tree";
import { PropsWithRef } from "@react-md/utils";

import { FlattenedLayoutComponentConfiguration } from "./Layout";
import { LayoutAppBar } from "./LayoutAppBar";
import { LayoutMain, LayoutMainProps } from "./LayoutMain";
import { LayoutNavigation } from "./LayoutNavigation";
import { useLayoutConfig } from "./LayoutProvider";
import { LayoutNavigationItem } from "./types";
import { isMiniLayout } from "./utils";

/**
* This used to just be the `LayoutProps` but was split up to help with mini
* layouts.
*
* @remarks \@since 2.7.0
*/
export interface LayoutChildrenProps<
T extends BaseTreeItem = LayoutNavigationItem
> extends FlattenedLayoutComponentConfiguration<T> {
/**
* The base id to use for everything within the layout component. The `id`
* will be applied to:
*
* - the `LayoutAppBar` as `${id}-header`
* - the `AppBarTitle` as `${id}-title`
* - the `LayoutNavToggle` as `${id}-nav-toggle`
* - the `LayoutMain` element as `${id}-main`
*/
id?: string;

/**
* Boolean if the main app bar should appear after the navigation component.
* It is generally recommended to enable this prop if the navigation component
* as a focusable element in the header since it will have a better tab focus
* order.
*/
navAfterAppBar?: boolean;

/**
* Any optional props to provide to the `<main>` element of the page.
*/
mainProps?: PropsWithRef<LayoutMainProps, HTMLDivElement>;

/**
* Any additional props to provide to the `<SkipToMainContent />` link that is
* automatically rendered in the layout.
*/
skipProps?: Omit<SkipToMainContentProps, "mainId">;

/**
* An optional tree to use for the mini navigation pane since the default
* behavior of rendering mini tree items might hide content in an
* undersireable way.
*
* @remarks \@since 2.7.0
* @see {@link defaultMiniNavigationItemRenderer} for more information
*/
miniNavItems?: TreeData<T>;

/**
* The children to display within the layout. This is pretty much required
* since you'll have an empty app otherwise, but it's left as optional just
* for prototyping purposes.
*/
children?: ReactNode;
}

/**
* The only purpose of this component is to render the children and different
* parts of the `Layout` depending on the current layout that is active. Since
* the `Layout` component defines the provider itself, this has to be a child
* component to get the resolved `layout` type.
*
* @remarks \@since 2.7.0
* @internal
*/
export function LayoutChildren({
id = "layout",
appBar: propAppBar,
appBarProps,
customTitle,
title,
titleProps,
navToggle,
navToggleProps,
navAfterAppBar = false,
nav: propNav,
miniNav: propMiniNav,
miniNavItems,
navHeader,
navHeaderProps,
navHeaderTitle,
navHeaderTitleProps,
closeNav,
closeNavProps,
treeProps,
navProps,
skipProps,
mainProps,
children,
}: LayoutChildrenProps): ReactElement {
const mainId = mainProps?.id || `${id}-main`;
const fixedAppBar = appBarProps?.fixed ?? typeof propAppBar === "undefined";
const { layout, visible } = useLayoutConfig();
const mini = isMiniLayout(layout);
const [miniHidden, setMiniHidden] = useState(visible);
// when the layout changes, the hidden state for the mini drawer must also be
// updated
useEffect(() => {
setMiniHidden(visible);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [layout]);

let appBar = propAppBar;
if (typeof appBar === "undefined") {
appBar = (
<LayoutAppBar
{...appBarProps}
customTitle={customTitle}
title={title}
titleProps={titleProps}
navToggle={navToggle}
navToggleProps={navToggleProps}
/>
);
}

let nav = propNav;
if (typeof nav === "undefined") {
nav = (
<LayoutNavigation
header={navHeader}
headerProps={navHeaderProps}
headerTitle={navHeaderTitle}
headerTitleProps={navHeaderTitleProps}
closeNav={closeNav}
closeNavProps={closeNavProps}
treeProps={treeProps}
{...navProps}
onEntered={(node, isAppearing) => {
navProps?.onEntered?.(node, isAppearing);
setMiniHidden(true);
}}
onExit={(node) => {
navProps?.onExit?.(node);
setMiniHidden(false);
}}
/>
);
}

let miniNav = propMiniNav;
if (mini && treeProps && typeof miniNav === "undefined") {
let miniTreeProps = treeProps;
if (miniNavItems) {
miniTreeProps = {
...miniTreeProps,
navItems: miniNavItems,
};
}

miniNav = (
<LayoutNavigation
header={navHeader}
headerProps={navHeaderProps}
headerTitle={navHeaderTitle}
headerTitleProps={navHeaderTitleProps}
closeNav={closeNav}
closeNavProps={closeNavProps}
treeProps={miniTreeProps}
{...navProps}
mini
hidden={miniHidden}
/>
);
}

return (
<>
<SkipToMainContent {...skipProps} mainId={mainId} />
{navAfterAppBar && appBar}
{nav}
{!navAfterAppBar && appBar}
{/* mini nav should always be in tab index after app bar */}
{miniNav}
<LayoutMain
headerOffset={fixedAppBar}
{...mainProps}
id={mainId}
mini={mini}
>
{children}
</LayoutMain>
</>
);
}

0 comments on commit 36b3cbc

Please sign in to comment.