|
| 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