Skip to content
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

fix(tabs): change scrollintoview logic on load #4143

Merged
merged 4 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/cold-eagles-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@twilio-paste/tabs": patch
"@twilio-paste/core": patch
---

[Tabs] fix issue with currently selected item causing vertical scroll
6 changes: 6 additions & 0 deletions .changeset/hot-owls-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@twilio-paste/in-page-navigation": patch
"@twilio-paste/core": patch
---

[InPageNavigation] fix issue with currently selected item causing vertical scroll
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,19 @@ const InPageNavigation = React.forwardRef<HTMLDivElement, InPageNavigationProps>
// 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Box width="size60">
<Box height="1800px" />
<InPageNavigation aria-label={`get started ${useUID()}`}>
<InPageNavigationItem href="#">Super SIM</InPageNavigationItem>
<InPageNavigationItem href="#" title="Programmable Wireless">
Programmable Wireless
</InPageNavigationItem>
<InPageNavigationItem href="#" title="Super Duper SIM">
Super Duper SIM
</InPageNavigationItem>
<InPageNavigationItem href="#" title="Programmable Wirefull">
Programmable Wirefull
</InPageNavigationItem>
<InPageNavigationItem href="#" title="Super SIM">
Super SIM
</InPageNavigationItem>
<InPageNavigationItem href="#" title="Programmable Wireless">
Programmable Wireless
</InPageNavigationItem>
<InPageNavigationItem currentPage={true} href="#" title="Super Duper SIM">
Super Duper SIM
</InPageNavigationItem>
<InPageNavigationItem href="#" title="Programmable Wirefull">
Programmable Wirefull
</InPageNavigationItem>
</InPageNavigation>
</Box>
);
};

export const Customized: StoryFn = () => {
const theme = useTheme();
return (
Expand Down
17 changes: 17 additions & 0 deletions packages/paste-core/components/tabs/src/TabList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const HorizontalTabList: React.FC<React.PropsWithChildren<{ variant?: Variants;
element,
}) => {
const ref = React.useRef<HTMLElement>(null);
const { selectedId } = React.useContext(TabsContext);
// ref to the scrollable element
const scrollableRef = React.useRef<HTMLDivElement>(null);
const isInverse = variant === "inverse" || variant === "inverse_fitted";
Expand All @@ -85,6 +86,22 @@ const HorizontalTabList: React.FC<React.PropsWithChildren<{ variant?: Variants;
}
}, [ref.current, scrollableRef.current]);

React.useEffect(() => {
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 () => {
Expand Down
18 changes: 0 additions & 18 deletions packages/paste-core/components/tabs/src/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export interface TabsProps extends TabPrimitiveInitialState {
const Tabs = React.forwardRef<HTMLDivElement, TabsProps>(
({ 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<string | undefined>(undefined);
const { orientation: tabOrientation = orientation, ...tab } =
state || useTabPrimitiveState({ orientation, ...initialState });
const elementName = getElementName(tabOrientation, "TABS", element);
Expand All @@ -57,23 +56,6 @@ const Tabs = React.forwardRef<HTMLDivElement, TabsProps>(
[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 = <TabsContext.Provider value={value}>{children}</TabsContext.Provider>;

if (tabOrientation === "vertical") {
Expand Down
119 changes: 119 additions & 0 deletions packages/paste-core/components/tabs/stories/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,68 @@ export const HorizontalTabsOverflow = (): JSX.Element => {
);
};

export const HorizontalTabOverflowScrollCheck = (): JSX.Element => {
const selectedId = useUID();
const uniqueBaseID = useUID();
return (
<Box maxWidth="size80">
<Box height="1800px" />
<Tabs selectedId={selectedId} baseId={`${uniqueBaseID}-horizontal-tabs-example`}>
<TabList aria-label="LGBTQ+ Projects">
<Tab>Inside Out</Tab>
<Tab>Transgender District</Tab>
<Tab>Transgender District</Tab>
<Tab>Transgender District</Tab>
<Tab>Transgender District</Tab>
<Tab>Transgender District</Tab>
<Tab id={selectedId}>Transgender District</Tab>
<Tab>Audre Lorde Project</Tab>
<Tab disabled>Coming soon...</Tab>
</TabList>
<TabPanels>
<TabPanel>
<Heading as="h2" variant="heading20">
Inside Out
</Heading>
<Paragraph>
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.
</Paragraph>
<Anchor href="https://insideoutys.org/">Support Inside Out</Anchor>
</TabPanel>
<TabPanel>
<Heading as="h2" variant="heading20">
Transgender District
</Heading>
<Paragraph>
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.
</Paragraph>
<Anchor href="https://www.transgenderdistrictsf.com/">Support The Transgender District</Anchor>
</TabPanel>
<TabPanel>
<Heading as="h2" variant="heading20">
Audre Lorde Project
</Heading>
<Paragraph>
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.
</Paragraph>
<Anchor href="https://alp.org/">Support The Audre Lorde Project</Anchor>
</TabPanel>
</TabPanels>
</Tabs>
</Box>
);
};

export const FittedTabs = (): JSX.Element => {
const selectedId = useUID();
const uniqueBaseID = useUID();
Expand Down Expand Up @@ -236,6 +298,63 @@ export const VerticalTabs = (): JSX.Element => {
);
};

export const VerticalTabsScrollCheck = (): JSX.Element => {
const selectedId = useUID();
const uniqueBaseID = useUID();
return (
<Box>
<Box height="1800px" />
<Tabs orientation="vertical" selectedId={selectedId} baseId={`${uniqueBaseID}-vertical-tabs-example`}>
<TabList aria-label="LGBTQ+ Projects">
<Tab id={selectedId}>Inside Out</Tab>
<Tab>Transgender District</Tab>
<Tab>Audre Lorde Project</Tab>
<Tab disabled>Coming soon...</Tab>
</TabList>
<TabPanels>
<TabPanel>
<Heading as="h2" variant="heading20">
Inside Out
</Heading>
<Paragraph>
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.
</Paragraph>
<Anchor href="https://insideoutys.org/">Support Inside Out</Anchor>
</TabPanel>
<TabPanel>
<Heading as="h2" variant="heading20">
Transgender District
</Heading>
<Paragraph>
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.
</Paragraph>
<Anchor href="https://www.transgenderdistrictsf.com/">Support The Transgender District</Anchor>
</TabPanel>
<TabPanel>
<Heading as="h2" variant="heading20">
Audre Lorde Project
</Heading>
<Paragraph>
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.
</Paragraph>
<Anchor href="https://alp.org/">Support The Audre Lorde Project</Anchor>
</TabPanel>
</TabPanels>
</Tabs>
</Box>
);
};

export const VerticalTabsOverflow = (): JSX.Element => {
const selectedId = useUID();
const uniqueBaseID = useUID();
Expand Down
Loading