Skip to content

Commit

Permalink
chore: put pagination in separate component and make it optional
Browse files Browse the repository at this point in the history
  • Loading branch information
r-mulder committed May 8, 2024
1 parent 9bc6527 commit 6843bf3
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 134 deletions.
5 changes: 2 additions & 3 deletions packages/libs/react-ui/src/components/Tabs/Tabs.css.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { style, styleVariants } from '@vanilla-extract/css';
import { style } from '@vanilla-extract/css';
import { recipe } from '@vanilla-extract/recipes';
import { token } from '../../styles';
import { atoms } from '../../styles/atoms.css';
Expand Down Expand Up @@ -155,8 +155,7 @@ export const tabContentClass = style([
export const paginationButton = style({
zIndex: 3,
opacity: 1,
transition: 'opacity 0.4s ease',
backgroundColor: token('color.background.base.default'),
transition: 'opacity 0.4s ease, background 0.4s ease',
});

export const hiddenClass = style({
Expand Down
10 changes: 7 additions & 3 deletions packages/libs/react-ui/src/components/Tabs/Tabs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const meta: Meta<ITabsProps> = {
component: Tabs,
decorators: [onLayer2],
parameters: {
status: { type: 'releaseCandidate' },
status: { type: 'beta' },
docs: {
description: {
component:
Expand Down Expand Up @@ -100,9 +100,13 @@ const meta: Meta<ITabsProps> = {
},
inverse: {
control: {
type: 'radio',
type: 'boolean',
},
},
paginated: {
control: {
type: 'boolean',
},
options: ['true', 'false'],
},
},
};
Expand Down
166 changes: 38 additions & 128 deletions packages/libs/react-ui/src/components/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import { MonoArrowBackIosNew, MonoArrowForwardIos } from '@kadena/react-icons';
import classNames from 'classnames';
import type { ReactNode } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef } from 'react';
import type { AriaTabListProps } from 'react-aria';
import { mergeProps, useFocusRing, useTabList } from 'react-aria';
import { Item as TabItem, useTabListState } from 'react-stately';
import { Button } from '../Button';
import { Tab } from './Tab';
import { TabPanel } from './TabPanel';
import {
hiddenClass,
paginationButton,
selectorLine,
tabListClass,
tabListControls,
tabListWrapperClass,
tabsContainerClass,
} from './Tabs.css';
import { TabsPagination } from './TabsPagination';

export { TabItem };

Expand All @@ -26,22 +22,22 @@ export interface ITabsProps
extends Omit<AriaTabListProps<object>, 'orientation' | 'items'> {
className?: string;
inverse?: boolean;
paginated?: boolean;
borderPosition?: 'top' | 'bottom';
}

export const Tabs = ({
className,
borderPosition = 'bottom',
inverse = false,
paginated = false,
...props
}: ITabsProps): ReactNode => {
const state = useTabListState(props);
const containerRef = useRef<HTMLDivElement | null>(null);
const [visibleButtons, setVisibleButtons] = useState({
left: false,
right: false,
});
const scrollRef = useRef<HTMLDivElement | null>(null);
const { focusProps, isFocusVisible } = useFocusRing({

const { focusProps } = useFocusRing({
within: true,
});

Expand All @@ -53,28 +49,6 @@ export const Tabs = ({

const selectedUnderlineRef = useRef<HTMLSpanElement | null>(null);

// on resize determine the button visibility

const determineButtonVisibility = () => {
if (!scrollRef.current || !containerRef.current) return;
const viewWidth = containerRef.current.offsetWidth;
const maxWidth = containerRef.current.scrollWidth;
const scrollPosition = scrollRef.current.scrollLeft;

if (scrollPosition === 0 && visibleButtons.left) {
setVisibleButtons((prev) => ({ ...prev, left: false }));
}
if (scrollPosition > 0) {
setVisibleButtons((prev) => ({ ...prev, left: true }));
}
// 20 is a margin to prevent having a very small last bit to scroll
if (viewWidth + scrollPosition >= maxWidth - 20) {
setVisibleButtons((prev) => ({ ...prev, right: false }));
} else {
setVisibleButtons((prev) => ({ ...prev, right: true }));
}
};

useEffect(() => {
if (!containerRef.current || !scrollRef.current) return;

Expand All @@ -87,7 +61,6 @@ export const Tabs = ({
containerRef.current.offsetWidth
) {
scrollRef.current.scrollLeft = selected.offsetLeft;
determineButtonVisibility();
}
}, []);

Expand Down Expand Up @@ -116,104 +89,41 @@ export const Tabs = ({
);
}, [containerRef, state.selectedItem?.key, selectedUnderlineRef]);

const getMinimalChildWidth = useCallback(() => {
const children = containerRef.current?.children;

if (!children) {
return 0;
}

let minimalWidth = 0;

for (let i = 0; i < children.length; i++) {
const child = children[i] as HTMLElement;

if (child.offsetWidth > minimalWidth) {
minimalWidth = child.offsetWidth;
}
}

return minimalWidth;
}, [containerRef]);

const handlePagination = (direction: 'back' | 'forward') => {
if (!containerRef.current || !scrollRef.current) return;
const maxWidth = containerRef.current?.scrollWidth || 0;
const viewWidth = containerRef.current?.offsetWidth || 0;
const offset = getMinimalChildWidth();
const currentValue = scrollRef.current.scrollLeft;

let nextValue = 0;

if (direction === 'forward') {
nextValue = Math.abs(currentValue + offset);

if (nextValue > maxWidth - viewWidth) {
nextValue = maxWidth - viewWidth;
}

if (nextValue > maxWidth) {
return;
}

scrollRef.current.scrollLeft = nextValue;
determineButtonVisibility();
} else {
nextValue = currentValue - offset;

if (Math.abs(currentValue) < offset) {
nextValue = 0;
}

scrollRef.current.scrollLeft = nextValue;
determineButtonVisibility();
}
};
const tablist = (
<div className={tabListWrapperClass} ref={scrollRef}>
<div
className={tabListClass}
{...mergeProps(tabListProps, focusProps)}
ref={containerRef}
>
{[...state.collection].map((item) => (
<Tab
key={item.key}
item={item}
state={state}
inverse={inverse}
borderPosition={borderPosition}
/>
))}
{borderPosition === 'bottom' && (
<span ref={selectedUnderlineRef} className={selectorLine}></span>
)}
</div>
</div>
);

return (
<div className={classNames(tabsContainerClass, className)}>
<div className={tabListControls}>
<Button
variant="transparent"
className={classNames(paginationButton, {
[hiddenClass]: !visibleButtons.left,
})}
onPress={() => handlePagination('back')}
>
<MonoArrowBackIosNew />
</Button>
<div className={tabListWrapperClass} ref={scrollRef}>
<div
className={classNames(tabListClass, {
pfocusVisible: isFocusVisible,
})}
{...mergeProps(tabListProps, focusProps)}
ref={containerRef}
>
{[...state.collection].map((item) => (
<Tab
key={item.key}
item={item}
state={state}
inverse={props.inverse}
borderPosition={borderPosition}
/>
))}
{borderPosition === 'bottom' && (
<span ref={selectedUnderlineRef} className={selectorLine}></span>
)}
</div>
</div>
<Button
variant="transparent"
className={classNames(paginationButton, {
[hiddenClass]: !visibleButtons.right,
})}
onPress={() => handlePagination('forward')}
{paginated ? (
<TabsPagination
wrapperContainerRef={containerRef}
scrollContainerRef={scrollRef}
>
<MonoArrowForwardIos />
</Button>
</div>
{tablist}
</TabsPagination>
) : (
tablist
)}
<TabPanel key={state.selectedItem?.key} state={state} />
</div>
);
Expand Down
88 changes: 88 additions & 0 deletions packages/libs/react-ui/src/components/Tabs/TabsPagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
MonoArrowBackIosNew,
MonoArrowForwardIos,
} from '@kadena/react-icons/system';
import classNames from 'classnames';
import React, { ReactElement, RefObject, useEffect, useState } from 'react';
import { Button } from '../Button';
import { hiddenClass, paginationButton, tabListControls } from './Tabs.css';
import { calculateScroll } from './utils/calculateScroll';

interface ITabsPaginationProps {
children: ReactElement;
wrapperContainerRef: RefObject<HTMLDivElement>;
scrollContainerRef: RefObject<HTMLDivElement>;
}

export const TabsPagination = ({
children,
wrapperContainerRef,
scrollContainerRef,
}: ITabsPaginationProps) => {
const [visibleButtons, setVisibleButtons] = useState({
left: false,
right: false,
});

const determineButtonVisibility = () => {
if (!scrollContainerRef.current || !wrapperContainerRef.current) return;

const viewWidth = wrapperContainerRef.current.offsetWidth;
const maxWidth = wrapperContainerRef.current.scrollWidth;
const scrollPosition = scrollContainerRef.current.scrollLeft;

if (scrollPosition === 0) {
setVisibleButtons((prev) => ({ ...prev, left: false }));
}
if (scrollPosition > 0) {
setVisibleButtons((prev) => ({ ...prev, left: true }));
}
// 20 is a margin to prevent having a very small last bit to scroll
if (viewWidth + scrollPosition >= maxWidth - 20) {
setVisibleButtons((prev) => ({ ...prev, right: false }));
} else {
setVisibleButtons((prev) => ({ ...prev, right: true }));
}
};

const handlePagination = (direction: 'back' | 'forward') => {
if (!wrapperContainerRef.current || !scrollContainerRef.current) return;

const nextValue = calculateScroll(
direction,
wrapperContainerRef,
scrollContainerRef,
);

scrollContainerRef.current.scrollLeft = nextValue;
determineButtonVisibility();
};

useEffect(() => {
determineButtonVisibility();
}, [scrollContainerRef.current?.scrollLeft]);

return (
<div className={tabListControls}>
<Button
variant="transparent"
className={classNames(paginationButton, {
[hiddenClass]: !visibleButtons.left,
})}
onPress={() => handlePagination('back')}
>
<MonoArrowBackIosNew />
</Button>
{children}
<Button
variant="transparent"
className={classNames(paginationButton, {
[hiddenClass]: !visibleButtons.right,
})}
onPress={() => handlePagination('forward')}
>
<MonoArrowForwardIos />
</Button>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { RefObject } from 'react';
import { getMinimalChildWidth } from './getMinimalChildWidth';

export const calculateScroll = (
direction: 'back' | 'forward',
wrapperContainerRef: RefObject<HTMLDivElement>,
scrollContainerRef: RefObject<HTMLDivElement>,
) => {
const maxWidth = wrapperContainerRef.current?.scrollWidth || 0;
const viewWidth = wrapperContainerRef.current?.offsetWidth || 0;
const offset = getMinimalChildWidth(scrollContainerRef);
const currentValue = scrollContainerRef.current?.scrollLeft || 0;

let nextValue = 0;

if (direction === 'forward') {
nextValue = Math.abs(currentValue + offset);

if (nextValue > maxWidth - viewWidth) {
nextValue = maxWidth - viewWidth;
}

if (nextValue > maxWidth) {
return currentValue;
}
} else {
nextValue = currentValue - offset;

if (Math.abs(currentValue) < offset) {
nextValue = 0;
}
}
return nextValue;
};
Loading

0 comments on commit 6843bf3

Please sign in to comment.