245 lines
9.5 KiB
JavaScript
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`;
|
|
} |