diff --git a/.changeset/cold-eagles-search.md b/.changeset/cold-eagles-search.md new file mode 100644 index 0000000000..7decd0fc5b --- /dev/null +++ b/.changeset/cold-eagles-search.md @@ -0,0 +1,6 @@ +--- +"@twilio-paste/tabs": patch +"@twilio-paste/core": patch +--- + +[Tabs] fix issue with currently selected item causing vertical scroll diff --git a/.changeset/hot-owls-dream.md b/.changeset/hot-owls-dream.md new file mode 100644 index 0000000000..f2502d4cc6 --- /dev/null +++ b/.changeset/hot-owls-dream.md @@ -0,0 +1,6 @@ +--- +"@twilio-paste/in-page-navigation": patch +"@twilio-paste/core": patch +--- + +[InPageNavigation] fix issue with currently selected item causing vertical scroll diff --git a/packages/paste-core/components/in-page-navigation/src/InPageNavigation.tsx b/packages/paste-core/components/in-page-navigation/src/InPageNavigation.tsx index 5d668bce9f..6edea06d89 100644 --- a/packages/paste-core/components/in-page-navigation/src/InPageNavigation.tsx +++ b/packages/paste-core/components/in-page-navigation/src/InPageNavigation.tsx @@ -106,15 +106,19 @@ const InPageNavigation = React.forwardRef // Scroll to the selected tab if it exists on mount React.useEffect(() => { if (listRef.current && scrollableRef.current) { - setTimeout( - () => - listRef.current - ?.querySelector(`[aria-current="page"]`) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - behavior is typed incorrectly in Typescript v4, fixed in v5+ - ?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" }), - 1, - ); + const currentSelectedTab = listRef.current.querySelector(`[aria-current="page"]`); + const scrollableWidth = scrollableRef.current.getBoundingClientRect().width; + + if ( + currentSelectedTab && + (currentSelectedTab?.getBoundingClientRect().x < 0 || + currentSelectedTab?.getBoundingClientRect().right > scrollableWidth) + ) { + const scrollLeft = + currentSelectedTab.getBoundingClientRect().x - scrollableRef.current.getBoundingClientRect().x; + scrollableRef.current.scrollLeft += scrollLeft; + } + scrollableRef.current?.addEventListener("scroll", handleScrollEvent); window.addEventListener("resize", handleScrollEvent); determineElementsOutOfBounds(scrollableRef.current, listRef.current); diff --git a/packages/paste-core/components/in-page-navigation/stories/index.stories.tsx b/packages/paste-core/components/in-page-navigation/stories/index.stories.tsx index 67900c82d5..5a88c79411 100644 --- a/packages/paste-core/components/in-page-navigation/stories/index.stories.tsx +++ b/packages/paste-core/components/in-page-navigation/stories/index.stories.tsx @@ -112,6 +112,39 @@ export const LinkOverflowExample: StoryFn = () => { ); }; +export const LinkOverflowExampleScrollTest: StoryFn = () => { + /* using UID here to make unique labels for landmarks in Storybook for axe testing */ + return ( + + + + Super SIM + + Programmable Wireless + + + Super Duper SIM + + + Programmable Wirefull + + + Super SIM + + + Programmable Wireless + + + Super Duper SIM + + + Programmable Wirefull + + + + ); +}; + export const Customized: StoryFn = () => { const theme = useTheme(); return ( diff --git a/packages/paste-core/components/tabs/src/TabList.tsx b/packages/paste-core/components/tabs/src/TabList.tsx index 1855c9568c..3f577ffde4 100644 --- a/packages/paste-core/components/tabs/src/TabList.tsx +++ b/packages/paste-core/components/tabs/src/TabList.tsx @@ -65,6 +65,7 @@ const HorizontalTabList: React.FC { const ref = React.useRef(null); + const { selectedId } = React.useContext(TabsContext); // ref to the scrollable element const scrollableRef = React.useRef(null); const isInverse = variant === "inverse" || variant === "inverse_fitted"; @@ -85,6 +86,22 @@ const HorizontalTabList: React.FC { + if (scrollableRef.current && selectedId) { + // eslint-disable-next-line unicorn/prefer-query-selector + const selectedTabEl = document.getElementById(selectedId); + const scrollableWidth = scrollableRef.current.getBoundingClientRect().width; + + if ( + selectedTabEl && + (selectedTabEl?.getBoundingClientRect().x < 0 || selectedTabEl?.getBoundingClientRect().right > scrollableWidth) + ) { + const scrollLeft = selectedTabEl.getBoundingClientRect().x - scrollableRef.current.getBoundingClientRect().x; + scrollableRef.current.scrollLeft += scrollLeft; + } + } + }, [scrollableRef.current, selectedId]); + // Cleanup event listeners on destroy React.useEffect(() => { return () => { diff --git a/packages/paste-core/components/tabs/src/Tabs.tsx b/packages/paste-core/components/tabs/src/Tabs.tsx index 464b053d88..b8793ae193 100644 --- a/packages/paste-core/components/tabs/src/Tabs.tsx +++ b/packages/paste-core/components/tabs/src/Tabs.tsx @@ -48,7 +48,6 @@ export interface TabsProps extends TabPrimitiveInitialState { const Tabs = React.forwardRef( ({ children, element, orientation = "horizontal", state, variant, ...initialState }, ref) => { // If returned state from primitive has orientation set to undefined, use the default "horizontal" - const [prevSelectedTab, setPrevSelectedTab] = React.useState(undefined); const { orientation: tabOrientation = orientation, ...tab } = state || useTabPrimitiveState({ orientation, ...initialState }); const elementName = getElementName(tabOrientation, "TABS", element); @@ -57,23 +56,6 @@ const Tabs = React.forwardRef( [tab, tabOrientation, variant], ); - const { selectedId } = tab; - // Scroll to the selected tab if it exists on mount - React.useEffect(() => { - if (typeof selectedId === "string") { - setTimeout(() => { - document - // eslint-disable-next-line unicorn/prefer-query-selector - .getElementById(selectedId) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - behavior is typed incorrectly in Typescript v4, fixed in v5+ - ?.scrollIntoView({ behavior: prevSelectedTab === undefined ? "instant" : "smooth" }); - - setPrevSelectedTab(selectedId); - }, 1); - } - }, [prevSelectedTab, selectedId]); - const returnValue = {children}; if (tabOrientation === "vertical") { diff --git a/packages/paste-core/components/tabs/stories/index.stories.tsx b/packages/paste-core/components/tabs/stories/index.stories.tsx index e2ce3b8972..f323bbe0e9 100644 --- a/packages/paste-core/components/tabs/stories/index.stories.tsx +++ b/packages/paste-core/components/tabs/stories/index.stories.tsx @@ -128,6 +128,68 @@ export const HorizontalTabsOverflow = (): JSX.Element => { ); }; +export const HorizontalTabOverflowScrollCheck = (): JSX.Element => { + const selectedId = useUID(); + const uniqueBaseID = useUID(); + return ( + + + + + Inside Out + Transgender District + Transgender District + Transgender District + Transgender District + Transgender District + Transgender District + Audre Lorde Project + Coming soon... + + + + + Inside Out + + + Inside Out empowers, educates, and advocates for LGBTQ+ of youth from the Pikes Peak Region in Southern + Colorado. Inside Out does this by creating safe spaces, support systems and teaching life skills to all + youth in the community and working to make the community safer and more accepting of gender and sexual + orientation diversity. + + Support Inside Out + + + + Transgender District + + + The mission of the Transgender District is to create an urban environment that fosters the rich history, + culture, legacy, and empowerment of transgender people and its deep roots in the southeastern Tenderloin + neighborhood. The transgender district aims to stabilize and economically empower the transgender + community through ownership of homes, businesses, historic and cultural sites, and safe community spaces. + + Support The Transgender District + + + + Audre Lorde Project + + + The Audre Lorde Project is a Lesbian, Gay, Bisexual, Two Spirit, Trans and Gender Non Conforming People of + Color center for community organizing, focusing on the New York City area. Through mobilization, education + and capacity-building, they work for community wellness and progressive social and economic justice. + Committed to struggling across differences, they seek to responsibly reflect, represent and serve their + various communities. + + Support The Audre Lorde Project + + + + + ); +}; + export const FittedTabs = (): JSX.Element => { const selectedId = useUID(); const uniqueBaseID = useUID(); @@ -236,6 +298,63 @@ export const VerticalTabs = (): JSX.Element => { ); }; +export const VerticalTabsScrollCheck = (): JSX.Element => { + const selectedId = useUID(); + const uniqueBaseID = useUID(); + return ( + + + + + Inside Out + Transgender District + Audre Lorde Project + Coming soon... + + + + + Inside Out + + + Inside Out empowers, educates, and advocates for LGBTQ+ of youth from the Pikes Peak Region in Southern + Colorado. Inside Out does this by creating safe spaces, support systems and teaching life skills to all + youth in the community and working to make the community safer and more accepting of gender and sexual + orientation diversity. + + Support Inside Out + + + + Transgender District + + + The mission of the Transgender District is to create an urban environment that fosters the rich history, + culture, legacy, and empowerment of transgender people and its deep roots in the southeastern Tenderloin + neighborhood. The transgender district aims to stabilize and economically empower the transgender + community through ownership of homes, businesses, historic and cultural sites, and safe community spaces. + + Support The Transgender District + + + + Audre Lorde Project + + + The Audre Lorde Project is a Lesbian, Gay, Bisexual, Two Spirit, Trans and Gender Non Conforming People of + Color center for community organizing, focusing on the New York City area. Through mobilization, education + and capacity-building, they work for community wellness and progressive social and economic justice. + Committed to struggling across differences, they seek to responsibly reflect, represent and serve their + various communities. + + Support The Audre Lorde Project + + + + + ); +}; + export const VerticalTabsOverflow = (): JSX.Element => { const selectedId = useUID(); const uniqueBaseID = useUID();