Skip to content

Commit 2cd881b

Browse files
committed
feat(Layout): auto height
1 parent 8e7ffc5 commit 2cd881b

File tree

2 files changed

+127
-7
lines changed

2 files changed

+127
-7
lines changed

src/components/content/Layout/Layout.stories.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,3 +545,54 @@ export const CompleteApplicationShell: Story = {
545545
);
546546
},
547547
};
548+
549+
/**
550+
* Layout automatically applies height: 100% when it detects it has collapsed
551+
* to 0 height. This happens when the Layout is placed in a container without
552+
* a defined height and no explicit height prop is provided.
553+
*
554+
* This story demonstrates the auto-height behavior where the Layout is inside
555+
* a container that has height defined, so it stretches to fill it.
556+
*/
557+
export const AutoHeight: Story = {
558+
render: () => (
559+
<Block height="300px" border="1bw dashed #dark-04" padding="1x">
560+
<Text preset="t4" color="#dark-03">
561+
Parent container with height: 300px
562+
</Text>
563+
<Layout fill="#light">
564+
<Layout.Header title="Auto-Height Layout" />
565+
<Layout.Content padding="2x">
566+
<Text>
567+
This Layout has no explicit height prop but stretches to fill the
568+
parent container because the parent has a defined height.
569+
</Text>
570+
</Layout.Content>
571+
</Layout>
572+
</Block>
573+
),
574+
};
575+
576+
/**
577+
* When a Layout collapses to 0 height (no parent height, no height prop),
578+
* and the auto-height fallback (100%) still results in 0 height,
579+
* the Layout shows a development warning and sets a minimum height.
580+
*
581+
* This warning is only visible in development mode or when `_forceShowDevWarning`
582+
* is enabled (used here for storybook which runs in production mode).
583+
*/
584+
export const CollapsedWithWarning: Story = {
585+
render: () => (
586+
<Block border="1bw dashed #dark-04" padding="1x">
587+
<Text preset="t4" color="#dark-03">
588+
Parent container with no height defined
589+
</Text>
590+
<Layout _forceShowDevWarning fill="#light">
591+
<Layout.Header title="Collapsed Layout" />
592+
<Layout.Content padding="2x">
593+
<Text>This content is hidden when collapsed</Text>
594+
</Layout.Content>
595+
</Layout>
596+
</Block>
597+
),
598+
};

src/components/content/Layout/Layout.tsx

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
ReactNode,
88
useEffect,
99
useMemo,
10+
useRef,
11+
useState,
1012
} from 'react';
1113

1214
import {
@@ -24,6 +26,8 @@ import {
2426
Styles,
2527
tasty,
2628
} from '../../../tasty';
29+
import { isDevEnv } from '../../../tasty/utils/isDevEnv';
30+
import { Alert } from '../Alert';
2731

2832
import { LayoutProvider, useLayoutContext } from './LayoutContext';
2933

@@ -42,6 +46,11 @@ const LayoutElement = tasty({
4246
overflow: 'hidden',
4347
flexGrow: 1,
4448
placeSelf: 'stretch',
49+
height: {
50+
'': 'auto',
51+
'auto-height': 'fixed 100%',
52+
collapsed: '6x',
53+
},
4554

4655
'$content-padding': '1x',
4756

@@ -82,6 +91,10 @@ export interface CubeLayoutProps
8291
/** Styles for wrapper and Inner sub-element */
8392
styles?: Styles;
8493
children?: ReactNode;
94+
/**
95+
* @internal Force show dev warning even in production (for storybook testing)
96+
*/
97+
_forceShowDevWarning?: boolean;
8598
}
8699

87100
// Check if a child is a Layout.Panel
@@ -97,6 +110,9 @@ function LayoutInner(
97110
props: CubeLayoutProps & { forwardedRef?: ForwardedRef<HTMLDivElement> },
98111
) {
99112
const layoutContext = useLayoutContext();
113+
const localRef = useRef<HTMLDivElement>(null);
114+
const [isAutoHeight, setIsAutoHeight] = useState(false);
115+
const [isCollapsed, setIsCollapsed] = useState(false);
100116

101117
const {
102118
isGrid,
@@ -108,6 +124,7 @@ function LayoutInner(
108124
children,
109125
style,
110126
forwardedRef,
127+
_forceShowDevWarning,
111128
...otherProps
112129
} = props;
113130

@@ -183,6 +200,29 @@ function LayoutInner(
183200
return () => cancelAnimationFrame(frameId);
184201
}, [markReady]);
185202

203+
// Auto-height detection: if layout collapses to 0 height after initialization,
204+
// automatically set height to 100% to prevent invisible layout
205+
useEffect(() => {
206+
if (isReady && localRef.current && localRef.current.offsetHeight === 0) {
207+
setIsAutoHeight(true);
208+
}
209+
}, [isReady]);
210+
211+
// Second check: if still 0 height after auto-height was applied,
212+
// show collapsed state with warning
213+
useEffect(() => {
214+
if (isAutoHeight && localRef.current) {
215+
// Use requestAnimationFrame to check after styles have been applied
216+
const frameId = requestAnimationFrame(() => {
217+
if (localRef.current && localRef.current.offsetHeight === 0) {
218+
setIsCollapsed(true);
219+
}
220+
});
221+
222+
return () => cancelAnimationFrame(frameId);
223+
}
224+
}, [isAutoHeight]);
225+
186226
const insetStyle = useMemo(() => {
187227
const baseStyle: Record<string, string> = {
188228
'--inset-top': `${panelSizes.top}px`,
@@ -199,22 +239,51 @@ function LayoutInner(
199239
}, [panelSizes, style]);
200240

201241
const mods = useMemo(
202-
() => ({ dragging: isDragging, 'not-ready': !isReady }),
203-
[isDragging, isReady],
242+
() => ({
243+
dragging: isDragging,
244+
'not-ready': !isReady,
245+
'auto-height': isAutoHeight && !isCollapsed,
246+
collapsed: isCollapsed,
247+
}),
248+
[isDragging, isReady, isAutoHeight, isCollapsed],
204249
);
205250

251+
// Combine local ref with forwarded ref
252+
const setRefs = (element: HTMLDivElement | null) => {
253+
localRef.current = element;
254+
255+
if (typeof forwardedRef === 'function') {
256+
forwardedRef(element);
257+
} else if (forwardedRef) {
258+
forwardedRef.current = element;
259+
}
260+
};
261+
262+
// Show dev warning when collapsed and in dev mode (or forced for stories)
263+
const showDevWarning = isCollapsed && (_forceShowDevWarning || isDevEnv());
264+
206265
return (
207266
<LayoutElement
208-
ref={forwardedRef}
267+
ref={setRefs}
209268
{...filterBaseProps(otherProps, { eventProps: true })}
210269
mods={mods}
211270
styles={finalStyles}
212271
style={insetStyle}
213272
>
214-
{/* Panels are rendered outside the Inner element */}
215-
{panels}
216-
{/* Other content goes inside the Inner element */}
217-
<div data-element="Inner">{content}</div>
273+
{showDevWarning ? (
274+
<Alert theme="danger">
275+
<b>UIKit:</b> <b>&lt;Layout/&gt;</b> has collapsed to <b>0</b> height.
276+
Ensure the parent container has a defined height or use the{' '}
277+
<b>height</b> prop on <b>&lt;Layout/&gt;</b>.
278+
</Alert>
279+
) : (
280+
<>
281+
{/* Panels are rendered outside the Inner element */}
282+
{panels}
283+
{/* Other content goes inside the Inner element */}
284+
<div data-element="Inner">{content}</div>
285+
</>
286+
)}
218287
</LayoutElement>
219288
);
220289
}

0 commit comments

Comments
 (0)