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/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` |
|
| `dashgui/FillSpace` | `src/FillSpace.jsx` | `FillSpace` — paginates `items` via `renderItem`, filling the viewport space between `aboveRef`/`belowRef`; wheel flips pages |
|
||||||
|
|
||||||
## Design rules
|
## Design rules
|
||||||
|
|
||||||
|
|||||||
@@ -260,13 +260,14 @@ class FillSpace extends Component {
|
|||||||
|
|
||||||
// FillSpace owns current page, wheel-pagination and the pagination header.
|
// FillSpace owns current page, wheel-pagination and the pagination header.
|
||||||
// Caller feeds `items` plus one row element via `renderItem` (component
|
// 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 { 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 pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
|
||||||
const safePage = Math.min(Math.max(1, currentPage), pageCount);
|
const safePage = Math.min(Math.max(1, currentPage), pageCount);
|
||||||
const offset = (safePage - 1) * pageSize;
|
const offset = (safePage - 1) * pageSize;
|
||||||
const pageItems = items.slice(offset, offset + pageSize);
|
const pageItems = list.slice(offset, offset + pageSize);
|
||||||
const showPagination = total > pageSize;
|
const showPagination = total > pageSize;
|
||||||
// Hidden data-free probe: measures `renderItem` itself (no data).
|
// Hidden data-free probe: measures `renderItem` itself (no data).
|
||||||
const probe = renderProbeElement(renderItem);
|
const probe = renderProbeElement(renderItem);
|
||||||
|
|||||||
@@ -1,14 +1,32 @@
|
|||||||
import { Component, createRef } from 'react';
|
import { Component, createRef } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
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 Paper from '@mui/material/Paper';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import FillSpace from '../../FillSpace';
|
import FillSpace from '../../FillSpace';
|
||||||
import { ComponentDocs } from '../ComponentDocs';
|
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
|
* Mock page exercising FillSpace: header/footer refs, `items` + `renderItem`,
|
||||||
* `measureElement` filling only the leftover space down to the end of the
|
* wheel pagination and the built-in pagination header.
|
||||||
* viewport.
|
|
||||||
*/
|
*/
|
||||||
class FillSpaceMockPage extends Component {
|
class FillSpaceMockPage extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -16,27 +34,26 @@ class FillSpaceMockPage extends Component {
|
|||||||
this.ex1 = { header: createRef(), footer: createRef() };
|
this.ex1 = { header: createRef(), footer: createRef() };
|
||||||
this.ex2 = { header: createRef(), footer: createRef() };
|
this.ex2 = { header: createRef(), footer: createRef() };
|
||||||
this.ex3 = { header: createRef(), footer: createRef() };
|
this.ex3 = { header: createRef(), footer: createRef() };
|
||||||
this.state = { fitCount: null };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { fitCount } = this.state;
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
FillSpace renders real content on top and repeats the measurement
|
FillSpace paginates <code>items</code> via a <code>renderItem</code>{' '}
|
||||||
element until the end of the page.
|
row, measures the row height, fills the leftover viewport space and
|
||||||
|
flips pages on wheel scroll.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Main example: content + repeating fill to end of page */}
|
{/* Main example: items + renderItem to end of page */}
|
||||||
<Paper variant="outlined" sx={{ p: 2 }}>
|
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
Content + repeating element to end of page
|
Items + renderItem, filled to end of page
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<Box
|
<Box
|
||||||
ref={this.ex1.header}
|
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>
|
<Typography variant="caption">Header (measured)</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -44,65 +61,17 @@ class FillSpaceMockPage extends Component {
|
|||||||
<FillSpace
|
<FillSpace
|
||||||
aboveRef={this.ex1.header}
|
aboveRef={this.ex1.header}
|
||||||
belowRef={this.ex1.footer}
|
belowRef={this.ex1.footer}
|
||||||
measureElement={
|
items={SAMPLE_ITEMS}
|
||||||
<Box
|
renderItem={Row}
|
||||||
sx={{
|
reserveBottom={32}
|
||||||
bgcolor: '#dbeafe',
|
minPageSize={5}
|
||||||
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>
|
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
ref={this.ex1.footer}
|
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>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
@@ -115,7 +84,7 @@ class FillSpaceMockPage extends Component {
|
|||||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<Box
|
<Box
|
||||||
ref={this.ex2.header}
|
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>
|
<Typography variant="caption">Header</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -123,24 +92,15 @@ class FillSpaceMockPage extends Component {
|
|||||||
<FillSpace
|
<FillSpace
|
||||||
aboveRef={this.ex2.header}
|
aboveRef={this.ex2.header}
|
||||||
belowRef={this.ex2.footer}
|
belowRef={this.ex2.footer}
|
||||||
|
items={SAMPLE_ITEMS.slice(0, 6)}
|
||||||
|
renderItem={Row}
|
||||||
|
minPageSize={4}
|
||||||
minHeight={200}
|
minHeight={200}
|
||||||
>
|
/>
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
bgcolor: '#fef3c7',
|
|
||||||
p: 2,
|
|
||||||
borderRadius: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="body2">
|
|
||||||
Minimum height of 200px enforced.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</FillSpace>
|
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
ref={this.ex2.footer}
|
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>
|
<Typography variant="caption">Footer</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -155,7 +115,7 @@ class FillSpaceMockPage extends Component {
|
|||||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<Box
|
<Box
|
||||||
ref={this.ex3.header}
|
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>
|
<Typography variant="caption">Header</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -163,25 +123,15 @@ class FillSpaceMockPage extends Component {
|
|||||||
<FillSpace
|
<FillSpace
|
||||||
aboveRef={this.ex3.header}
|
aboveRef={this.ex3.header}
|
||||||
belowRef={this.ex3.footer}
|
belowRef={this.ex3.footer}
|
||||||
|
items={SAMPLE_ITEMS}
|
||||||
|
renderItem={Row}
|
||||||
|
minPageSize={5}
|
||||||
debug
|
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
|
<Box
|
||||||
ref={this.ex3.footer}
|
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>
|
<Typography variant="caption">Footer</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ 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';
|
||||||
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import FolderIcon from '@mui/icons-material/FolderOutlined';
|
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
|
* Deterministic FillSpace example for the screenshot harness:
|
||||||
* `maxHeight` so the reference is stable regardless of the viewport height.
|
* `items` + a component `renderItem`, bounded by above/below refs.
|
||||||
*/
|
*/
|
||||||
class FillSpaceExample extends Component {
|
class FillSpaceExample extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -322,7 +324,7 @@ class FillSpaceExample extends Component {
|
|||||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<Box
|
<Box
|
||||||
ref={this.headerRef}
|
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>
|
<Typography variant="caption">Header (gemessen)</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -330,26 +332,15 @@ class FillSpaceExample extends Component {
|
|||||||
<FillSpace
|
<FillSpace
|
||||||
aboveRef={this.headerRef}
|
aboveRef={this.headerRef}
|
||||||
belowRef={this.footerRef}
|
belowRef={this.footerRef}
|
||||||
|
items={FILL_SAMPLE_ITEMS}
|
||||||
|
renderItem={FillRow}
|
||||||
|
minPageSize={4}
|
||||||
minHeight={80}
|
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
|
<Box
|
||||||
ref={this.footerRef}
|
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>
|
<Typography variant="caption">Footer (gemessen)</Typography>
|
||||||
</Box>
|
</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,
|
* FillSpace with `items` + a component `renderItem`, for the screenshot
|
||||||
* for the screenshot harness. Bound with maxHeight so the reference is stable.
|
* harness. Bounded by above/below refs so the reference is stable.
|
||||||
*/
|
*/
|
||||||
class FillSpaceMeasureExample extends Component {
|
class FillSpaceMeasureExample extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -374,7 +392,7 @@ class FillSpaceMeasureExample extends Component {
|
|||||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<Box
|
<Box
|
||||||
ref={this.headerRef}
|
ref={this.headerRef}
|
||||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
|
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1, mb: 1 }}
|
||||||
>
|
>
|
||||||
<Typography variant="caption">Header</Typography>
|
<Typography variant="caption">Header</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -382,36 +400,15 @@ class FillSpaceMeasureExample extends Component {
|
|||||||
<FillSpace
|
<FillSpace
|
||||||
aboveRef={this.headerRef}
|
aboveRef={this.headerRef}
|
||||||
belowRef={this.footerRef}
|
belowRef={this.footerRef}
|
||||||
measureElement={
|
items={FILL_SAMPLE_ITEMS}
|
||||||
<Box
|
renderItem={FillRow}
|
||||||
sx={{
|
minPageSize={4}
|
||||||
bgcolor: '#c7d2fe',
|
|
||||||
p: 1,
|
|
||||||
borderRadius: 1,
|
|
||||||
textAlign: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="caption">Einheit</Typography>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
measureGap={8}
|
|
||||||
minHeight={60}
|
minHeight={60}
|
||||||
maxHeight={260}
|
/>
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
bgcolor: '#fef3c7',
|
|
||||||
p: 1,
|
|
||||||
borderRadius: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="body2">Echte Inhalte</Typography>
|
|
||||||
</Box>
|
|
||||||
</FillSpace>
|
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
ref={this.footerRef}
|
ref={this.footerRef}
|
||||||
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1 }}
|
sx={{ bgcolor: '#e2e8f0', p: 1, borderRadius: 1, mt: 1 }}
|
||||||
>
|
>
|
||||||
<Typography variant="caption">Footer</Typography>
|
<Typography variant="caption">Footer</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user