Skip to content

Commit

Permalink
fix: extract scrollable to a hook
Browse files Browse the repository at this point in the history
  • Loading branch information
savutsang committed Mar 18, 2024
1 parent 959c6e7 commit 7f25032
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 44 deletions.
55 changes: 12 additions & 43 deletions packages/react/src/components/tabs/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import {
KeyboardEvent,
ReactNode,
RefObject,
useEffect,
useMemo,
useState,
VoidFunctionComponent,
} from 'react';
import styled, { css } from 'styled-components';
import { useScrollable } from '../../hooks/use-scrollable';
import { useTranslation } from '../../i18n/use-translation';
import { getNextElement, getPreviousElement } from '../../utils/array';
import { v4 as uuid } from '../../utils/uuid';
Expand Down Expand Up @@ -132,6 +132,15 @@ export const Tabs: VoidFunctionComponent<Props> = ({
const scrollLeftButtonRef = createRef<HTMLButtonElement>();
const scrollRightButtonRef = createRef<HTMLButtonElement>();

const { scrollToLeft, scrollToRight } = useScrollable({
scrollableElement: tabsListRef,
scrollByPercent: 0.6,
onScroll: ({ atStartX, atEndX }) => {
scrollLeftButtonRef.current?.classList.toggle('hidden', atStartX);
scrollRightButtonRef.current?.classList.toggle('hidden', atEndX);
},
});

const tabItems: TabItem[] = useMemo((): TabItem[] => tabs.map(
(tab, i) => ({
...tab,
Expand All @@ -142,33 +151,6 @@ export const Tabs: VoidFunctionComponent<Props> = ({
), [tabs]);
const [selectedTab, setSelectedTab] = useState(tabItems[0]);

useEffect(() => {
const scrollArea = tabsListRef.current!;

function handleButtonsVisibility(): void {
const scrollX = scrollArea.scrollLeft;
if (scrollLeftButtonRef.current) {
scrollLeftButtonRef.current.classList.toggle('hidden', scrollX === 0);
}
if (scrollRightButtonRef.current) {
const wholeWidth = scrollArea.scrollWidth;
const scrollVisibleWidth = scrollArea.offsetWidth;
const isEndOfScroll = Math.ceil(scrollX) + scrollVisibleWidth >= wholeWidth;
scrollRightButtonRef.current.classList.toggle('hidden', isEndOfScroll);
}
}

handleButtonsVisibility();

const resizeObserver = new ResizeObserver(handleButtonsVisibility);
resizeObserver.observe(scrollArea);
scrollArea.addEventListener('scroll', handleButtonsVisibility);
return () => {
resizeObserver.unobserve(scrollArea);
scrollArea.removeEventListener('scroll', handleButtonsVisibility);
};
}, [tabsListRef, scrollLeftButtonRef, scrollRightButtonRef]);

async function handleTabSelected(tabItem: TabItem): Promise<void> {
if (selectedTab?.onBeforeUnload) {
const isConfirmed = await selectedTab.onBeforeUnload();
Expand Down Expand Up @@ -230,19 +212,6 @@ export const Tabs: VoidFunctionComponent<Props> = ({
}
}

const handleScroll = (dir: 'left' | 'right') => () => {
if (!tabsListRef.current) {
return;
}

const scrollVisibleWidth = tabsListRef.current.offsetWidth;
const moveBy = scrollVisibleWidth * 0.5;
const currentPosX = tabsListRef.current.scrollLeft;
const newPosX = dir === 'left' ? currentPosX - moveBy : currentPosX + moveBy;

tabsListRef.current.scrollTo({ left: newPosX, behavior: 'smooth' });
};

return (
<div className={className}>
<TabButtonsContainer $global={global}>
Expand All @@ -251,7 +220,7 @@ export const Tabs: VoidFunctionComponent<Props> = ({
buttonType="tertiary"
type="button"
aria-hidden="true"
onClick={handleScroll('left')}
onClick={() => scrollToLeft()}
$position="left"
$global={global}
ref={scrollLeftButtonRef}
Expand Down Expand Up @@ -294,7 +263,7 @@ export const Tabs: VoidFunctionComponent<Props> = ({
buttonType="tertiary"
type="button"
aria-hidden="true"
onClick={handleScroll('right')}
onClick={() => scrollToRight()}
$position="right"
$global={global}
ref={scrollRightButtonRef}
Expand Down
68 changes: 68 additions & 0 deletions packages/react/src/hooks/use-scrollable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { RefObject, useEffect } from 'react';

interface UseScrollableOptions {
scrollableElement: RefObject<HTMLElement>;
scrollByPercent: number;
onScroll: (params: { atStartX: boolean; atEndX: boolean; }) => void;
}

interface UseScrollableReturns {
scrollToLeft: () => void;
scrollToRight: () => void;
}

/**
* Support only horizontal scroll for now
*/
export function useScrollable({
scrollableElement,
scrollByPercent,
onScroll,
}: UseScrollableOptions): UseScrollableReturns {
useEffect(() => {
if (!scrollableElement.current) {
return;
}

const scrollArea = scrollableElement.current;

function handleScroll(): void {
const scrollX = scrollArea.scrollLeft;
const wholeWidth = scrollArea.scrollWidth;
const scrollVisibleWidth = scrollArea.offsetWidth;

onScroll({
atStartX: scrollX === 0,
atEndX: Math.ceil(scrollX) + scrollVisibleWidth >= wholeWidth,
});
}

handleScroll();

const resizeObserver = new ResizeObserver(handleScroll);
resizeObserver.observe(scrollArea);
scrollArea.addEventListener('scroll', handleScroll);
return () => {
resizeObserver.unobserve(scrollArea);
scrollArea.removeEventListener('scroll', handleScroll);
};
}, [scrollableElement, onScroll]);

const handleScroll = (dir: 'left' | 'right') => () => {
if (!scrollableElement.current) {
return;
}

const scrollableVisibleWidth = scrollableElement.current.offsetWidth;
const moveBy = scrollableVisibleWidth * scrollByPercent;
const currentPosX = scrollableElement.current.scrollLeft;
const newPosX = dir === 'left' ? currentPosX - moveBy : currentPosX + moveBy;

scrollableElement.current.scrollTo({ left: newPosX, behavior: 'smooth' });
};

return {
scrollToLeft: handleScroll('left'),
scrollToRight: handleScroll('right'),
};
}
2 changes: 1 addition & 1 deletion packages/storybook/stories/tabs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export const Scrollable: Story = () => {
return (
<>
<Tabs tabs={tabs} contained />
<Tabs tabs={tabs} contained global />
<Tabs tabs={tabs} global />
</>
);
};
Expand Down

0 comments on commit 7f25032

Please sign in to comment.