From 42aee522d93573a138887010b9e783499b213832 Mon Sep 17 00:00:00 2001 From: atomiks Date: Wed, 2 Oct 2024 14:59:30 +1000 Subject: [PATCH] Support inlay scrollbars --- docs/data/api/scroll-area-corner.json | 3 ++ docs/data/api/scroll-area-root.json | 9 +++- docs/data/api/scroll-area-scrollbar.json | 3 ++ docs/data/api/scroll-area-thumb.json | 3 ++ docs/data/api/scroll-area-viewport.json | 12 ++++- .../ScrollAreaIntroduction/system/index.js | 2 +- .../ScrollAreaIntroduction/system/index.tsx | 2 +- .../scroll-area-root/scroll-area-root.json | 3 +- .../scroll-area-viewport.json | 5 +- docs/next-env.d.ts | 2 +- .../Corner/ScrollAreaCorner.test.tsx | 13 +++++ .../ScrollArea/Root/ScrollAreaRoot.test.tsx | 13 +++++ .../src/ScrollArea/Root/ScrollAreaRoot.tsx | 31 ++++++++---- .../ScrollArea/Root/ScrollAreaRootContext.ts | 1 + .../Scrollbar/ScrollAreaScrollbar.test.tsx | 13 +++++ .../ScrollArea/Thumb/ScrollAreaThumb.test.tsx | 13 +++++ .../Viewport/ScrollAreaViewport.test.tsx | 13 +++++ .../Viewport/ScrollAreaViewport.tsx | 48 ++++++++++++++++--- 18 files changed, 165 insertions(+), 24 deletions(-) create mode 100644 packages/mui-base/src/ScrollArea/Corner/ScrollAreaCorner.test.tsx create mode 100644 packages/mui-base/src/ScrollArea/Root/ScrollAreaRoot.test.tsx create mode 100644 packages/mui-base/src/ScrollArea/Scrollbar/ScrollAreaScrollbar.test.tsx create mode 100644 packages/mui-base/src/ScrollArea/Thumb/ScrollAreaThumb.test.tsx create mode 100644 packages/mui-base/src/ScrollArea/Viewport/ScrollAreaViewport.test.tsx diff --git a/docs/data/api/scroll-area-corner.json b/docs/data/api/scroll-area-corner.json index dd2da4bb49..f8f8337dbd 100644 --- a/docs/data/api/scroll-area-corner.json +++ b/docs/data/api/scroll-area-corner.json @@ -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": "", diff --git a/docs/data/api/scroll-area-root.json b/docs/data/api/scroll-area-root.json index 1b6d7d7847..afa42682ce 100644 --- a/docs/data/api/scroll-area-root.json +++ b/docs/data/api/scroll-area-root.json @@ -1,14 +1,21 @@ { "props": { "className": { "type": { "name": "union", "description": "func
| string" } }, - "render": { "type": { "name": "union", "description": "element
| func" } } + "render": { "type": { "name": "union", "description": "element
| func" } }, + "type": { + "type": { "name": "enum", "description": "'inlay'
| '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": "", diff --git a/docs/data/api/scroll-area-scrollbar.json b/docs/data/api/scroll-area-scrollbar.json index 02b6d0d0af..dd6585e663 100644 --- a/docs/data/api/scroll-area-scrollbar.json +++ b/docs/data/api/scroll-area-scrollbar.json @@ -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": "", diff --git a/docs/data/api/scroll-area-thumb.json b/docs/data/api/scroll-area-thumb.json index bb6c55e172..285c974dd9 100644 --- a/docs/data/api/scroll-area-thumb.json +++ b/docs/data/api/scroll-area-thumb.json @@ -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": "", diff --git a/docs/data/api/scroll-area-viewport.json b/docs/data/api/scroll-area-viewport.json index c54dbb3cc9..b6f0d35843 100644 --- a/docs/data/api/scroll-area-viewport.json +++ b/docs/data/api/scroll-area-viewport.json @@ -1,14 +1,24 @@ { "props": { "className": { "type": { "name": "union", "description": "func
| string" } }, - "render": { "type": { "name": "union", "description": "element
| func" } } + "render": { "type": { "name": "union", "description": "element
| func" } }, + "scrollbarGutter": { + "type": { + "name": "enum", + "description": "'both-edges'
| 'none'
| '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": "", diff --git a/docs/data/components/scroll-area/ScrollAreaIntroduction/system/index.js b/docs/data/components/scroll-area/ScrollAreaIntroduction/system/index.js index dda02d185a..0c678407b6 100644 --- a/docs/data/components/scroll-area/ScrollAreaIntroduction/system/index.js +++ b/docs/data/components/scroll-area/ScrollAreaIntroduction/system/index.js @@ -20,7 +20,7 @@ export default function ScrollAreaIntroduction() { }} > {data.map((value) => ( -
  • +
  • List item {value}
  • ))} diff --git a/docs/data/components/scroll-area/ScrollAreaIntroduction/system/index.tsx b/docs/data/components/scroll-area/ScrollAreaIntroduction/system/index.tsx index dda02d185a..0c678407b6 100644 --- a/docs/data/components/scroll-area/ScrollAreaIntroduction/system/index.tsx +++ b/docs/data/components/scroll-area/ScrollAreaIntroduction/system/index.tsx @@ -20,7 +20,7 @@ export default function ScrollAreaIntroduction() { }} > {data.map((value) => ( -
  • +
  • List item {value}
  • ))} diff --git a/docs/data/translations/api-docs/scroll-area-root/scroll-area-root.json b/docs/data/translations/api-docs/scroll-area-root/scroll-area-root.json index 4bc12cf1e0..859f861cdb 100644 --- a/docs/data/translations/api-docs/scroll-area-root/scroll-area-root.json +++ b/docs/data/translations/api-docs/scroll-area-root/scroll-area-root.json @@ -4,7 +4,8 @@ "className": { "description": "Class names applied to the element or a function that returns them based on the component'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": {} } diff --git a/docs/data/translations/api-docs/scroll-area-viewport/scroll-area-viewport.json b/docs/data/translations/api-docs/scroll-area-viewport/scroll-area-viewport.json index 4bc12cf1e0..79f65576cf 100644 --- a/docs/data/translations/api-docs/scroll-area-viewport/scroll-area-viewport.json +++ b/docs/data/translations/api-docs/scroll-area-viewport/scroll-area-viewport.json @@ -4,7 +4,10 @@ "className": { "description": "Class names applied to the element or a function that returns them based on the component'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 inlay type." + } }, "classDescriptions": {} } diff --git a/docs/next-env.d.ts b/docs/next-env.d.ts index 4f11a03dc6..40c3d68096 100644 --- a/docs/next-env.d.ts +++ b/docs/next-env.d.ts @@ -2,4 +2,4 @@ /// // 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. diff --git a/packages/mui-base/src/ScrollArea/Corner/ScrollAreaCorner.test.tsx b/packages/mui-base/src/ScrollArea/Corner/ScrollAreaCorner.test.tsx new file mode 100644 index 0000000000..4fc6b50738 --- /dev/null +++ b/packages/mui-base/src/ScrollArea/Corner/ScrollAreaCorner.test.tsx @@ -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('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render, + })); +}); diff --git a/packages/mui-base/src/ScrollArea/Root/ScrollAreaRoot.test.tsx b/packages/mui-base/src/ScrollArea/Root/ScrollAreaRoot.test.tsx new file mode 100644 index 0000000000..a2d5c976f3 --- /dev/null +++ b/packages/mui-base/src/ScrollArea/Root/ScrollAreaRoot.test.tsx @@ -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('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render, + })); +}); diff --git a/packages/mui-base/src/ScrollArea/Root/ScrollAreaRoot.tsx b/packages/mui-base/src/ScrollArea/Root/ScrollAreaRoot.tsx index 65dd23c9d7..d67ef1e246 100644 --- a/packages/mui-base/src/ScrollArea/Root/ScrollAreaRoot.tsx +++ b/packages/mui-base/src/ScrollArea/Root/ScrollAreaRoot.tsx @@ -25,17 +25,16 @@ const ScrollAreaRoot = React.forwardRef(function ScrollAreaRoot( props: ScrollAreaRoot.Props, forwardedRef: React.ForwardedRef, ) { - 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(null); - - const scrollbarYRef = React.useRef(null); - const scrollbarXRef = React.useRef(null); - const thumbYRef = React.useRef(null); - const thumbXRef = React.useRef(null); + const viewportRef = React.useRef(null); + const scrollbarYRef = React.useRef(null); + const scrollbarXRef = React.useRef(null); + const thumbYRef = React.useRef(null); + const thumbXRef = React.useRef(null); const thumbDraggingRef = React.useRef(false); const startYRef = React.useRef(0); @@ -151,6 +150,7 @@ const ScrollAreaRoot = React.forwardRef(function ScrollAreaRoot( const contextValue = React.useMemo( () => ({ dir, + type, hovering, setHovering, scrolling, @@ -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 ( @@ -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 */ = { @@ -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 }; diff --git a/packages/mui-base/src/ScrollArea/Root/ScrollAreaRootContext.ts b/packages/mui-base/src/ScrollArea/Root/ScrollAreaRootContext.ts index 367f613190..05a5b5bb12 100644 --- a/packages/mui-base/src/ScrollArea/Root/ScrollAreaRootContext.ts +++ b/packages/mui-base/src/ScrollArea/Root/ScrollAreaRootContext.ts @@ -2,6 +2,7 @@ import * as React from 'react'; export interface ScrollAreaRootContext { dir: string | undefined; + type: 'overlay' | 'inlay'; hovering: boolean; setHovering: React.Dispatch>; scrolling: boolean; diff --git a/packages/mui-base/src/ScrollArea/Scrollbar/ScrollAreaScrollbar.test.tsx b/packages/mui-base/src/ScrollArea/Scrollbar/ScrollAreaScrollbar.test.tsx new file mode 100644 index 0000000000..3df7c13428 --- /dev/null +++ b/packages/mui-base/src/ScrollArea/Scrollbar/ScrollAreaScrollbar.test.tsx @@ -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('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render, + })); +}); diff --git a/packages/mui-base/src/ScrollArea/Thumb/ScrollAreaThumb.test.tsx b/packages/mui-base/src/ScrollArea/Thumb/ScrollAreaThumb.test.tsx new file mode 100644 index 0000000000..cf9fe633db --- /dev/null +++ b/packages/mui-base/src/ScrollArea/Thumb/ScrollAreaThumb.test.tsx @@ -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('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render, + })); +}); diff --git a/packages/mui-base/src/ScrollArea/Viewport/ScrollAreaViewport.test.tsx b/packages/mui-base/src/ScrollArea/Viewport/ScrollAreaViewport.test.tsx new file mode 100644 index 0000000000..b6e7e2f705 --- /dev/null +++ b/packages/mui-base/src/ScrollArea/Viewport/ScrollAreaViewport.test.tsx @@ -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('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render, + })); +}); diff --git a/packages/mui-base/src/ScrollArea/Viewport/ScrollAreaViewport.tsx b/packages/mui-base/src/ScrollArea/Viewport/ScrollAreaViewport.tsx index 327f7f47c5..02cea8a4d4 100644 --- a/packages/mui-base/src/ScrollArea/Viewport/ScrollAreaViewport.tsx +++ b/packages/mui-base/src/ScrollArea/Viewport/ScrollAreaViewport.tsx @@ -25,16 +25,32 @@ const ScrollAreaViewport = React.forwardRef(function ScrollAreaViewport( props: ScrollAreaViewport.Props, forwardedRef: React.ForwardedRef, ) { - 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(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; @@ -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} ), }), @@ -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 */ = { @@ -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 };