-
Notifications
You must be signed in to change notification settings - Fork 4.2k
feat: scrollable tabs list #38855
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
feat: scrollable tabs list #38855
Changes from 1 commit
c9a9d8a
aca2541
a2d7849
777b8c7
1a43e3f
6d3e7ea
7c4a5be
d299e5b
3d320f1
2bec5e8
2498569
23c8fbe
b676070
2e48d37
5f44034
a011e73
cf0324e
e4cbb72
2e82109
3c6c283
2007e08
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import React, { useEffect, useRef, useState } from "react"; | ||
import { noop } from "lodash"; | ||
import { usePrevious } from "@mantine/hooks"; | ||
|
||
import { ScrollArea } from "../ScrollArea"; | ||
|
||
|
@@ -23,37 +24,66 @@ export const DismissibleTabBar = ({ | |
disableAdd = false, | ||
onTabAdd, | ||
}: DismissibleTabBarProps) => { | ||
const [isStuck, setIsStuck] = useState(false); | ||
const [isLeftStuck, setIsLeftStuck] = useState(false); | ||
const [isRightStuck, setIsRightStuck] = useState(false); | ||
|
||
const containerRef = useRef<HTMLDivElement | null>(null); | ||
const sentinelRef = useRef<HTMLDivElement | null>(null); | ||
const sentinelLeftRef = useRef<HTMLDivElement | null>(null); | ||
const sentinelRightRef = useRef<HTMLDivElement | null>(null); | ||
|
||
const totalChildren = React.Children.count(children); | ||
const prevTotalChildren = usePrevious(totalChildren) ?? totalChildren; | ||
|
||
const handleAdd = disableAdd ? noop : onTabAdd; | ||
|
||
useEffect(function observeSticky() { | ||
if (!containerRef.current || !sentinelRef.current) return; | ||
if ( | ||
!containerRef.current || | ||
!sentinelLeftRef.current || | ||
!sentinelRightRef.current | ||
) | ||
return; | ||
|
||
const observer = new IntersectionObserver( | ||
([entry]) => { | ||
if (entry.isIntersecting) { | ||
setIsStuck(false); | ||
} else { | ||
setIsStuck(true); | ||
} | ||
(entries) => { | ||
entries.forEach((entry) => { | ||
if (entry.target === sentinelLeftRef.current) { | ||
setIsLeftStuck(!entry.isIntersecting); | ||
} | ||
|
||
if (entry.target === sentinelRightRef.current) { | ||
setIsRightStuck(!entry.isIntersecting); | ||
} | ||
}); | ||
}, | ||
{ | ||
root: containerRef.current, | ||
threshold: 1.0, | ||
}, | ||
); | ||
|
||
observer.observe(sentinelRef.current); | ||
observer.observe(sentinelLeftRef.current); | ||
observer.observe(sentinelRightRef.current); | ||
|
||
return () => observer.disconnect(); | ||
}, []); | ||
|
||
useEffect( | ||
function scrollAddedTabIntoView() { | ||
const element = sentinelRightRef.current; | ||
|
||
if (element && totalChildren > prevTotalChildren) { | ||
element.scrollIntoView({ | ||
inline: "center", | ||
behavior: "smooth", | ||
}); | ||
} | ||
}, | ||
[totalChildren, prevTotalChildren], | ||
); | ||
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. This is not the same as activeTab. A previously added tab, could get active outside of a tabs selection and that would be needed a scroll into view 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. Yes, it's not meant to be active, we only need to scroll to a newly added tab, which will be marked active. 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. So the consumer handles the scroll to an active tab that is not new? 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. Refactored. |
||
|
||
return ( | ||
<Styled.Root> | ||
<Styled.Root $showLeftBorder={isLeftStuck}> | ||
<ScrollArea | ||
data-testid="t--editor-tabs" | ||
options={SCROLL_AREA_OPTIONS} | ||
|
@@ -62,13 +92,14 @@ export const DismissibleTabBar = ({ | |
style={SCROLL_AREA_STYLE} | ||
> | ||
<Styled.TabsContainer data-testid="t--tabs-container" role="tablist"> | ||
<Styled.StickySentinel ref={sentinelLeftRef} /> | ||
ankitakinger marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{children} | ||
<Styled.StickySentinel ref={sentinelRef} /> | ||
<Styled.StickySentinel ref={sentinelRightRef} /> | ||
</Styled.TabsContainer> | ||
</ScrollArea> | ||
<Styled.PlusButtonContainer $isStuck={isStuck}> | ||
<Styled.PlusButtonContainer $showLeftBorder={isRightStuck}> | ||
<Styled.PlusButton | ||
disabled={disableAdd} | ||
isDisabled={disableAdd} | ||
isIconButton | ||
kind="tertiary" | ||
onClick={handleAdd} | ||
|
Uh oh!
There was an error while loading. Please reload this page.