-
Notifications
You must be signed in to change notification settings - Fork 6
feat: migrate base-ui to @base-ui/react and simplify scroll masks #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,19 +2,12 @@ | |||||
|
|
||||||
| import * as React from "react"; | ||||||
|
|
||||||
| import { ScrollArea as ScrollAreaPrimitive } from "@base-ui-components/react/scroll-area"; | ||||||
| import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"; | ||||||
|
|
||||||
| import { cn } from "@/lib/utils"; | ||||||
|
|
||||||
| import { useTouchPrimary } from "@/hooks/use-has-primary-touch"; | ||||||
|
|
||||||
| type Mask = { | ||||||
| top: boolean; | ||||||
| bottom: boolean; | ||||||
| left: boolean; | ||||||
| right: boolean; | ||||||
| }; | ||||||
|
|
||||||
| export type ScrollAreaContextProps = { | ||||||
| isTouch: boolean; | ||||||
| type: "auto" | "always" | "scroll" | "hover"; | ||||||
|
|
@@ -39,54 +32,9 @@ const ScrollArea = React.forwardRef< | |||||
| maskClassName?: string; | ||||||
| } | ||||||
| >(({ className, children, type = "hover", maskHeight = 30, maskClassName, viewportClassName, ...props }, ref) => { | ||||||
| const [showMask, setShowMask] = React.useState<Mask>({ | ||||||
| top: false, | ||||||
| bottom: false, | ||||||
| left: false, | ||||||
| right: false, | ||||||
| }); | ||||||
|
|
||||||
| const viewportRef = React.useRef<HTMLDivElement>(null); | ||||||
| const isTouch = useTouchPrimary(); | ||||||
|
|
||||||
| const checkScrollability = React.useCallback(() => { | ||||||
| const element = viewportRef.current; | ||||||
| if (!element) return; | ||||||
|
|
||||||
| const { scrollTop, scrollLeft, scrollWidth, clientWidth, scrollHeight, clientHeight } = element; | ||||||
| setShowMask((prev) => ({ | ||||||
| ...prev, | ||||||
| top: scrollTop > 0, | ||||||
| bottom: scrollTop + clientHeight < scrollHeight - 1, | ||||||
| left: scrollLeft > 0, | ||||||
| right: scrollLeft + clientWidth < scrollWidth - 1, | ||||||
| })); | ||||||
| }, []); | ||||||
|
|
||||||
| React.useEffect(() => { | ||||||
| if (typeof window === "undefined") return; | ||||||
|
|
||||||
| const element = viewportRef.current; | ||||||
| if (!element) return; | ||||||
|
|
||||||
| const controller = new AbortController(); | ||||||
| const { signal } = controller; | ||||||
|
|
||||||
| const resizeObserver = new ResizeObserver(checkScrollability); | ||||||
| resizeObserver.observe(element); | ||||||
|
|
||||||
| element.addEventListener("scroll", checkScrollability, { signal }); | ||||||
| window.addEventListener("resize", checkScrollability, { signal }); | ||||||
|
|
||||||
| // Run an initial check whenever dependencies change (including pointer mode) | ||||||
| checkScrollability(); | ||||||
|
|
||||||
| return () => { | ||||||
| controller.abort(); | ||||||
| resizeObserver.disconnect(); | ||||||
| }; | ||||||
| }, [checkScrollability, isTouch]); | ||||||
|
|
||||||
| return ( | ||||||
| <ScrollAreaContext.Provider value={{ isTouch, type }}> | ||||||
| {isTouch ? ( | ||||||
|
|
@@ -96,18 +44,18 @@ const ScrollArea = React.forwardRef< | |||||
| role="group" | ||||||
| data-slot="scroll-area" | ||||||
| aria-roledescription="scroll area" | ||||||
| className={cn("relative overflow-hidden", className)} | ||||||
| className={cn("group relative overflow-hidden", className)} | ||||||
| > | ||||||
| <div ref={viewportRef} className={cn("size-full overflow-auto", viewportClassName)} tabIndex={0}> | ||||||
| {children} | ||||||
| </div> | ||||||
| {maskHeight > 0 && <ScrollMask showMask={showMask} className={maskClassName} maskHeight={maskHeight} />} | ||||||
| {maskHeight > 0 && <ScrollMask className={maskClassName} size={maskHeight} />} | ||||||
| </div> | ||||||
| ) : ( | ||||||
| <ScrollAreaPrimitive.Root | ||||||
| ref={ref} | ||||||
| data-slot="scroll-area" | ||||||
| className={cn("relative overflow-hidden", viewportClassName, className)} | ||||||
| className={cn("group relative overflow-hidden", viewportClassName, className)} | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove incorrectly placed The 🔎 Proposed fix- className={cn("group relative overflow-hidden", viewportClassName, className)}
+ className={cn("group relative overflow-hidden", className)}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| {...props} | ||||||
| > | ||||||
| <ScrollAreaPrimitive.Viewport | ||||||
|
|
@@ -119,7 +67,7 @@ const ScrollArea = React.forwardRef< | |||||
| </ScrollAreaPrimitive.Viewport> | ||||||
| <ScrollBar /> | ||||||
| <ScrollAreaPrimitive.Corner /> | ||||||
| {maskHeight > 0 && <ScrollMask showMask={showMask} className={maskClassName} maskHeight={maskHeight} />} | ||||||
| {maskHeight > 0 && <ScrollMask className={maskClassName} size={maskHeight} />} | ||||||
| </ScrollAreaPrimitive.Root> | ||||||
| )} | ||||||
| </ScrollAreaContext.Provider> | ||||||
|
|
@@ -166,13 +114,11 @@ const ScrollBar = React.forwardRef< | |||||
| ScrollBar.displayName = ScrollAreaPrimitive.Scrollbar.displayName; | ||||||
|
|
||||||
| const ScrollMask = ({ | ||||||
| showMask, | ||||||
| maskHeight, | ||||||
| size, | ||||||
| className, | ||||||
| ...props | ||||||
| }: React.ComponentProps<"div"> & { | ||||||
| showMask: Mask; | ||||||
| maskHeight: number; | ||||||
| size: number; | ||||||
| }) => { | ||||||
| return ( | ||||||
| <> | ||||||
|
|
@@ -181,17 +127,17 @@ const ScrollMask = ({ | |||||
| aria-hidden="true" | ||||||
| style={ | ||||||
| { | ||||||
| "--top-fade-height": showMask.top ? `${maskHeight}px` : "0px", | ||||||
| "--bottom-fade-height": showMask.bottom ? `${maskHeight}px` : "0px", | ||||||
| "--vertical-mask-height": `${size}px`, | ||||||
| } as React.CSSProperties | ||||||
| } | ||||||
| className={cn( | ||||||
| "pointer-events-none absolute inset-0 z-10", | ||||||
| "before:absolute before:inset-x-0 before:top-0 before:transition-[height,opacity] before:duration-300 before:content-['']", | ||||||
| "after:absolute after:inset-x-0 after:bottom-0 after:transition-[height,opacity] after:duration-300 after:content-['']", | ||||||
| "before:h-(--top-fade-height) after:h-(--bottom-fade-height)", | ||||||
| showMask.top ? "before:opacity-100" : "before:opacity-0", | ||||||
| showMask.bottom ? "after:opacity-100" : "after:opacity-0", | ||||||
| "before:h-0 group-data-overflow-y-start:before:h-(--vertical-mask-height)", | ||||||
| "after:h-0 group-data-overflow-y-end:after:h-(--vertical-mask-height)", | ||||||
| "before:opacity-0 group-data-overflow-y-start:before:opacity-100", | ||||||
| "after:opacity-0 group-data-overflow-y-end:after:opacity-100", | ||||||
| "before:from-background before:bg-gradient-to-b before:to-transparent", | ||||||
| "after:from-background after:bg-gradient-to-t after:to-transparent", | ||||||
| className | ||||||
|
|
@@ -202,17 +148,17 @@ const ScrollMask = ({ | |||||
| aria-hidden="true" | ||||||
| style={ | ||||||
| { | ||||||
| "--left-fade-width": showMask.left ? `${maskHeight}px` : "0px", | ||||||
| "--right-fade-width": showMask.right ? `${maskHeight}px` : "0px", | ||||||
| "--horizontal-mask-width": `${size}px`, | ||||||
| } as React.CSSProperties | ||||||
| } | ||||||
| className={cn( | ||||||
| "pointer-events-none absolute inset-0 z-10", | ||||||
| "before:absolute before:inset-y-0 before:left-0 before:transition-[width,opacity] before:duration-300 before:content-['']", | ||||||
| "after:absolute after:inset-y-0 after:right-0 after:transition-[width,opacity] after:duration-300 after:content-['']", | ||||||
| "before:w-(--left-fade-width) after:w-(--right-fade-width)", | ||||||
| showMask.left ? "before:opacity-100" : "before:opacity-0", | ||||||
| showMask.right ? "after:opacity-100" : "after:opacity-0", | ||||||
| "before:w-0 group-data-overflow-x-start:before:w-(--horizontal-mask-width)", | ||||||
| "after:w-0 group-data-overflow-x-end:after:w-(--horizontal-mask-width)", | ||||||
| "before:opacity-0 group-data-overflow-x-start:before:opacity-100", | ||||||
| "after:opacity-0 group-data-overflow-x-end:after:opacity-100", | ||||||
| "before:from-background before:bg-gradient-to-r before:to-transparent", | ||||||
| "after:from-background after:bg-gradient-to-l after:to-transparent", | ||||||
| className | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
@base-ui/react version 1.0.0 breaking changes from @base-ui-components/react beta💡 Result:
Quick summary of breaking changes to expect when moving from the @base-ui-components/react beta series to @base-ui/react v1.0.0:
If you want, tell me which components/props you use and I’ll extract the exact migration steps and code diffs for them.
Sources: Base UI changelogs and release notes. [1] Base UI releases (v1.0.0 and changelog). [2] @base-ui-components/react beta changelogs (beta.3 / beta.0). [3] v1.0.0-beta.5 release notes.
🌐 Web query:
@base-ui/react Tabs API documentation stable release💡 Result:
The stable Tabs API docs are on the official Base UI site: https://base-ui.com/react/components/tabs. See the component API reference there. [1][2]
Sources:
🌐 Web query:
@base-ui/react migration guide from beta to 1.0.0💡 Result:
Summary migration checklist — beta → v1.0.0
Useful links
If you want, tell me which Base UI components you use and I’ll produce a focused diff/patch list to update your code.
Update code to handle @base-ui/react v1.0.0 breaking changes.
The package migration from @base-ui-components/react to @base-ui/react v1.0.0 includes significant breaking changes beyond the import path update:
eventDetailsobject instead of separateeventandreasonparameters. Update any Tabs event handlers (e.g.,onOpenChange(open, eventDetails)instead ofonOpenChange(open, event, reason)).loop→loopFocus,trackAnchor→disableAnchorTracking(with inverted boolean semantics).Review the official Base UI v1.0.0 migration guide and Tabs API documentation for the complete list of changes and update all Tabs usage accordingly.
🤖 Prompt for AI Agents