Skip to content

Commit

Permalink
Support inlay scrollbars
Browse files Browse the repository at this point in the history
  • Loading branch information
atomiks committed Oct 2, 2024
1 parent 08566c9 commit 42aee52
Show file tree
Hide file tree
Showing 18 changed files with 165 additions and 24 deletions.
3 changes: 3 additions & 0 deletions docs/data/api/scroll-area-corner.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
"import * as ScrollArea from '@base_ui/react/ScrollArea';\nconst ScrollAreaCorner = ScrollArea.Corner;"
],
"classes": [],
"spread": true,
"themeDefaultProps": true,
"muiName": "ScrollAreaCorner",
"forwardsRefTo": "HTMLDivElement",
"filename": "/packages/mui-base/src/ScrollArea/Corner/ScrollAreaCorner.tsx",
"inheritance": null,
"demos": "<ul><li><a href=\"/components/react-scroll-area/\">Scroll Area</a></li></ul>",
Expand Down
9 changes: 8 additions & 1 deletion docs/data/api/scroll-area-root.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
{
"props": {
"className": { "type": { "name": "union", "description": "func<br>&#124;&nbsp;string" } },
"render": { "type": { "name": "union", "description": "element<br>&#124;&nbsp;func" } }
"render": { "type": { "name": "union", "description": "element<br>&#124;&nbsp;func" } },
"type": {
"type": { "name": "enum", "description": "'inlay'<br>&#124;&nbsp;'overlay'" },
"default": "'overlay'"
}
},
"name": "ScrollAreaRoot",
"imports": [
"import * as ScrollArea from '@base_ui/react/ScrollArea';\nconst ScrollAreaRoot = ScrollArea.Root;"
],
"classes": [],
"spread": true,
"themeDefaultProps": true,
"muiName": "ScrollAreaRoot",
"forwardsRefTo": "HTMLDivElement",
"filename": "/packages/mui-base/src/ScrollArea/Root/ScrollAreaRoot.tsx",
"inheritance": null,
"demos": "<ul><li><a href=\"/components/react-scroll-area/\">Scroll Area</a></li></ul>",
Expand Down
3 changes: 3 additions & 0 deletions docs/data/api/scroll-area-scrollbar.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
"import * as ScrollArea from '@base_ui/react/ScrollArea';\nconst ScrollAreaScrollbar = ScrollArea.Scrollbar;"
],
"classes": [],
"spread": true,
"themeDefaultProps": true,
"muiName": "ScrollAreaScrollbar",
"forwardsRefTo": "HTMLDivElement",
"filename": "/packages/mui-base/src/ScrollArea/Scrollbar/ScrollAreaScrollbar.tsx",
"inheritance": null,
"demos": "<ul><li><a href=\"/components/react-scroll-area/\">Scroll Area</a></li></ul>",
Expand Down
3 changes: 3 additions & 0 deletions docs/data/api/scroll-area-thumb.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
"import * as ScrollArea from '@base_ui/react/ScrollArea';\nconst ScrollAreaThumb = ScrollArea.Thumb;"
],
"classes": [],
"spread": true,
"themeDefaultProps": true,
"muiName": "ScrollAreaThumb",
"forwardsRefTo": "HTMLDivElement",
"filename": "/packages/mui-base/src/ScrollArea/Thumb/ScrollAreaThumb.tsx",
"inheritance": null,
"demos": "<ul><li><a href=\"/components/react-scroll-area/\">Scroll Area</a></li></ul>",
Expand Down
12 changes: 11 additions & 1 deletion docs/data/api/scroll-area-viewport.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
{
"props": {
"className": { "type": { "name": "union", "description": "func<br>&#124;&nbsp;string" } },
"render": { "type": { "name": "union", "description": "element<br>&#124;&nbsp;func" } }
"render": { "type": { "name": "union", "description": "element<br>&#124;&nbsp;func" } },
"scrollbarGutter": {
"type": {
"name": "enum",
"description": "'both-edges'<br>&#124;&nbsp;'none'<br>&#124;&nbsp;'stable'"
},
"default": "'stable'"
}
},
"name": "ScrollAreaViewport",
"imports": [
"import * as ScrollArea from '@base_ui/react/ScrollArea';\nconst ScrollAreaViewport = ScrollArea.Viewport;"
],
"classes": [],
"spread": true,
"themeDefaultProps": true,
"muiName": "ScrollAreaViewport",
"forwardsRefTo": "HTMLDivElement",
"filename": "/packages/mui-base/src/ScrollArea/Viewport/ScrollAreaViewport.tsx",
"inheritance": null,
"demos": "<ul><li><a href=\"/components/react-scroll-area/\">Scroll Area</a></li></ul>",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function ScrollAreaIntroduction() {
}}
>
{data.map((value) => (
<li key={value} style={{ padding: 5, width: 500 }}>
<li key={value} style={{ padding: 5, width: 'auto' }}>
List item {value}
</li>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function ScrollAreaIntroduction() {
}}
>
{data.map((value) => (
<li key={value} style={{ padding: 5, width: 500 }}>
<li key={value} style={{ padding: 5, width: 'auto' }}>
List item {value}
</li>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"className": {
"description": "Class names applied to the element or a function that returns them based on the component&#39;s state."
},
"render": { "description": "A function to customize rendering of the component." }
"render": { "description": "A function to customize rendering of the component." },
"type": { "description": "The type of scrollbars." }
},
"classDescriptions": {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
"className": {
"description": "Class names applied to the element or a function that returns them based on the component&#39;s state."
},
"render": { "description": "A function to customize rendering of the component." }
"render": { "description": "A function to customize rendering of the component." },
"scrollbarGutter": {
"description": "Determines whether to add a scrollbar gutter when using the <code>inlay</code> type."
}
},
"classDescriptions": {}
}
2 changes: 1 addition & 1 deletion docs/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
13 changes: 13 additions & 0 deletions packages/mui-base/src/ScrollArea/Corner/ScrollAreaCorner.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from 'react';
import * as ScrollArea from '@base_ui/react/ScrollArea';
import { createRenderer } from '@mui/internal-test-utils';
import { describeConformance } from '../../../test/describeConformance';

describe('<ScrollArea.Corner />', () => {
const { render } = createRenderer();

describeConformance(<ScrollArea.Corner />, () => ({
refInstanceof: window.HTMLDivElement,
render,
}));
});
13 changes: 13 additions & 0 deletions packages/mui-base/src/ScrollArea/Root/ScrollAreaRoot.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from 'react';
import * as ScrollArea from '@base_ui/react/ScrollArea';
import { createRenderer } from '@mui/internal-test-utils';
import { describeConformance } from '../../../test/describeConformance';

describe('<ScrollArea.Root />', () => {
const { render } = createRenderer();

describeConformance(<ScrollArea.Root />, () => ({
refInstanceof: window.HTMLDivElement,
render,
}));
});
31 changes: 21 additions & 10 deletions packages/mui-base/src/ScrollArea/Root/ScrollAreaRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,16 @@ const ScrollAreaRoot = React.forwardRef(function ScrollAreaRoot(
props: ScrollAreaRoot.Props,
forwardedRef: React.ForwardedRef<HTMLDivElement>,
) {
const { render, className, dir: dirProp, ...otherProps } = props;
const { render, className, dir: dirProp, type = 'overlay', ...otherProps } = props;

const [hovering, setHovering] = React.useState(false);
const [scrolling, setScrolling] = React.useState(false);

const viewportRef = React.useRef<HTMLDivElement>(null);

const scrollbarYRef = React.useRef<HTMLDivElement>(null);
const scrollbarXRef = React.useRef<HTMLDivElement>(null);
const thumbYRef = React.useRef<HTMLDivElement>(null);
const thumbXRef = React.useRef<HTMLDivElement>(null);
const viewportRef = React.useRef<HTMLDivElement | null>(null);
const scrollbarYRef = React.useRef<HTMLDivElement | null>(null);
const scrollbarXRef = React.useRef<HTMLDivElement | null>(null);
const thumbYRef = React.useRef<HTMLDivElement | null>(null);
const thumbXRef = React.useRef<HTMLDivElement | null>(null);

const thumbDraggingRef = React.useRef(false);
const startYRef = React.useRef(0);
Expand Down Expand Up @@ -151,6 +150,7 @@ const ScrollAreaRoot = React.forwardRef(function ScrollAreaRoot(
const contextValue = React.useMemo(
() => ({
dir,
type,
hovering,
setHovering,
scrolling,
Expand All @@ -164,7 +164,7 @@ const ScrollAreaRoot = React.forwardRef(function ScrollAreaRoot(
handlePointerMove,
handlePointerUp,
}),
[dir, hovering, scrolling, handlePointerDown, handlePointerMove, handlePointerUp],
[dir, type, hovering, scrolling, handlePointerDown, handlePointerMove, handlePointerUp],
);

return (
Expand All @@ -175,9 +175,15 @@ const ScrollAreaRoot = React.forwardRef(function ScrollAreaRoot(
});

namespace ScrollAreaRoot {
export interface OwnerState {}
export interface Props extends BaseUIComponentProps<'div', OwnerState> {
/**
* The type of scrollbars.
* @default 'overlay'
*/
type?: 'overlay' | 'inlay';
}

export interface Props extends BaseUIComponentProps<'div', OwnerState> {}
export interface OwnerState {}
}

ScrollAreaRoot.propTypes /* remove-proptypes */ = {
Expand All @@ -201,6 +207,11 @@ ScrollAreaRoot.propTypes /* remove-proptypes */ = {
* A function to customize rendering of the component.
*/
render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
/**
* The type of scrollbars.
* @default 'overlay'
*/
type: PropTypes.oneOf(['inlay', 'overlay']),
} as any;

export { ScrollAreaRoot };
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';

export interface ScrollAreaRootContext {
dir: string | undefined;
type: 'overlay' | 'inlay';
hovering: boolean;
setHovering: React.Dispatch<React.SetStateAction<boolean>>;
scrolling: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from 'react';
import * as ScrollArea from '@base_ui/react/ScrollArea';
import { createRenderer } from '@mui/internal-test-utils';
import { describeConformance } from '../../../test/describeConformance';

describe('<ScrollArea.Scrollbar />', () => {
const { render } = createRenderer();

describeConformance(<ScrollArea.Scrollbar />, () => ({
refInstanceof: window.HTMLDivElement,
render,
}));
});
13 changes: 13 additions & 0 deletions packages/mui-base/src/ScrollArea/Thumb/ScrollAreaThumb.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from 'react';
import * as ScrollArea from '@base_ui/react/ScrollArea';
import { createRenderer } from '@mui/internal-test-utils';
import { describeConformance } from '../../../test/describeConformance';

describe('<ScrollArea.Thumb />', () => {
const { render } = createRenderer();

describeConformance(<ScrollArea.Thumb />, () => ({
refInstanceof: window.HTMLDivElement,
render,
}));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from 'react';
import * as ScrollArea from '@base_ui/react/ScrollArea';
import { createRenderer } from '@mui/internal-test-utils';
import { describeConformance } from '../../../test/describeConformance';

describe('<ScrollArea.Viewport />', () => {
const { render } = createRenderer();

describeConformance(<ScrollArea.Viewport />, () => ({
refInstanceof: window.HTMLDivElement,
render,
}));
});
48 changes: 41 additions & 7 deletions packages/mui-base/src/ScrollArea/Viewport/ScrollAreaViewport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,32 @@ const ScrollAreaViewport = React.forwardRef(function ScrollAreaViewport(
props: ScrollAreaViewport.Props,
forwardedRef: React.ForwardedRef<HTMLDivElement>,
) {
const { render, className, children, ...otherProps } = props;

const { viewportRef, scrollbarYRef, scrollbarXRef, thumbYRef, thumbXRef, setScrolling } =
useScrollAreaRootContext();
const { render, className, children, scrollbarGutter = 'stable', ...otherProps } = props;

const {
type,
viewportRef,
scrollbarYRef,
scrollbarXRef,
thumbYRef,
thumbXRef,
setScrolling,
dir,
} = useScrollAreaRootContext();

const timeoutRef = React.useRef(-1);

const mergedRef = useForkRef(forwardedRef, viewportRef);
const tableWrapperRef = React.useRef<HTMLDivElement | null>(null);

const [paddingX, setPaddingX] = React.useState(0);

useEnhancedEffect(() => {
if (scrollbarYRef.current) {
setPaddingX(parseFloat(getComputedStyle(scrollbarYRef.current).width));
}
}, [scrollbarYRef, scrollbarXRef]);

const computeThumb = useEventCallback(() => {
const viewportEl = viewportRef.current;
const scrollbarYEl = scrollbarYRef.current;
Expand Down Expand Up @@ -149,9 +165,16 @@ const ScrollAreaViewport = React.forwardRef(function ScrollAreaViewport(
style={{
minWidth: '100%',
display: 'table',
...(type === 'inlay' &&
scrollbarGutter !== 'none' && {
[dir === 'rtl' ? 'paddingLeft' : 'paddingRight']: paddingX,
...(scrollbarGutter === 'both-edges' && {
[dir === 'rtl' ? 'paddingRight' : 'paddingLeft']: paddingX,
}),
}),
}}
>
{props.children}
{children}
</div>
),
}),
Expand All @@ -161,9 +184,15 @@ const ScrollAreaViewport = React.forwardRef(function ScrollAreaViewport(
});

namespace ScrollAreaViewport {
export interface OwnerState {}
export interface Props extends BaseUIComponentProps<'div', OwnerState> {
/**
* Determines whether to add a scrollbar gutter when using the `inlay` type.
* @default 'stable'
*/
scrollbarGutter?: 'none' | 'stable' | 'both-edges';
}

export interface Props extends BaseUIComponentProps<'div', OwnerState> {}
export interface OwnerState {}
}

ScrollAreaViewport.propTypes /* remove-proptypes */ = {
Expand All @@ -183,6 +212,11 @@ ScrollAreaViewport.propTypes /* remove-proptypes */ = {
* A function to customize rendering of the component.
*/
render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
/**
* Determines whether to add a scrollbar gutter when using the `inlay` type.
* @default 'stable'
*/
scrollbarGutter: PropTypes.oneOf(['both-edges', 'none', 'stable']),
} as any;

export { ScrollAreaViewport };

0 comments on commit 42aee52

Please sign in to comment.