Files
2026-08-07 21:39:30 +02:00

245 lines
9.5 KiB
JavaScript

'use client';
import * as React from 'react';
import useForkRef from '@mui/utils/useForkRef';
import useEventCallback from '@mui/utils/useEventCallback';
import { platform } from '@base-ui/utils/platform';
import { createSelectorMemoized } from '@mui/x-internals/store';
import { Dimensions } from "../dimensions.mjs";
import { Virtualization } from "./virtualization.mjs";
import useRefCallback from "../../utils/useRefCallback.mjs";
/* eslint-disable react-hooks/rules-of-hooks */
export class Layout {
static elements = ['scroller', 'container'];
constructor(refs) {
this.refs = refs;
}
refSetter(name) {
return node => {
if (this.refs[name].current !== node) {
this.refs[name].current = node;
}
};
}
}
export class LayoutDataGrid extends Layout {
static elements = ['scroller', 'container', 'content', 'positioner', 'scrollbarVertical', 'scrollbarHorizontal'];
use(store, _params, api, layoutParams) {
const {
scrollerRef,
containerRef
} = layoutParams;
const scrollbarVerticalRef = useScrollbarRefCallback(this.refs.scroller, this.refSetter('scrollbarVertical'), 'scrollTop', api.updateDimensions);
const scrollbarHorizontalRef = useScrollbarRefCallback(this.refs.scroller, this.refSetter('scrollbarHorizontal'), 'scrollLeft', api.updateDimensions);
store.state.virtualization.context = {
scrollerRef,
containerRef,
scrollbarVerticalRef,
scrollbarHorizontalRef
};
}
static selectors = {
containerProps: createSelectorMemoized(Virtualization.selectors.context, context => ({
ref: context.containerRef
})),
scrollerProps: createSelectorMemoized(Virtualization.selectors.context, Dimensions.selectors.autoHeight, Dimensions.selectors.needsHorizontalScrollbar, (context, autoHeight, needsHorizontalScrollbar) => ({
ref: context.scrollerRef,
style: {
// TODO: fall back to overflow: 'auto' if no overflowX or overflowY is set?
overflowX: !needsHorizontalScrollbar ? 'hidden' : undefined,
overflowY: autoHeight ? 'hidden' : undefined
// TODO: should include display: 'flex', flexDirection: 'column' since the Content has flexBasis and flexShrink?
},
role: 'presentation',
// `tabIndex` shouldn't be used along role=presentation, but it fixes a Firefox bug
// https://github.com/mui/mui-x/pull/13891#discussion_r1683416024
tabIndex: platform.engine.gecko ? -1 : undefined
})),
scrollerContentProps: createSelectorMemoized(Virtualization.selectors.layoutMode, Dimensions.selectors.dimensions, Dimensions.selectors.needsVerticalScrollbar, Dimensions.selectors.needsHorizontalScrollbar, (layoutMode, dimensions, needsVerticalScrollbar, needsHorizontalScrollbar) => {
let style;
if (layoutMode === 'controlled') {
const {
contentSize,
scrollbarSize,
topContainerHeight,
bottomContainerHeight,
minimalContentHeight,
columnsTotalWidth,
viewportOuterSize
} = dimensions;
const verticalScrollbarSize = needsVerticalScrollbar ? scrollbarSize : 0;
const horizontalScrollbarSize = needsHorizontalScrollbar ? scrollbarSize : 0;
const contentHeight = contentSize.height === 0 ? minimalContentHeight : contentSize.height;
const width = needsHorizontalScrollbar ? verticalScrollbarSize + columnsTotalWidth : 'auto';
const height = cssAdd(cssAdd(cssAdd(contentHeight, topContainerHeight), bottomContainerHeight), horizontalScrollbarSize);
style = {
width: cssMax(width, viewportOuterSize.width - verticalScrollbarSize),
height: cssMax(height, viewportOuterSize.height - horizontalScrollbarSize),
flex: '0 0 auto'
};
}
return {
style,
role: 'presentation'
};
}),
viewportProps: createSelectorMemoized(Dimensions.selectors.dimensions, dimensions => ({
style: {
width: dimensions.viewportOuterSize.width,
height: dimensions.viewportOuterSize.height
},
role: 'presentation'
})),
contentProps: createSelectorMemoized(Dimensions.selectors.contentHeight, Dimensions.selectors.minimalContentHeight, Dimensions.selectors.columnsTotalWidth, Dimensions.selectors.needsHorizontalScrollbar, (contentHeight, minimalContentHeight, columnsTotalWidth, needsHorizontalScrollbar) => ({
style: {
width: needsHorizontalScrollbar ? columnsTotalWidth : 'auto',
height: contentHeight === 0 ? minimalContentHeight : contentHeight,
flex: '0 0 auto'
},
role: 'presentation'
})),
positionerProps: createSelectorMemoized(Virtualization.selectors.layoutMode, Virtualization.selectors.offsetTop, Virtualization.selectors.scrollPosition, (layoutMode, offsetTop, scrollPosition) => ({
style: {
transform: layoutMode === 'uncontrolled' ? `translate3d(0, ${offsetTop}px, 0)` : `translate3d(${-scrollPosition.current.left}px, ${offsetTop - scrollPosition.current.top}px, 0)`
}
})),
containerVerticalProps: createSelectorMemoized(Virtualization.selectors.layoutMode, Virtualization.selectors.scrollPosition, (layoutMode, scrollPosition) => layoutMode === 'uncontrolled' ? undefined : {
style: {
transform: `translate3d(${-scrollPosition.current.left}px, 0, 0)`
}
}),
scrollbarHorizontalProps: createSelectorMemoized(Virtualization.selectors.context, Virtualization.selectors.scrollPosition, (context, scrollPosition) => ({
ref: context.scrollbarHorizontalRef,
scrollPosition
})),
scrollbarVerticalProps: createSelectorMemoized(Virtualization.selectors.context, Virtualization.selectors.scrollPosition, (context, scrollPosition) => ({
ref: context.scrollbarVerticalRef,
scrollPosition
})),
scrollAreaProps: createSelectorMemoized(Virtualization.selectors.scrollPosition, scrollPosition => ({
scrollPosition
}))
};
}
export class LayoutList extends Layout {
static elements = ['scroller', 'container', 'content', 'positioner'];
use(store, _params, _api, layoutParams) {
const {
scrollerRef,
containerRef
} = layoutParams;
const mergedRef = useForkRef(scrollerRef, containerRef);
store.state.virtualization.context = {
mergedRef
};
}
static selectors = {
containerProps: createSelectorMemoized(Virtualization.selectors.context, Dimensions.selectors.autoHeight, Dimensions.selectors.needsHorizontalScrollbar, (context, autoHeight, needsHorizontalScrollbar) => ({
ref: context.mergedRef,
style: {
overflowX: !needsHorizontalScrollbar ? 'hidden' : undefined,
overflowY: autoHeight ? 'hidden' : undefined,
position: 'relative'
},
role: 'presentation',
// `tabIndex` shouldn't be used along role=presentation, but it fixes a Firefox bug
// https://github.com/mui/mui-x/pull/13891#discussion_r1683416024
tabIndex: platform.engine.gecko ? -1 : undefined
})),
contentProps: createSelectorMemoized(Dimensions.selectors.contentHeight, contentHeight => ({
style: {
position: 'absolute',
display: 'inline-block',
width: '100%',
height: contentHeight,
top: 0,
left: 0,
zIndex: -1
},
role: 'presentation'
})),
positionerProps: createSelectorMemoized(Virtualization.selectors.offsetTop, offsetTop => ({
style: {
height: offsetTop
}
}))
};
}
function useScrollbarRefCallback(scrollerRef, refSetter, scrollProperty, updateDimensions) {
const isLocked = React.useRef(false);
const lastPosition = React.useRef(0);
const handleScrollerScroll = useEventCallback(scrollbar => {
const scroller = scrollerRef.current;
if (!scroller) {
return;
}
const scrollerPosition = scroller[scrollProperty];
if (scrollerPosition === lastPosition.current) {
return;
}
lastPosition.current = scrollerPosition;
if (isLocked.current) {
isLocked.current = false;
return;
}
isLocked.current = true;
scrollbar[scrollProperty] = scrollerPosition;
});
const handleScrollbarScroll = useEventCallback(scrollbar => {
const scroller = scrollerRef.current;
if (!scroller) {
return;
}
if (isLocked.current) {
isLocked.current = false;
return;
}
isLocked.current = true;
scroller[scrollProperty] = scrollbar[scrollProperty];
});
return useRefCallback(scrollbar => {
refSetter(scrollbar);
updateDimensions();
const scroller = scrollerRef.current;
if (!scroller) {
return () => {
refSetter(null);
};
}
const onScrollerScroll = () => handleScrollerScroll(scrollbar);
const onScrollbarScroll = () => handleScrollbarScroll(scrollbar);
const options = {
passive: true
};
scroller.addEventListener('scroll', onScrollerScroll, options);
scrollbar.addEventListener('scroll', onScrollbarScroll, options);
return () => {
scroller.removeEventListener('scroll', onScrollerScroll);
scrollbar.removeEventListener('scroll', onScrollbarScroll);
refSetter(null);
};
});
}
function cssAdd(a, b) {
if (typeof a === 'number' && typeof b === 'number') {
return a + b;
}
return `calc(${valueToCSSString(a)} + ${valueToCSSString(b)})`;
}
function cssMax(a, b) {
if (typeof a === 'number' && typeof b === 'number') {
return Math.max(a, b);
}
return `max(${valueToCSSString(a)}, ${valueToCSSString(b)})`;
}
function valueToCSSString(value) {
if (typeof value === 'string') {
return value;
}
if (typeof value === 'undefined') {
return '0';
}
return `${value}px`;
}