Compare commits
10 Commits
db5a64a69a
...
6e23c34bd3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e23c34bd3 | ||
|
|
a7cb541e9d | ||
|
|
b0290852cc | ||
|
|
91ef0c2567 | ||
|
|
41079f3057 | ||
|
|
f997a954ee | ||
|
|
649239a930 | ||
|
|
eda3b193c6 | ||
|
|
14770f27ce | ||
|
|
9f8b46aa49 |
@@ -1,72 +1,110 @@
|
|||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import Pagination from '@mui/material/Pagination';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
import { alpha } from '@mui/material/styles';
|
import { alpha } from '@mui/material/styles';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vertical spacer that fills the remaining viewport height between the content
|
* FillSpace — paginates `items`.
|
||||||
* above it and (optionally) a footer below it — useful for creating scrollable
|
|
||||||
* middle sections in apps with fixed headers/footers.
|
|
||||||
*
|
*
|
||||||
* @component FillSpace
|
* @component FillSpace
|
||||||
* @import dashgui/FillSpace
|
* @import dashgui/FillSpace
|
||||||
* @defaultExport
|
* @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 {Array} items - Wheel flips pages.
|
||||||
* @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 {React.ComponentType|Function} renderItem - Row renderer (component or callback).
|
||||||
* @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} [minPageSize=10] - Rows per page.
|
||||||
* @prop {number} [measureGap=0] - Vertical gap (px) between repeated `measureElement` units.
|
* @prop {(page: number) => void} [onPageChange] - Called on page change.
|
||||||
* @prop {(count: number) => void} [onMeasurement] - Called whenever the number of `measureElement` units that would fit in the whole spacer height changes, as if there were no `children` (i.e. the total row count the space could hold). Fires once after mount with the initial count.
|
* @prop {boolean} [debug=false] - Dashed outline for layout debugging.
|
||||||
* @prop {number} [minHeight=0] - Minimum spacer height in px.
|
* @prop {object} [sx] - Extra styles.
|
||||||
* @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 `onMeasurement` fires with the count of units that would fit in the whole spacer height (as if `children` were absent) — not the number actually rendered below the content — whenever that count changes.
|
|
||||||
* @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.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** True for class components (e.g. `renderItem={ArtikelMarke}`). Rendered as element, not called. */
|
||||||
|
function isRowComponent(renderItem) {
|
||||||
|
return (
|
||||||
|
typeof renderItem === 'function' &&
|
||||||
|
(renderItem.prototype?.isReactComponent || renderItem.isFillRowComponent === true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Renders one row: component types become `<Row brand item index />`, callbacks are called. */
|
||||||
|
function renderRowElement(renderItem, item, index) {
|
||||||
|
if (isRowComponent(renderItem)) {
|
||||||
|
const Row = renderItem;
|
||||||
|
return <Row brand={item} item={item} index={index} />;
|
||||||
|
}
|
||||||
|
return renderItem(item, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data-free probe of the row height, rendered hidden. Class components
|
||||||
|
* (e.g. `renderItem={ArtikelMarke}`) tolerate empty props
|
||||||
|
* (`brand`/`item` undefined) and keep a fixed height (40px thumb /
|
||||||
|
* text field), so the probe measures the row without depending on the
|
||||||
|
* first data element. Plain callbacks are NOT probed — calling arbitrary
|
||||||
|
* user code with `undefined` could throw — they fall back to the first
|
||||||
|
* rendered data row.
|
||||||
|
*/
|
||||||
|
function renderProbeElement(renderItem) {
|
||||||
|
if (!isRowComponent(renderItem)) return null;
|
||||||
|
const Row = renderItem;
|
||||||
|
return <Row brand={undefined} item={undefined} index={0} disabled />;
|
||||||
|
}
|
||||||
|
|
||||||
class FillSpace extends Component {
|
class FillSpace extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
height: 0,
|
|
||||||
unitHeight: 0,
|
unitHeight: 0,
|
||||||
contentHeight: 0,
|
viewportHeight: null,
|
||||||
totalCount: 0,
|
pageSize: Number(props.minPageSize) || 10,
|
||||||
fillCount: 0,
|
currentPage: 1,
|
||||||
};
|
};
|
||||||
this.lastTotalCount = null;
|
|
||||||
this.ref = null;
|
this.ref = null;
|
||||||
this.contentRef = null;
|
this.probeRef = null;
|
||||||
this.unitRef = null;
|
this.wheelAcc = 0;
|
||||||
this.observer = null;
|
this.observer = null;
|
||||||
this.raf = null;
|
this.raf = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
requestAnimationFrame(() => {
|
this._probeMeasure();
|
||||||
this._measure();
|
this._viewportMeasure();
|
||||||
this._observe();
|
this._observe();
|
||||||
});
|
|
||||||
window.addEventListener('resize', this._onViewportChange);
|
window.addEventListener('resize', this._onViewportChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
|
if (prevProps.renderItem !== this.props.renderItem) {
|
||||||
|
this._probeMeasure();
|
||||||
|
}
|
||||||
|
if (prevProps.minPageSize !== this.props.minPageSize) {
|
||||||
|
this.setState({ pageSize: Number(this.props.minPageSize) || 10 });
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
prevProps.aboveRef !== this.props.aboveRef ||
|
prevProps.aboveRef !== this.props.aboveRef ||
|
||||||
prevProps.belowRef !== this.props.belowRef ||
|
prevProps.belowRef !== this.props.belowRef ||
|
||||||
prevProps.containerRef !== this.props.containerRef ||
|
prevProps.containerRef !== this.props.containerRef ||
|
||||||
prevProps.minHeight !== this.props.minHeight ||
|
prevProps.reserveBottom !== this.props.reserveBottom ||
|
||||||
prevProps.maxHeight !== this.props.maxHeight ||
|
prevProps.items !== this.props.items
|
||||||
prevProps.measureElement !== this.props.measureElement ||
|
|
||||||
prevProps.measureGap !== this.props.measureGap
|
|
||||||
) {
|
) {
|
||||||
this._observe();
|
this._observe();
|
||||||
this._measure();
|
this._viewportMeasure();
|
||||||
|
}
|
||||||
|
// Clamp current page when the item list shrinks (e.g. brand deleted).
|
||||||
|
if (
|
||||||
|
Array.isArray(this.props.items) &&
|
||||||
|
this.props.items !== prevProps.items
|
||||||
|
) {
|
||||||
|
const pageCount = Math.max(
|
||||||
|
1,
|
||||||
|
Math.ceil(this.props.items.length / Math.max(1, this.state.pageSize)),
|
||||||
|
);
|
||||||
|
if (this.state.currentPage > pageCount) {
|
||||||
|
this.setState({ currentPage: pageCount });
|
||||||
|
this.props.onPageChange?.(pageCount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +124,7 @@ class FillSpace extends Component {
|
|||||||
if (this.raf) return;
|
if (this.raf) return;
|
||||||
this.raf = requestAnimationFrame(() => {
|
this.raf = requestAnimationFrame(() => {
|
||||||
this.raf = null;
|
this.raf = null;
|
||||||
this._measure();
|
this._viewportMeasure();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,20 +132,16 @@ class FillSpace extends Component {
|
|||||||
if (this.observer) {
|
if (this.observer) {
|
||||||
this.observer.disconnect();
|
this.observer.disconnect();
|
||||||
}
|
}
|
||||||
|
this.observer = new ResizeObserver(() => this._onViewportChange());
|
||||||
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
|
|
||||||
const { aboveRef, belowRef, containerRef } = this.props;
|
const { aboveRef, belowRef, containerRef } = this.props;
|
||||||
const aboveList = Array.isArray(aboveRef) ? aboveRef : [aboveRef];
|
const aboveList = Array.isArray(aboveRef) ? aboveRef : [aboveRef];
|
||||||
const belowList = Array.isArray(belowRef) ? belowRef : [belowRef];
|
const belowList = Array.isArray(belowRef) ? belowRef : [belowRef];
|
||||||
const containerTarget = containerRef && 'current' in containerRef ? containerRef.current : containerRef;
|
const containerTarget =
|
||||||
|
containerRef && 'current' in containerRef ? containerRef.current : containerRef;
|
||||||
const targets = [
|
const targets = [
|
||||||
...aboveList.map((r) => (r && 'current' in r ? r.current : r)),
|
...aboveList.map((r) => (r && 'current' in r ? r.current : r)),
|
||||||
...belowList.map((r) => (r && 'current' in r ? r.current : r)),
|
...belowList.map((r) => (r && 'current' in r ? r.current : r)),
|
||||||
containerTarget,
|
containerTarget,
|
||||||
this.contentRef,
|
|
||||||
];
|
];
|
||||||
for (const el of targets) {
|
for (const el of targets) {
|
||||||
if (el && el.offsetParent !== null) {
|
if (el && el.offsetParent !== null) {
|
||||||
@@ -116,162 +150,201 @@ class FillSpace extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
measure = () => {
|
probeMeasure = () => {
|
||||||
this._measure();
|
this._probeMeasure();
|
||||||
};
|
};
|
||||||
|
|
||||||
_measure() {
|
viewportMeasure = () => {
|
||||||
if (!this.ref) return;
|
this._viewportMeasure();
|
||||||
|
};
|
||||||
|
|
||||||
const {
|
measure = () => {
|
||||||
belowRef,
|
this._probeMeasure();
|
||||||
containerRef,
|
this._viewportMeasure();
|
||||||
measureElement,
|
};
|
||||||
measureGap = 0,
|
|
||||||
minHeight = 0,
|
|
||||||
maxHeight,
|
|
||||||
onMeasurement,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
// Distance from the document top to the spacer top — includes every
|
/** Only measures the height of the `renderItem` probe. Nothing else. */
|
||||||
// element above the spacer plus their margins and the parent's padding.
|
_probeMeasure() {
|
||||||
// Document-relative (rect.top + scrollY), so scrolling the page does not
|
const h = this.probeRef?.offsetHeight;
|
||||||
// change the computed height (fixes "grows with scrolling").
|
if (h > 0 && h !== this.state.unitHeight) {
|
||||||
const rect = this.ref.getBoundingClientRect();
|
this.setState({ unitHeight: h }, () => this._viewportMeasure());
|
||||||
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 containerEl = containerRef && 'current' in containerRef ? containerRef.current : containerRef;
|
/** Viewport height minus above/below refs: free space for the list. */
|
||||||
|
_viewportMeasure() {
|
||||||
|
if (!this.ref) return;
|
||||||
|
const { belowRef, containerRef, reserveBottom = 0, minPageSize } = this.props;
|
||||||
|
const rect = this.ref.getBoundingClientRect();
|
||||||
|
let belowPx = Number(reserveBottom) || 0;
|
||||||
|
const belowList = Array.isArray(belowRef) ? belowRef : [belowRef];
|
||||||
|
for (const ref of belowList) {
|
||||||
|
const el = ref && 'current' in ref ? ref.current : ref;
|
||||||
|
if (el && el.offsetParent !== null) {
|
||||||
|
belowPx += el.offsetHeight;
|
||||||
|
const mt = parseFloat(window.getComputedStyle(el).marginTop);
|
||||||
|
if (Number.isFinite(mt)) belowPx += mt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const containerEl =
|
||||||
|
containerRef && 'current' in containerRef ? containerRef.current : containerRef;
|
||||||
let available;
|
let available;
|
||||||
if (containerEl && typeof containerEl.getBoundingClientRect === 'function') {
|
if (containerEl && typeof containerEl.getBoundingClientRect === 'function') {
|
||||||
const cRect = containerEl.getBoundingClientRect();
|
const cRect = containerEl.getBoundingClientRect();
|
||||||
const paddingBottom = parseFloat(window.getComputedStyle(containerEl).paddingBottom) || 0;
|
const paddingBottom = parseFloat(window.getComputedStyle(containerEl).paddingBottom) || 0;
|
||||||
available = Math.max(0, cRect.bottom - rect.top - belowPx - paddingBottom);
|
available = Math.max(0, cRect.bottom - rect.top - belowPx - paddingBottom);
|
||||||
} else {
|
} else {
|
||||||
available = Math.max(0, window.innerHeight - abovePx - belowPx);
|
available = Math.max(0, window.innerHeight - rect.top - belowPx);
|
||||||
}
|
}
|
||||||
|
const minSize = Math.max(1, Number(minPageSize) || 10);
|
||||||
const clamped = Math.min(
|
const { unitHeight } = this.state;
|
||||||
Math.max(available, minHeight),
|
const nextPageSize =
|
||||||
maxHeight ?? Infinity,
|
unitHeight > 0 ? Math.max(minSize, Math.floor(available / unitHeight)) : minSize;
|
||||||
);
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// How many units would fit in the WHOLE spacer height, as if there were
|
|
||||||
// no children — this is what onMeasurement reports.
|
|
||||||
let totalCount = 0;
|
|
||||||
// How many units actually fit in the leftover space below the children —
|
|
||||||
// this is what gets rendered. Starts at 0: if the content already fills
|
|
||||||
// the space, none fit.
|
|
||||||
let fillCount = 0;
|
|
||||||
if (measureElement != null && unitHeight > 0) {
|
|
||||||
totalCount = Math.floor((clamped + measureGap) / (unitHeight + measureGap));
|
|
||||||
const fillHeight = Math.max(0, clamped - contentHeight);
|
|
||||||
fillCount = Math.floor((fillHeight + measureGap) / (unitHeight + measureGap));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire onMeasurement with the TOTAL count whenever it changes — including
|
|
||||||
// the first measurement. lastTotalCount starts as null so the initial
|
|
||||||
// value (even 0) is always reported once.
|
|
||||||
if (
|
if (
|
||||||
measureElement != null &&
|
available !== this.state.viewportHeight ||
|
||||||
typeof onMeasurement === 'function' &&
|
nextPageSize !== this.state.pageSize
|
||||||
totalCount !== this.lastTotalCount
|
|
||||||
) {
|
) {
|
||||||
this.lastTotalCount = totalCount;
|
this.setState({ viewportHeight: available, pageSize: nextPageSize });
|
||||||
onMeasurement(totalCount);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
_handleWheel = (event) => {
|
||||||
Math.abs(clamped - this.state.height) > 0.5 ||
|
const { items } = this.props;
|
||||||
Math.abs(unitHeight - this.state.unitHeight) > 0.5 ||
|
if (!Array.isArray(items)) return;
|
||||||
Math.abs(contentHeight - this.state.contentHeight) > 0.5 ||
|
const { pageSize, currentPage } = this.state;
|
||||||
totalCount !== this.state.totalCount ||
|
const pageCount = Math.max(1, Math.ceil(items.length / Math.max(1, pageSize)));
|
||||||
fillCount !== this.state.fillCount
|
const delta = event.deltaY || 0;
|
||||||
) {
|
if (pageCount <= 1) return;
|
||||||
this.setState({ height: clamped, unitHeight, contentHeight, totalCount, fillCount });
|
if (delta === 0) return;
|
||||||
}
|
if (Math.sign(delta) !== Math.sign(this.wheelAcc || 0)) this.wheelAcc = 0;
|
||||||
}
|
this.wheelAcc = (this.wheelAcc || 0) + delta;
|
||||||
|
const threshold = Math.max(50, 800 / pageCount);
|
||||||
|
if (Math.abs(this.wheelAcc) < threshold) return;
|
||||||
|
const dir = this.wheelAcc > 0 ? 1 : -1;
|
||||||
|
this.wheelAcc = 0;
|
||||||
|
const next = dir > 0
|
||||||
|
? Math.min(pageCount, currentPage + 1)
|
||||||
|
: Math.max(1, currentPage - 1);
|
||||||
|
if (next === currentPage) return;
|
||||||
|
this._goToPage(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
_goToPage = (page) => {
|
||||||
|
const { items, onPageChange } = this.props;
|
||||||
|
const total = Array.isArray(items) ? items.length : 0;
|
||||||
|
const pageCount = Math.max(1, Math.ceil(total / Math.max(1, this.state.pageSize)));
|
||||||
|
const next = Math.min(Math.max(1, Number(page) || 1), pageCount);
|
||||||
|
if (next === this.state.currentPage) return;
|
||||||
|
this.setState({ currentPage: next });
|
||||||
|
onPageChange?.(next);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
// Discarded legacy layout props (kept here so they don't leak to the DOM).
|
||||||
aboveRef,
|
aboveRef,
|
||||||
belowRef,
|
belowRef,
|
||||||
onMeasurement,
|
containerRef,
|
||||||
minHeight = 0,
|
reserveBottom,
|
||||||
|
minHeight,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
|
minPageSize,
|
||||||
|
onPageChange,
|
||||||
|
onWheel,
|
||||||
debug = false,
|
debug = false,
|
||||||
children,
|
items,
|
||||||
measureElement,
|
renderItem,
|
||||||
measureGap = 0,
|
|
||||||
sx,
|
sx,
|
||||||
...rest
|
...rest
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { height, fillCount } = this.state;
|
// FillSpace owns current page, wheel-pagination and the pagination header.
|
||||||
|
// Caller feeds `items` plus one row element via `renderItem` (component
|
||||||
// Repeated measurement units fill only the leftover space below the real
|
// type or callback).
|
||||||
// children. fillCount (computed in _measure) starts at 0: if the content
|
const { pageSize, currentPage, viewportHeight } = this.state;
|
||||||
// already fills the space, no units are rendered.
|
const total = items.length;
|
||||||
let fill = null;
|
const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
|
||||||
if (measureElement != null) {
|
const safePage = Math.min(Math.max(1, currentPage), pageCount);
|
||||||
const items = [];
|
const offset = (safePage - 1) * pageSize;
|
||||||
for (let i = 0; i < fillCount; i++) {
|
const pageItems = items.slice(offset, offset + pageSize);
|
||||||
items.push(
|
const showPagination = total > pageSize;
|
||||||
<Box key={i} sx={{ width: '100%' }}>
|
// Hidden data-free probe: measures `renderItem` itself (no data).
|
||||||
{measureElement}
|
const probe = renderProbeElement(renderItem);
|
||||||
</Box>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
fill = (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: measureGap,
|
|
||||||
width: '100%',
|
|
||||||
overflow: 'hidden',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{items}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Box
|
||||||
|
// Fixed single-line bar: the header height must not depend on the
|
||||||
|
// page count. Otherwise the first fit (e.g. "1-17") pushes the
|
||||||
|
// spacer down, the available height shrinks, and a second pass
|
||||||
|
// flips to "1-15". One pass: constant 44px, real values always
|
||||||
|
// rendered (only visibility is gated), no placeholder numbers.
|
||||||
|
sx={{ mb: 1, height: 44, overflow: 'hidden' }}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
aria-hidden={!showPagination || undefined}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexWrap: 'nowrap',
|
||||||
|
gap: 1,
|
||||||
|
width: '100%',
|
||||||
|
height: 44,
|
||||||
|
visibility: showPagination ? 'visible' : 'hidden',
|
||||||
|
pointerEvents: showPagination ? 'auto' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
noWrap
|
||||||
|
sx={{ flexShrink: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }}
|
||||||
|
>
|
||||||
|
{total === 0 ? '0' : `${offset + 1}–${Math.min(offset + pageSize, total)}`} von{' '}
|
||||||
|
{total}
|
||||||
|
</Typography>
|
||||||
|
<Pagination
|
||||||
|
count={pageCount}
|
||||||
|
page={safePage}
|
||||||
|
onChange={(_e, page) => this._goToPage(page)}
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
sx={{
|
||||||
|
flexShrink: 0,
|
||||||
|
maxWidth: '100%',
|
||||||
|
overflowX: 'auto',
|
||||||
|
scrollbarWidth: 'none',
|
||||||
|
'&::-webkit-scrollbar': { display: 'none' },
|
||||||
|
'& .MuiPagination-ul': { flexWrap: 'nowrap' },
|
||||||
|
'& .MuiPaginationItem-root': {
|
||||||
|
minWidth: 44,
|
||||||
|
height: 44,
|
||||||
|
fontSize: '1rem',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
this.ref = el;
|
this.ref = el;
|
||||||
}}
|
}}
|
||||||
|
onWheel={(e) => {
|
||||||
|
this._handleWheel(e);
|
||||||
|
onWheel?.(e);
|
||||||
|
}}
|
||||||
{...rest}
|
{...rest}
|
||||||
sx={[
|
sx={[
|
||||||
{
|
{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
height,
|
height: viewportHeight ?? 'auto',
|
||||||
minHeight,
|
overflow: 'hidden',
|
||||||
...(maxHeight != null ? { maxHeight } : {}),
|
width: '100%',
|
||||||
|
// Keep wheel ticks from scrolling the page behind the list.
|
||||||
|
// (No preventDefault needed — React wheel is passive.)
|
||||||
|
overscrollBehavior: 'contain',
|
||||||
},
|
},
|
||||||
debug
|
debug
|
||||||
? (theme) => ({
|
? (theme) => ({
|
||||||
@@ -281,21 +354,19 @@ class FillSpace extends Component {
|
|||||||
sx,
|
sx,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{children != null ? (
|
<Box sx={{ width: '100%' }}>
|
||||||
<Box
|
<List dense disablePadding sx={{ width: '100%' }}>
|
||||||
ref={(el) => {
|
{pageItems.map((item, i) => (
|
||||||
this.contentRef = el;
|
<Box key={item?.id ?? offset + i} data-fill-row sx={{ width: '100%' }}>
|
||||||
}}
|
{renderRowElement(renderItem, item, offset + i)}
|
||||||
sx={{ width: '100%', ...(measureElement == null ? { height: '100%' } : {}) }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Box>
|
</Box>
|
||||||
) : null}
|
))}
|
||||||
{fill}
|
</List>
|
||||||
{measureElement != null ? (
|
</Box>
|
||||||
|
{probe != null ? (
|
||||||
<Box
|
<Box
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
this.unitRef = el;
|
this.probeRef = el;
|
||||||
}}
|
}}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
sx={{
|
sx={{
|
||||||
@@ -307,7 +378,11 @@ class FillSpace extends Component {
|
|||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{measureElement}
|
<List dense disablePadding sx={{ width: '100%' }}>
|
||||||
|
<Box data-fill-row sx={{ width: '100%' }}>
|
||||||
|
{probe}
|
||||||
|
</Box>
|
||||||
|
</List>
|
||||||
</Box>
|
</Box>
|
||||||
) : null}
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user