-
Notifications
You must be signed in to change notification settings - Fork 144
/
layout.tsx
206 lines (187 loc) · 7.22 KB
/
layout.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useContext, useLayoutEffect } from 'react';
import clsx from 'clsx';
import { AppLayoutContext } from './context';
import { SplitPanelContext } from '../../internal/context/split-panel-context';
import styles from './styles.css.js';
import testutilStyles from '../test-classes/styles.css.js';
import { AppLayoutProps } from '../interfaces';
import customCssProps from '../../internal/generated/custom-css-properties';
interface LayoutProps {
children: React.ReactNode;
}
/**
* The layoutElement ref will be used by the resize observers to calculate the offset from
* the top and bottom of the viewport based on the header and footer elements. This is to
* ensure the Layout component minimum height will fill 100% of the viewport less those
* cumulative heights.
*/
export default function Layout({ children }: LayoutProps) {
const {
contentHeader,
contentType,
disableBodyScroll,
disableContentHeaderOverlap,
dynamicOverlapHeight,
footerHeight,
hasNotificationsContent,
headerHeight,
isNavigationOpen,
isSplitPanelOpen,
isToolsOpen,
layoutElement,
layoutWidth,
mainOffsetLeft,
maxContentWidth,
minContentWidth,
navigationHide,
notificationsHeight,
setOffsetBottom,
splitPanel,
stickyNotifications,
toolsHide,
} = useContext(AppLayoutContext);
const {
getHeader: getSplitPanelHeader,
position: splitPanelPosition,
size: splitPanelSize,
} = useContext(SplitPanelContext);
const isOverlapDisabled = getOverlapDisabled(dynamicOverlapHeight, contentHeader, disableContentHeaderOverlap);
// Content gaps on the left and right are used with the minmax function in the CSS grid column definition
const hasContentGapLeft = getContentGapLeft(isNavigationOpen, navigationHide);
const hasContentGapRight = getContentGapRight(
splitPanelPosition,
isSplitPanelOpen,
isToolsOpen,
splitPanel,
toolsHide
);
/**
* Determine the offsetBottom value based on the presence of a footer element and
* the SplitPanel component. Ignore the SplitPanel if it is not in the bottom
* position. Use the size property if it is open and the header height if it is closed.
*/
useLayoutEffect(
function handleOffsetBottom() {
let offsetBottom = footerHeight;
if (splitPanel && splitPanelPosition === 'bottom') {
if (isSplitPanelOpen) {
offsetBottom += splitPanelSize;
} else {
const splitPanelHeader = getSplitPanelHeader();
offsetBottom += splitPanelHeader ? splitPanelHeader.clientHeight : 0;
}
}
setOffsetBottom(offsetBottom);
},
[
footerHeight,
getSplitPanelHeader,
isSplitPanelOpen,
setOffsetBottom,
splitPanelPosition,
splitPanel,
splitPanelSize,
]
);
return (
<main
className={clsx(
styles.layout,
styles[`content-type-${contentType}`],
styles[`split-panel-position-${splitPanelPosition ?? 'bottom'}`],
{
[styles['disable-body-scroll']]: disableBodyScroll,
[testutilStyles['disable-body-scroll-root']]: disableBodyScroll,
[styles['has-content-gap-left']]: hasContentGapLeft,
[styles['has-content-gap-right']]: hasContentGapRight,
[styles['has-max-content-width']]: maxContentWidth && maxContentWidth > 0,
[styles['has-split-panel']]: splitPanel,
[styles['has-sticky-notifications']]: stickyNotifications && hasNotificationsContent,
[styles['is-overlap-disabled']]: isOverlapDisabled,
},
testutilStyles.root
)}
ref={layoutElement}
style={{
[customCssProps.headerHeight]: `${headerHeight}px`,
[customCssProps.footerHeight]: `${footerHeight}px`,
[customCssProps.layoutWidth]: `${layoutWidth}px`,
[customCssProps.mainOffsetLeft]: `${mainOffsetLeft}px`,
...(maxContentWidth && { [customCssProps.maxContentWidth]: `${maxContentWidth}px` }),
...(minContentWidth && { [customCssProps.minContentWidth]: `${minContentWidth}px` }),
[customCssProps.notificationsHeight]: `${notificationsHeight}px`,
...(!isOverlapDisabled &&
dynamicOverlapHeight > 0 && { [customCssProps.overlapHeight]: `${dynamicOverlapHeight}px` }),
}}
>
{children}
</main>
);
}
/**
* When the Navigation and Tools are present the grid definition has the center column
* touch the first and last columns with no gap. The forms with the circular buttons
* for Navigation and Tools have internal padding which creates the necessary
* horizontal space when the drawers are closed. The remaining conditions below
* determine the necessity of utilizing the content gap left property to create
* horizontal space between the center column and its adjacent siblings.
*/
function getContentGapRight(
splitPanelPosition: AppLayoutProps.SplitPanelPosition,
isSplitPanelOpen?: boolean,
isToolsOpen?: boolean,
splitPanel?: React.ReactNode,
toolsHide?: boolean
) {
let hasContentGapRight = false;
// Main is touching the edge of the Layout and needs a content gap
if (!splitPanel && toolsHide) {
hasContentGapRight = true;
}
// Main is touching the Tools drawer and needs a content gap
if ((!splitPanel || !isSplitPanelOpen) && !toolsHide && isToolsOpen) {
hasContentGapRight = true;
}
// Main is touching the edge of the Layout and needs a content gap
if (splitPanel && splitPanelPosition === 'bottom' && (isToolsOpen || toolsHide)) {
hasContentGapRight = true;
}
// Main is touching the Split Panel drawer and needs a content gap
if (splitPanel && isSplitPanelOpen && splitPanelPosition === 'side') {
hasContentGapRight = true;
}
return hasContentGapRight;
}
/**
* Additional function to determine whether or not a content gap is needed
* on the left (see the getContentGapRight function). The same render logic applies
* regarding the center column touching an adjacent sibling but the only
* component state that needs to be tracked is the Navigation.
*/
function getContentGapLeft(isNavigationOpen: boolean, navigationHide?: boolean) {
return isNavigationOpen || navigationHide ? true : false;
}
/**
* Determine whether the overlap between the contentHeader and content slots should be disabled.
* The disableContentHeaderOverlap property is absolute and will always disable the overlap
* if it is set to true. If there is no contentHeader then the overlap should be disabled
* unless there is a dynamicOverlapHeight. The dynamicOverlapHeight property is set by a
* component in the content slot that needs to manually control the overlap height. Components
* such as the Table (full page variant), Wizard, ContentLayout use this property and will
* retain the overlap even if there is nothing rendered in the contentHeader slot.
*/
function getOverlapDisabled(
dynamicOverlapHeight: number,
contentHeader?: React.ReactNode,
disableContentHeaderOverlap?: boolean
) {
let isOverlapDisabled = false;
if (disableContentHeaderOverlap) {
isOverlapDisabled = true;
} else if (!contentHeader && dynamicOverlapHeight <= 0) {
isOverlapDisabled = true;
}
return isOverlapDisabled;
}