Skip to content

Commit 36b3cbc

Browse files
committed
feat(layout): added support for mini layouts
1 parent 172ee40 commit 36b3cbc

12 files changed

Lines changed: 666 additions & 123 deletions

File tree

packages/layout/src/Layout.tsx

Lines changed: 26 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React, { ReactNode, ReactElement } from "react";
2-
import { SkipToMainContent, SkipToMainContentProps } from "@react-md/link";
32
import { BaseTreeItem } from "@react-md/tree";
43
import { PropsWithRef } from "@react-md/utils";
54

@@ -9,9 +8,9 @@ import {
98
DEFAULT_PHONE_LAYOUT,
109
DEFAULT_TABLET_LAYOUT,
1110
} from "./constants";
12-
import { LayoutAppBar, LayoutAppBarProps } from "./LayoutAppBar";
13-
import { LayoutMain, LayoutMainProps } from "./LayoutMain";
14-
import { LayoutNavigation, LayoutNavigationProps } from "./LayoutNavigation";
11+
import { LayoutChildren, LayoutChildrenProps } from "./LayoutChildren";
12+
import { LayoutAppBarProps } from "./LayoutAppBar";
13+
import { LayoutNavigationProps } from "./LayoutNavigation";
1514
import { LayoutWithNavToggle } from "./LayoutNavToggle";
1615
import { LayoutProvider } from "./LayoutProvider";
1716
import { LayoutTreeProps } from "./LayoutTree";
@@ -105,6 +104,26 @@ export interface FlattenedLayoutComponentConfiguration<
105104
*/
106105
nav?: ReactNode;
107106

107+
/**
108+
* A custom implementation for the main mini navigation component within the
109+
* `Layout`. If this is not `undefined`, it will be used instead of the
110+
* default implementation.
111+
*
112+
* Using this prop will make the following props do nothing for the mini nav:
113+
*
114+
* - `navProps`
115+
* - `navHeader`
116+
* - `navHeaderProps`
117+
* - `navHeaderTitle`
118+
* - `navHeaderTitleProps`
119+
* - `closeNav`
120+
* - `closeNavProps`
121+
* - `treeProps`
122+
*
123+
* @remarks \@since.2.7.0
124+
*/
125+
miniNav?: ReactNode;
126+
108127
/**
109128
* Any additional props to provide to the default `LayoutNavigation`.
110129
*/
@@ -205,44 +224,7 @@ export interface FlattenedLayoutComponentConfiguration<
205224

206225
export interface LayoutProps<T extends BaseTreeItem = LayoutNavigationItem>
207226
extends LayoutConfiguration,
208-
FlattenedLayoutComponentConfiguration<T> {
209-
/**
210-
* The base id to use for everything within the layout component. The `id`
211-
* will be applied to:
212-
*
213-
* - the `LayoutAppBar` as `${id}-header`
214-
* - the `AppBarTitle` as `${id}-title`
215-
* - the `LayoutNavToggle` as `${id}-nav-toggle`
216-
* - the `LayoutMain` element as `${id}-main`
217-
*/
218-
id?: string;
219-
220-
/**
221-
* The children to display within the layout. This is pretty much required
222-
* since you'll have an empty app otherwise, but it's left as optional just
223-
* for prototyping purposes.
224-
*/
225-
children?: ReactNode;
226-
227-
/**
228-
* Any additional props to provide to the `<SkipToMainContent />` link that is
229-
* automatically rendered in the layout.
230-
*/
231-
skipProps?: Omit<SkipToMainContentProps, "mainId">;
232-
233-
/**
234-
* Any optional props to provide to the `<main>` element of the page.
235-
*/
236-
mainProps?: PropsWithRef<LayoutMainProps, HTMLDivElement>;
237-
238-
/**
239-
* Boolean if the main app bar should appear after the navigation component.
240-
* It is generally recommended to enable this prop if the navigation component
241-
* as a focusable element in the header since it will have a better tab focus
242-
* order.
243-
*/
244-
navAfterAppBar?: boolean;
245-
}
227+
LayoutChildrenProps<T> {}
246228

247229
/**
248230
* The layout to use for your app. There are 9 different types of layouts
@@ -256,66 +238,14 @@ export interface LayoutProps<T extends BaseTreeItem = LayoutNavigationItem>
256238
*/
257239
export function Layout({
258240
id = "layout",
259-
appBar: propAppBar,
260-
appBarProps,
261-
navAfterAppBar = false,
262-
children,
263-
skipProps,
264-
mainProps,
265241
phoneLayout = DEFAULT_PHONE_LAYOUT,
266242
tabletLayout = DEFAULT_TABLET_LAYOUT,
267243
landscapeTabletLayout = DEFAULT_LANDSCAPE_TABLET_LAYOUT,
268244
desktopLayout = DEFAULT_DESKTOP_LAYOUT,
269245
largeDesktopLayout,
270246
defaultToggleableVisible = false,
271-
customTitle,
272-
title,
273-
titleProps,
274-
navToggle,
275-
navToggleProps,
276-
nav: propNav,
277-
navProps,
278-
navHeader,
279-
navHeaderProps,
280-
navHeaderTitle,
281-
navHeaderTitleProps,
282-
closeNav,
283-
closeNavProps,
284-
treeProps,
247+
...props
285248
}: LayoutProps): ReactElement {
286-
const fixedAppBar = appBarProps?.fixed ?? typeof propAppBar === "undefined";
287-
const mainId = mainProps?.id || `${id}-main`;
288-
289-
let appBar = propAppBar;
290-
if (typeof appBar === "undefined") {
291-
appBar = (
292-
<LayoutAppBar
293-
{...appBarProps}
294-
customTitle={customTitle}
295-
title={title}
296-
titleProps={titleProps}
297-
navToggle={navToggle}
298-
navToggleProps={navToggleProps}
299-
/>
300-
);
301-
}
302-
303-
let nav = propNav;
304-
if (typeof nav === "undefined") {
305-
nav = (
306-
<LayoutNavigation
307-
header={navHeader}
308-
headerProps={navHeaderProps}
309-
headerTitle={navHeaderTitle}
310-
headerTitleProps={navHeaderTitleProps}
311-
closeNav={closeNav}
312-
closeNavProps={closeNavProps}
313-
treeProps={treeProps}
314-
{...navProps}
315-
/>
316-
);
317-
}
318-
319249
return (
320250
<LayoutProvider
321251
baseId={id}
@@ -326,13 +256,7 @@ export function Layout({
326256
largeDesktopLayout={largeDesktopLayout}
327257
defaultToggleableVisible={defaultToggleableVisible}
328258
>
329-
<SkipToMainContent {...skipProps} mainId={mainId} />
330-
{navAfterAppBar && appBar}
331-
{nav}
332-
{!navAfterAppBar && appBar}
333-
<LayoutMain headerOffset={fixedAppBar} {...mainProps} id={mainId}>
334-
{children}
335-
</LayoutMain>
259+
<LayoutChildren id={id} {...props} />
336260
</LayoutProvider>
337261
);
338262
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import React, { ReactElement, ReactNode, useEffect, useState } from "react";
2+
import { SkipToMainContent, SkipToMainContentProps } from "@react-md/link";
3+
import { BaseTreeItem, TreeData } from "@react-md/tree";
4+
import { PropsWithRef } from "@react-md/utils";
5+
6+
import { FlattenedLayoutComponentConfiguration } from "./Layout";
7+
import { LayoutAppBar } from "./LayoutAppBar";
8+
import { LayoutMain, LayoutMainProps } from "./LayoutMain";
9+
import { LayoutNavigation } from "./LayoutNavigation";
10+
import { useLayoutConfig } from "./LayoutProvider";
11+
import { LayoutNavigationItem } from "./types";
12+
import { isMiniLayout } from "./utils";
13+
14+
/**
15+
* This used to just be the `LayoutProps` but was split up to help with mini
16+
* layouts.
17+
*
18+
* @remarks \@since 2.7.0
19+
*/
20+
export interface LayoutChildrenProps<
21+
T extends BaseTreeItem = LayoutNavigationItem
22+
> extends FlattenedLayoutComponentConfiguration<T> {
23+
/**
24+
* The base id to use for everything within the layout component. The `id`
25+
* will be applied to:
26+
*
27+
* - the `LayoutAppBar` as `${id}-header`
28+
* - the `AppBarTitle` as `${id}-title`
29+
* - the `LayoutNavToggle` as `${id}-nav-toggle`
30+
* - the `LayoutMain` element as `${id}-main`
31+
*/
32+
id?: string;
33+
34+
/**
35+
* Boolean if the main app bar should appear after the navigation component.
36+
* It is generally recommended to enable this prop if the navigation component
37+
* as a focusable element in the header since it will have a better tab focus
38+
* order.
39+
*/
40+
navAfterAppBar?: boolean;
41+
42+
/**
43+
* Any optional props to provide to the `<main>` element of the page.
44+
*/
45+
mainProps?: PropsWithRef<LayoutMainProps, HTMLDivElement>;
46+
47+
/**
48+
* Any additional props to provide to the `<SkipToMainContent />` link that is
49+
* automatically rendered in the layout.
50+
*/
51+
skipProps?: Omit<SkipToMainContentProps, "mainId">;
52+
53+
/**
54+
* An optional tree to use for the mini navigation pane since the default
55+
* behavior of rendering mini tree items might hide content in an
56+
* undersireable way.
57+
*
58+
* @remarks \@since 2.7.0
59+
* @see {@link defaultMiniNavigationItemRenderer} for more information
60+
*/
61+
miniNavItems?: TreeData<T>;
62+
63+
/**
64+
* The children to display within the layout. This is pretty much required
65+
* since you'll have an empty app otherwise, but it's left as optional just
66+
* for prototyping purposes.
67+
*/
68+
children?: ReactNode;
69+
}
70+
71+
/**
72+
* The only purpose of this component is to render the children and different
73+
* parts of the `Layout` depending on the current layout that is active. Since
74+
* the `Layout` component defines the provider itself, this has to be a child
75+
* component to get the resolved `layout` type.
76+
*
77+
* @remarks \@since 2.7.0
78+
* @internal
79+
*/
80+
export function LayoutChildren({
81+
id = "layout",
82+
appBar: propAppBar,
83+
appBarProps,
84+
customTitle,
85+
title,
86+
titleProps,
87+
navToggle,
88+
navToggleProps,
89+
navAfterAppBar = false,
90+
nav: propNav,
91+
miniNav: propMiniNav,
92+
miniNavItems,
93+
navHeader,
94+
navHeaderProps,
95+
navHeaderTitle,
96+
navHeaderTitleProps,
97+
closeNav,
98+
closeNavProps,
99+
treeProps,
100+
navProps,
101+
skipProps,
102+
mainProps,
103+
children,
104+
}: LayoutChildrenProps): ReactElement {
105+
const mainId = mainProps?.id || `${id}-main`;
106+
const fixedAppBar = appBarProps?.fixed ?? typeof propAppBar === "undefined";
107+
const { layout, visible } = useLayoutConfig();
108+
const mini = isMiniLayout(layout);
109+
const [miniHidden, setMiniHidden] = useState(visible);
110+
// when the layout changes, the hidden state for the mini drawer must also be
111+
// updated
112+
useEffect(() => {
113+
setMiniHidden(visible);
114+
// eslint-disable-next-line react-hooks/exhaustive-deps
115+
}, [layout]);
116+
117+
let appBar = propAppBar;
118+
if (typeof appBar === "undefined") {
119+
appBar = (
120+
<LayoutAppBar
121+
{...appBarProps}
122+
customTitle={customTitle}
123+
title={title}
124+
titleProps={titleProps}
125+
navToggle={navToggle}
126+
navToggleProps={navToggleProps}
127+
/>
128+
);
129+
}
130+
131+
let nav = propNav;
132+
if (typeof nav === "undefined") {
133+
nav = (
134+
<LayoutNavigation
135+
header={navHeader}
136+
headerProps={navHeaderProps}
137+
headerTitle={navHeaderTitle}
138+
headerTitleProps={navHeaderTitleProps}
139+
closeNav={closeNav}
140+
closeNavProps={closeNavProps}
141+
treeProps={treeProps}
142+
{...navProps}
143+
onEntered={(node, isAppearing) => {
144+
navProps?.onEntered?.(node, isAppearing);
145+
setMiniHidden(true);
146+
}}
147+
onExit={(node) => {
148+
navProps?.onExit?.(node);
149+
setMiniHidden(false);
150+
}}
151+
/>
152+
);
153+
}
154+
155+
let miniNav = propMiniNav;
156+
if (mini && treeProps && typeof miniNav === "undefined") {
157+
let miniTreeProps = treeProps;
158+
if (miniNavItems) {
159+
miniTreeProps = {
160+
...miniTreeProps,
161+
navItems: miniNavItems,
162+
};
163+
}
164+
165+
miniNav = (
166+
<LayoutNavigation
167+
header={navHeader}
168+
headerProps={navHeaderProps}
169+
headerTitle={navHeaderTitle}
170+
headerTitleProps={navHeaderTitleProps}
171+
closeNav={closeNav}
172+
closeNavProps={closeNavProps}
173+
treeProps={miniTreeProps}
174+
{...navProps}
175+
mini
176+
hidden={miniHidden}
177+
/>
178+
);
179+
}
180+
181+
return (
182+
<>
183+
<SkipToMainContent {...skipProps} mainId={mainId} />
184+
{navAfterAppBar && appBar}
185+
{nav}
186+
{!navAfterAppBar && appBar}
187+
{/* mini nav should always be in tab index after app bar */}
188+
{miniNav}
189+
<LayoutMain
190+
headerOffset={fixedAppBar}
191+
{...mainProps}
192+
id={mainId}
193+
mini={mini}
194+
>
195+
{children}
196+
</LayoutMain>
197+
</>
198+
);
199+
}

0 commit comments

Comments
 (0)