u
This commit is contained in:
@@ -16,7 +16,7 @@ This package is source-only: consumers import directly from `src/` via the
|
||||
| `dashgui/tiles` | `src/tiles/` | `Tile`, `TILE_SIZE`, `tileBaseSx` |
|
||||
| `dashgui/ElevatedPill` | `src/ElevatedPill.jsx` | `ElevatedPill`, `ToggleElevatedPill`, `ActionElevatedPill` |
|
||||
| `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` |
|
||||
| `dashgui/FillSpace` | `src/FillSpace.jsx` | `FillSpace` — paginates `items` via `renderItem`, filling the viewport space between `aboveRef`/`belowRef`; wheel flips pages |
|
||||
|
||||
## Design rules
|
||||
|
||||
|
||||
@@ -260,13 +260,14 @@ class FillSpace extends Component {
|
||||
|
||||
// FillSpace owns current page, wheel-pagination and the pagination header.
|
||||
// Caller feeds `items` plus one row element via `renderItem` (component
|
||||
// type or callback).
|
||||
// type or callback). `items` may be undefined while data is loading.
|
||||
const { pageSize, currentPage, viewportHeight } = this.state;
|
||||
const total = items.length;
|
||||
const list = Array.isArray(items) ? items : [];
|
||||
const total = list.length;
|
||||
const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
|
||||
const safePage = Math.min(Math.max(1, currentPage), pageCount);
|
||||
const offset = (safePage - 1) * pageSize;
|
||||
const pageItems = items.slice(offset, offset + pageSize);
|
||||
const pageItems = list.slice(offset, offset + pageSize);
|
||||
const showPagination = total > pageSize;
|
||||
// Hidden data-free probe: measures `renderItem` itself (no data).
|
||||
const probe = renderProbeElement(renderItem);
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
import { Component, createRef } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import FillSpace from '../../FillSpace';
|
||||
import { ComponentDocs } from '../ComponentDocs';
|
||||
|
||||
/** Row renderer: component types are rendered as elements by FillSpace. */
|
||||
class Row extends Component {
|
||||
render() {
|
||||
const { item } = this.props;
|
||||
return (
|
||||
<ListItemButton divider disabled={!item}>
|
||||
<ListItemText primary={item ? item.name : '\u00A0'} />
|
||||
</ListItemButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SAMPLE_ITEMS = Array.from({ length: 40 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: `Eintrag ${i + 1}`,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Mock page exercising FillSpace: real content on top, and a repeating
|
||||
* `measureElement` filling only the leftover space down to the end of the
|
||||
* viewport.
|
||||
* Mock page exercising FillSpace: header/footer refs, `items` + `renderItem`,
|
||||
* wheel pagination and the built-in pagination header.
|
||||
*/
|
||||
class FillSpaceMockPage extends Component {
|
||||
constructor(props) {
|
||||
@@ -16,27 +34,26 @@ class FillSpaceMockPage extends Component {
|
||||
this.ex1 = { header: createRef(), footer: createRef() };
|
||||
this.ex2 = { header: createRef(), footer: createRef() };
|
||||
this.ex3 = { header: createRef(), footer: createRef() };
|
||||
this.state = { fitCount: null };
|
||||
}
|
||||
|
||||
render() {
|
||||
const { fitCount } = this.state;
|
||||
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.
|
||||
FillSpace paginates <code>items</code> via a <code>renderItem</code>{' '}
|
||||
row, measures the row height, fills the leftover viewport space and
|
||||
flips pages on wheel scroll.
|
||||
</Typography>
|
||||
|
||||
{/* Main example: content + repeating fill to end of page */}
|
||||
{/* Main example: items + renderItem to end of page */}
|
||||
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Content + repeating element to end of page
|
||||
Items + renderItem, filled to end of page
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Box
|
||||
ref={this.ex1.header}
|
||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
|
||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1, mb: 1 }}
|
||||
>
|
||||
<Typography variant="caption">Header (measured)</Typography>
|
||||
</Box>
|
||||
@@ -44,65 +61,17 @@ class FillSpaceMockPage extends Component {
|
||||
<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>
|
||||
}
|
||||
onMeasurement={(count) => this.setState({ fitCount: count })}
|
||||
>
|
||||
<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
|
||||
sx={{
|
||||
mt: 1,
|
||||
px: 1.5,
|
||||
py: 0.75,
|
||||
borderRadius: 1,
|
||||
bgcolor: 'background.paper',
|
||||
border: '1px dashed',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" component="span">
|
||||
📏 onMeasurement (würden ohne Inhalt passen):
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="span"
|
||||
fontWeight={700}
|
||||
color="primary"
|
||||
>
|
||||
{fitCount != null ? `${fitCount} Einheiten` : '…'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</FillSpace>
|
||||
items={SAMPLE_ITEMS}
|
||||
renderItem={Row}
|
||||
reserveBottom={32}
|
||||
minPageSize={5}
|
||||
/>
|
||||
|
||||
<Box
|
||||
ref={this.ex1.footer}
|
||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
|
||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1, mt: 1 }}
|
||||
>
|
||||
<Typography variant="caption">Footer (measured)</Typography>
|
||||
<Typography variant="caption">Footer (reserved)</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
@@ -115,7 +84,7 @@ class FillSpaceMockPage extends Component {
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Box
|
||||
ref={this.ex2.header}
|
||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
|
||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1, mb: 1 }}
|
||||
>
|
||||
<Typography variant="caption">Header</Typography>
|
||||
</Box>
|
||||
@@ -123,24 +92,15 @@ class FillSpaceMockPage extends Component {
|
||||
<FillSpace
|
||||
aboveRef={this.ex2.header}
|
||||
belowRef={this.ex2.footer}
|
||||
items={SAMPLE_ITEMS.slice(0, 6)}
|
||||
renderItem={Row}
|
||||
minPageSize={4}
|
||||
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 }}
|
||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1, mt: 1 }}
|
||||
>
|
||||
<Typography variant="caption">Footer</Typography>
|
||||
</Box>
|
||||
@@ -155,7 +115,7 @@ class FillSpaceMockPage extends Component {
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Box
|
||||
ref={this.ex3.header}
|
||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
|
||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1, mb: 1 }}
|
||||
>
|
||||
<Typography variant="caption">Header</Typography>
|
||||
</Box>
|
||||
@@ -163,25 +123,15 @@ class FillSpaceMockPage extends Component {
|
||||
<FillSpace
|
||||
aboveRef={this.ex3.header}
|
||||
belowRef={this.ex3.footer}
|
||||
items={SAMPLE_ITEMS}
|
||||
renderItem={Row}
|
||||
minPageSize={5}
|
||||
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 }}
|
||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1, mt: 1 }}
|
||||
>
|
||||
<Typography variant="caption">Footer</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { Component, createRef } from 'react';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import FolderIcon from '@mui/icons-material/FolderOutlined';
|
||||
@@ -307,8 +309,8 @@ class ScreenshotHarness extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministic FillSpace example for the screenshot harness. Bound with a
|
||||
* `maxHeight` so the reference is stable regardless of the viewport height.
|
||||
* Deterministic FillSpace example for the screenshot harness:
|
||||
* `items` + a component `renderItem`, bounded by above/below refs.
|
||||
*/
|
||||
class FillSpaceExample extends Component {
|
||||
constructor(props) {
|
||||
@@ -322,7 +324,7 @@ class FillSpaceExample extends Component {
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Box
|
||||
ref={this.headerRef}
|
||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
|
||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1, mb: 1 }}
|
||||
>
|
||||
<Typography variant="caption">Header (gemessen)</Typography>
|
||||
</Box>
|
||||
@@ -330,26 +332,15 @@ class FillSpaceExample extends Component {
|
||||
<FillSpace
|
||||
aboveRef={this.headerRef}
|
||||
belowRef={this.footerRef}
|
||||
items={FILL_SAMPLE_ITEMS}
|
||||
renderItem={FillRow}
|
||||
minPageSize={4}
|
||||
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 }}
|
||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1, mt: 1 }}
|
||||
>
|
||||
<Typography variant="caption">Footer (gemessen)</Typography>
|
||||
</Box>
|
||||
@@ -358,9 +349,36 @@ class FillSpaceExample extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
/** Row renderer used by the FillSpace harness examples. */
|
||||
class FillRow extends Component {
|
||||
render() {
|
||||
const { item } = this.props;
|
||||
return (
|
||||
<ListItemButton divider disabled={!item}>
|
||||
<ListItemText
|
||||
primary={
|
||||
item ? (
|
||||
`${item.name}`
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ visibility: 'hidden' }}>
|
||||
Platzhalter
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</ListItemButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const FILL_SAMPLE_ITEMS = Array.from({ length: 24 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: `Eintrag ${i + 1}`,
|
||||
}));
|
||||
|
||||
/**
|
||||
* FillSpace with a repeating measurement element plus real children on top,
|
||||
* for the screenshot harness. Bound with maxHeight so the reference is stable.
|
||||
* FillSpace with `items` + a component `renderItem`, for the screenshot
|
||||
* harness. Bounded by above/below refs so the reference is stable.
|
||||
*/
|
||||
class FillSpaceMeasureExample extends Component {
|
||||
constructor(props) {
|
||||
@@ -374,7 +392,7 @@ class FillSpaceMeasureExample extends Component {
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Box
|
||||
ref={this.headerRef}
|
||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
|
||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1, mb: 1 }}
|
||||
>
|
||||
<Typography variant="caption">Header</Typography>
|
||||
</Box>
|
||||
@@ -382,36 +400,15 @@ class FillSpaceMeasureExample extends Component {
|
||||
<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}
|
||||
items={FILL_SAMPLE_ITEMS}
|
||||
renderItem={FillRow}
|
||||
minPageSize={4}
|
||||
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 }}
|
||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1, mt: 1 }}
|
||||
>
|
||||
<Typography variant="caption">Footer</Typography>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user