feat: add FillSpace viewport-height spacer with repeating measureElement
Adds a reusable FillSpace component to dashgui that fills the remaining viewport height between an aboveRef and optional belowRef, with a scrollable middle section. Supports an optional measureElement that is repeated top-to-bottom to fill the leftover space below children (rows are rendered first, units fill height - contentHeight). Includes mock page, docs wiring, and screenshot references.
This commit is contained in:
@@ -16,6 +16,7 @@ This package is source-only: consumers import directly from `src/` via the
|
|||||||
| `dashgui/tiles` | `src/tiles/` | `Tile`, `TILE_SIZE`, `tileBaseSx` |
|
| `dashgui/tiles` | `src/tiles/` | `Tile`, `TILE_SIZE`, `tileBaseSx` |
|
||||||
| `dashgui/ElevatedPill` | `src/ElevatedPill.jsx` | `ElevatedPill`, `ToggleElevatedPill`, `ActionElevatedPill` |
|
| `dashgui/ElevatedPill` | `src/ElevatedPill.jsx` | `ElevatedPill`, `ToggleElevatedPill`, `ActionElevatedPill` |
|
||||||
| `dashgui/SingleLineTextEdit` | `src/SingleLineTextEdit.jsx` | Centralized single-line text field |
|
| `dashgui/SingleLineTextEdit` | `src/SingleLineTextEdit.jsx` | Centralized single-line text field |
|
||||||
|
| `dashgui/FillSpace` | `src/FillSpace.jsx` | `FillSpace` — viewport-height spacer between header/footer refs; optional repeating `measureElement` |
|
||||||
|
|
||||||
## Design rules
|
## Design rules
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"./tiles": "./src/tiles/index.js",
|
"./tiles": "./src/tiles/index.js",
|
||||||
"./ElevatedPill": "./src/ElevatedPill.jsx",
|
"./ElevatedPill": "./src/ElevatedPill.jsx",
|
||||||
"./SingleLineTextEdit": "./src/SingleLineTextEdit.jsx",
|
"./SingleLineTextEdit": "./src/SingleLineTextEdit.jsx",
|
||||||
|
"./FillSpace": "./src/FillSpace.jsx",
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
268
src/FillSpace.jsx
Normal file
268
src/FillSpace.jsx
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
import { Component } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import { alpha } from '@mui/material/styles';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vertical spacer that fills the remaining viewport height between the content
|
||||||
|
* above it and (optionally) a footer below it — useful for creating scrollable
|
||||||
|
* middle sections in apps with fixed headers/footers.
|
||||||
|
*
|
||||||
|
* @component FillSpace
|
||||||
|
* @import dashgui/FillSpace
|
||||||
|
* @defaultExport
|
||||||
|
*
|
||||||
|
* @prop {React.Ref} [aboveRef] - Ref to the element immediately above the spacer. It is observed (not strictly required) so the spacer re-measures when that element's height changes.
|
||||||
|
* @prop {React.Ref} [belowRef] - Ref to the footer element below. Its height (plus top margin) is reserved so the footer stays visible without page scrolling.
|
||||||
|
* @prop {ReactNode} [measureElement] - When given, this element is used as the measurement unit of the spacer: its height is read from a hidden copy and it is repeated top-to-bottom to fill only the leftover space below `children` (like a repeating ruler/row pattern). When omitted, only `children` are rendered.
|
||||||
|
* @prop {number} [measureGap=0] - Vertical gap (px) between repeated `measureElement` units.
|
||||||
|
* @prop {number} [minHeight=0] - Minimum spacer height in px.
|
||||||
|
* @prop {number} [maxHeight] - Optional maximum spacer height in px.
|
||||||
|
* @prop {boolean} [debug=false] - When true, draws a dashed outline around the spacer and a translucent overlay over the whole viewport so the measured bounds are visible.
|
||||||
|
* @prop {ReactNode} [children] - Real content rendered at the top of the spacer. The repeated `measureElement` units (if any) fill the remaining space below it.
|
||||||
|
* @prop {object} [sx] - Extra styles merged into the Box.
|
||||||
|
*
|
||||||
|
* @behaviour Fills to the bottom of the initial viewport: height = viewport height − spacer's distance from the document top − reserved below height.
|
||||||
|
* @behaviour The distance-from-top is read from the spacer's own position (document-relative), so every element above (including their margins and the parent's padding) is accounted for, and scrolling the page does not change the computed height.
|
||||||
|
* @behaviour When `measureElement` is set, `children` are rendered on top and the units fill only `height − contentHeight`, so no units are shown once the content already fills the space.
|
||||||
|
* @behaviour Re-measures on window resize and whenever the observed refs or the content change size (ResizeObserver), throttled with requestAnimationFrame.
|
||||||
|
* @behaviour `debug` draws a dashed primary outline around the spacer plus a translucent full-viewport overlay.
|
||||||
|
*/
|
||||||
|
class FillSpace extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { height: 0, unitHeight: 0, contentHeight: 0 };
|
||||||
|
this.ref = null;
|
||||||
|
this.contentRef = null;
|
||||||
|
this.unitRef = null;
|
||||||
|
this.observer = null;
|
||||||
|
this.raf = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this._measure();
|
||||||
|
this._observe();
|
||||||
|
});
|
||||||
|
window.addEventListener('resize', this._onViewportChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (
|
||||||
|
prevProps.aboveRef !== this.props.aboveRef ||
|
||||||
|
prevProps.belowRef !== this.props.belowRef ||
|
||||||
|
prevProps.minHeight !== this.props.minHeight ||
|
||||||
|
prevProps.maxHeight !== this.props.maxHeight ||
|
||||||
|
prevProps.measureElement !== this.props.measureElement ||
|
||||||
|
prevProps.measureGap !== this.props.measureGap
|
||||||
|
) {
|
||||||
|
this._observe();
|
||||||
|
this._measure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('resize', this._onViewportChange);
|
||||||
|
if (this.raf) {
|
||||||
|
cancelAnimationFrame(this.raf);
|
||||||
|
this.raf = null;
|
||||||
|
}
|
||||||
|
if (this.observer) {
|
||||||
|
this.observer.disconnect();
|
||||||
|
this.observer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onViewportChange = () => {
|
||||||
|
if (this.raf) return;
|
||||||
|
this.raf = requestAnimationFrame(() => {
|
||||||
|
this.raf = null;
|
||||||
|
this._measure();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
_observe() {
|
||||||
|
if (this.observer) {
|
||||||
|
this.observer.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.observer = new ResizeObserver(() => this._measure());
|
||||||
|
|
||||||
|
// Only observe the referenced elements + the rendered content — never the
|
||||||
|
// spacer's own parent, since resizing the spacer would resize the parent
|
||||||
|
// and create a loop.
|
||||||
|
const { aboveRef, belowRef } = this.props;
|
||||||
|
const targets = [aboveRef?.current, belowRef?.current, this.contentRef];
|
||||||
|
for (const el of targets) {
|
||||||
|
if (el && el.offsetParent !== null) {
|
||||||
|
this.observer.observe(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_measure() {
|
||||||
|
if (!this.ref) return;
|
||||||
|
|
||||||
|
const { belowRef, measureElement, minHeight = 0, maxHeight } = this.props;
|
||||||
|
|
||||||
|
// Distance from the document top to the spacer top — includes every
|
||||||
|
// element above the spacer plus their margins and the parent's padding.
|
||||||
|
// Document-relative (rect.top + scrollY), so scrolling the page does not
|
||||||
|
// change the computed height (fixes "grows with scrolling").
|
||||||
|
const rect = this.ref.getBoundingClientRect();
|
||||||
|
const abovePx = rect.top + window.scrollY;
|
||||||
|
|
||||||
|
// Height to reserve for the footer below (its box + top margin).
|
||||||
|
const belowEl = belowRef?.current;
|
||||||
|
let belowPx = 0;
|
||||||
|
if (belowEl && belowEl.offsetParent !== null) {
|
||||||
|
belowPx = belowEl.offsetHeight;
|
||||||
|
const marginTop = parseFloat(window.getComputedStyle(belowEl).marginTop);
|
||||||
|
belowPx += Number.isFinite(marginTop) ? marginTop : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const available = Math.max(0, window.innerHeight - abovePx - belowPx);
|
||||||
|
const clamped = Math.min(
|
||||||
|
Math.max(available, minHeight),
|
||||||
|
maxHeight ?? Infinity,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Height of the real rendered children (the top part of the spacer).
|
||||||
|
let contentHeight = 0;
|
||||||
|
if (this.contentRef) {
|
||||||
|
contentHeight = this.contentRef.offsetHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure the height of one measurement unit from the hidden copy so the
|
||||||
|
// repeats match the real rendered size.
|
||||||
|
let unitHeight = this.state.unitHeight;
|
||||||
|
if (measureElement != null && this.unitRef) {
|
||||||
|
const h = this.unitRef.offsetHeight;
|
||||||
|
if (h > 0) unitHeight = h;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Math.abs(clamped - this.state.height) > 0.5 ||
|
||||||
|
Math.abs(unitHeight - this.state.unitHeight) > 0.5 ||
|
||||||
|
Math.abs(contentHeight - this.state.contentHeight) > 0.5
|
||||||
|
) {
|
||||||
|
this.setState({ height: clamped, unitHeight, contentHeight });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
minHeight = 0,
|
||||||
|
maxHeight,
|
||||||
|
debug = false,
|
||||||
|
children,
|
||||||
|
measureElement,
|
||||||
|
measureGap = 0,
|
||||||
|
sx,
|
||||||
|
...rest
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const { height, unitHeight, contentHeight } = this.state;
|
||||||
|
|
||||||
|
// Repeated measurement units fill only the leftover space below the real
|
||||||
|
// children. Count starts at 0: if the content already fills the space,
|
||||||
|
// no units are rendered.
|
||||||
|
let fill = null;
|
||||||
|
if (measureElement != null) {
|
||||||
|
const unit = unitHeight > 0 ? unitHeight : 0;
|
||||||
|
const fillHeight = Math.max(0, height - contentHeight);
|
||||||
|
const count =
|
||||||
|
unit > 0
|
||||||
|
? Math.floor((fillHeight + measureGap) / (unit + measureGap))
|
||||||
|
: 0;
|
||||||
|
const items = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
items.push(
|
||||||
|
<Box key={i} sx={{ width: '100%' }}>
|
||||||
|
{measureElement}
|
||||||
|
</Box>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fill = (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: measureGap,
|
||||||
|
width: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{items}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
ref={(el) => {
|
||||||
|
this.ref = el;
|
||||||
|
}}
|
||||||
|
{...rest}
|
||||||
|
sx={[
|
||||||
|
{
|
||||||
|
position: 'relative',
|
||||||
|
height,
|
||||||
|
minHeight,
|
||||||
|
...(maxHeight != null ? { maxHeight } : {}),
|
||||||
|
},
|
||||||
|
debug
|
||||||
|
? (theme) => ({
|
||||||
|
outline: `2px dashed ${theme.palette.primary.main}`,
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
|
sx,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{children != null ? (
|
||||||
|
<Box
|
||||||
|
ref={(el) => {
|
||||||
|
this.contentRef = el;
|
||||||
|
}}
|
||||||
|
sx={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
{fill}
|
||||||
|
{measureElement != null ? (
|
||||||
|
<Box
|
||||||
|
ref={(el) => {
|
||||||
|
this.unitRef = el;
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
width: '100%',
|
||||||
|
visibility: 'hidden',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{measureElement}
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
{debug ? (
|
||||||
|
<Box
|
||||||
|
sx={(theme) => ({
|
||||||
|
position: 'fixed',
|
||||||
|
inset: 0,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
background: alpha(theme.palette.primary.main, 0.08),
|
||||||
|
zIndex: 9999,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FillSpace;
|
||||||
@@ -8,6 +8,7 @@ import TilesMockPage from './mock-pages/TilesMockPage';
|
|||||||
import PillsMockPage from './mock-pages/PillsMockPage';
|
import PillsMockPage from './mock-pages/PillsMockPage';
|
||||||
import TextFieldsMockPage from './mock-pages/TextFieldsMockPage';
|
import TextFieldsMockPage from './mock-pages/TextFieldsMockPage';
|
||||||
import ListsMockPage from './mock-pages/ListsMockPage';
|
import ListsMockPage from './mock-pages/ListsMockPage';
|
||||||
|
import FillSpaceMockPage from './mock-pages/FillSpaceMockPage';
|
||||||
|
|
||||||
const PAGES = [
|
const PAGES = [
|
||||||
{ id: 'panels', label: 'Panels', node: <PanelsMockPage /> },
|
{ id: 'panels', label: 'Panels', node: <PanelsMockPage /> },
|
||||||
@@ -15,6 +16,7 @@ const PAGES = [
|
|||||||
{ id: 'tiles', label: 'Tiles', node: <TilesMockPage /> },
|
{ id: 'tiles', label: 'Tiles', node: <TilesMockPage /> },
|
||||||
{ id: 'pills', label: 'Pills', node: <PillsMockPage /> },
|
{ id: 'pills', label: 'Pills', node: <PillsMockPage /> },
|
||||||
{ id: 'textfields', label: 'Text fields', node: <TextFieldsMockPage /> },
|
{ id: 'textfields', label: 'Text fields', node: <TextFieldsMockPage /> },
|
||||||
|
{ id: 'fillspace', label: 'FillSpace', node: <FillSpaceMockPage /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
171
src/dev/mock-pages/FillSpaceMockPage.jsx
Normal file
171
src/dev/mock-pages/FillSpaceMockPage.jsx
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import { Component, createRef } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import FillSpace from '../../FillSpace';
|
||||||
|
import { ComponentDocs } from '../ComponentDocs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock page exercising FillSpace: real content on top, and a repeating
|
||||||
|
* `measureElement` filling only the leftover space down to the end of the
|
||||||
|
* viewport.
|
||||||
|
*/
|
||||||
|
class FillSpaceMockPage extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.ex1 = { header: createRef(), footer: createRef() };
|
||||||
|
this.ex2 = { header: createRef(), footer: createRef() };
|
||||||
|
this.ex3 = { header: createRef(), footer: createRef() };
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
FillSpace renders real content on top and repeats the measurement
|
||||||
|
element until the end of the page.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Main example: content + repeating fill to end of page */}
|
||||||
|
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||||
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
|
Content + repeating element to end of page
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Box
|
||||||
|
ref={this.ex1.header}
|
||||||
|
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
|
||||||
|
>
|
||||||
|
<Typography variant="caption">Header (measured)</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<FillSpace
|
||||||
|
aboveRef={this.ex1.header}
|
||||||
|
belowRef={this.ex1.footer}
|
||||||
|
measureElement={
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: '#dbeafe',
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 1,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2">Einheit</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: '#fef3c7',
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2" fontWeight={600}>
|
||||||
|
Echte Inhalte (children)
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Diese Zeilen stehen oben, darunter wiederholt sich die
|
||||||
|
Einheit bis zum Seitenende.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</FillSpace>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
ref={this.ex1.footer}
|
||||||
|
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
|
||||||
|
>
|
||||||
|
<Typography variant="caption">Footer (measured)</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Example 2: With minHeight */}
|
||||||
|
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||||
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
|
With minHeight constraint
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Box
|
||||||
|
ref={this.ex2.header}
|
||||||
|
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
|
||||||
|
>
|
||||||
|
<Typography variant="caption">Header</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<FillSpace
|
||||||
|
aboveRef={this.ex2.header}
|
||||||
|
belowRef={this.ex2.footer}
|
||||||
|
minHeight={200}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: '#fef3c7',
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2">
|
||||||
|
Minimum height of 200px enforced.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</FillSpace>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
ref={this.ex2.footer}
|
||||||
|
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
|
||||||
|
>
|
||||||
|
<Typography variant="caption">Footer</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* Example 3: Debug mode */}
|
||||||
|
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||||
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
|
Debug mode overlay
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Box
|
||||||
|
ref={this.ex3.header}
|
||||||
|
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
|
||||||
|
>
|
||||||
|
<Typography variant="caption">Header</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<FillSpace
|
||||||
|
aboveRef={this.ex3.header}
|
||||||
|
belowRef={this.ex3.footer}
|
||||||
|
debug
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: '#dcfce7',
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2">
|
||||||
|
Green background shows actual spacer bounds; purple overlay
|
||||||
|
shows full viewport coverage.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</FillSpace>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
ref={this.ex3.footer}
|
||||||
|
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
|
||||||
|
>
|
||||||
|
<Typography variant="caption">Footer</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<ComponentDocs name="FillSpace" />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FillSpaceMockPage;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from 'react';
|
import { Component, createRef } from 'react';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
@@ -15,6 +15,7 @@ import ElevatedPill, {
|
|||||||
ToggleElevatedPill,
|
ToggleElevatedPill,
|
||||||
} from '../../ElevatedPill';
|
} from '../../ElevatedPill';
|
||||||
import SingleLineTextEdit from '../../SingleLineTextEdit';
|
import SingleLineTextEdit from '../../SingleLineTextEdit';
|
||||||
|
import FillSpace from '../../FillSpace';
|
||||||
|
|
||||||
const MOCK_ROWS = [
|
const MOCK_ROWS = [
|
||||||
{
|
{
|
||||||
@@ -292,6 +293,128 @@ class ScreenshotHarness extends Component {
|
|||||||
color="#e2e8f0"
|
color="#e2e8f0"
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
<Section id="fillspace" label="FillSpace">
|
||||||
|
<FillSpaceExample />
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section id="fillspace-measure" label="FillSpace – repeat measure element">
|
||||||
|
<FillSpaceMeasureExample />
|
||||||
|
</Section>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deterministic FillSpace example for the screenshot harness. Bound with a
|
||||||
|
* `maxHeight` so the reference is stable regardless of the viewport height.
|
||||||
|
*/
|
||||||
|
class FillSpaceExample extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.headerRef = createRef();
|
||||||
|
this.footerRef = createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Box
|
||||||
|
ref={this.headerRef}
|
||||||
|
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
|
||||||
|
>
|
||||||
|
<Typography variant="caption">Header (gemessen)</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<FillSpace
|
||||||
|
aboveRef={this.headerRef}
|
||||||
|
belowRef={this.footerRef}
|
||||||
|
minHeight={80}
|
||||||
|
maxHeight={220}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: '#dbeafe',
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 1,
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2">
|
||||||
|
FillSpace füllt den Platz zwischen Header und Footer.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</FillSpace>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
ref={this.footerRef}
|
||||||
|
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
|
||||||
|
>
|
||||||
|
<Typography variant="caption">Footer (gemessen)</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FillSpace with a repeating measurement element plus real children on top,
|
||||||
|
* for the screenshot harness. Bound with maxHeight so the reference is stable.
|
||||||
|
*/
|
||||||
|
class FillSpaceMeasureExample extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.headerRef = createRef();
|
||||||
|
this.footerRef = createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Box
|
||||||
|
ref={this.headerRef}
|
||||||
|
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
|
||||||
|
>
|
||||||
|
<Typography variant="caption">Header</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<FillSpace
|
||||||
|
aboveRef={this.headerRef}
|
||||||
|
belowRef={this.footerRef}
|
||||||
|
measureElement={
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: '#c7d2fe',
|
||||||
|
p: 1,
|
||||||
|
borderRadius: 1,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="caption">Einheit</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
measureGap={8}
|
||||||
|
minHeight={60}
|
||||||
|
maxHeight={260}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: '#fef3c7',
|
||||||
|
p: 1,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2">Echte Inhalte</Typography>
|
||||||
|
</Box>
|
||||||
|
</FillSpace>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
ref={this.footerRef}
|
||||||
|
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
|
||||||
|
>
|
||||||
|
<Typography variant="caption">Footer</Typography>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ export * from './lists';
|
|||||||
export * from './tiles';
|
export * from './tiles';
|
||||||
export { default as ElevatedPill } from './ElevatedPill';
|
export { default as ElevatedPill } from './ElevatedPill';
|
||||||
export { default as SingleLineTextEdit } from './SingleLineTextEdit';
|
export { default as SingleLineTextEdit } from './SingleLineTextEdit';
|
||||||
|
export { default as FillSpace } from './FillSpace';
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const STATIC_SECTIONS = [
|
|||||||
'lists-loading',
|
'lists-loading',
|
||||||
'lists-failed',
|
'lists-failed',
|
||||||
'lists-no-actions',
|
'lists-no-actions',
|
||||||
|
'fillspace',
|
||||||
|
'fillspace-measure',
|
||||||
];
|
];
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
|||||||
BIN
tests/screenshots/reference/fillspace-measure.png
Normal file
BIN
tests/screenshots/reference/fillspace-measure.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
BIN
tests/screenshots/reference/fillspace.png
Normal file
BIN
tests/screenshots/reference/fillspace.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -32,6 +32,7 @@ const SOURCES = [
|
|||||||
{ file: 'src/panels/EditablePanelTile.jsx', importPath: 'dashgui/panels' },
|
{ file: 'src/panels/EditablePanelTile.jsx', importPath: 'dashgui/panels' },
|
||||||
{ file: 'src/ElevatedPill.jsx', importPath: 'dashgui/ElevatedPill' },
|
{ file: 'src/ElevatedPill.jsx', importPath: 'dashgui/ElevatedPill' },
|
||||||
{ file: 'src/SingleLineTextEdit.jsx', importPath: 'dashgui/SingleLineTextEdit' },
|
{ file: 'src/SingleLineTextEdit.jsx', importPath: 'dashgui/SingleLineTextEdit' },
|
||||||
|
{ file: 'src/FillSpace.jsx', importPath: 'dashgui/FillSpace' },
|
||||||
{ file: 'src/tiles/Tile.jsx', importPath: 'dashgui/tiles' },
|
{ file: 'src/tiles/Tile.jsx', importPath: 'dashgui/tiles' },
|
||||||
{ file: 'src/lists/RecordList.jsx', importPath: 'dashgui/lists' },
|
{ file: 'src/lists/RecordList.jsx', importPath: 'dashgui/lists' },
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user