Skip to content

Commit

Permalink
feat: wip
Browse files Browse the repository at this point in the history
  • Loading branch information
QuentinLeCaignec committed Oct 11, 2023
1 parent d644c7c commit d1d960f
Show file tree
Hide file tree
Showing 6 changed files with 325 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@ import { CaretDown, CaretUp } from '@phosphor-icons/react';
import { useId, useState } from 'react';

export interface IDropdownButtonProps extends MenuProps {
/** Override default menu target with custom component*/
buttonComponent?: ReactNode;
children?: ReactNode;
label: string;
label?: string;
}

/** Additional props will be forwarded to the [Mantine Menu component](https://mantine.dev/core/menu) */
export function DropdownButton(props: IDropdownButtonProps): ReactElement {
const { children, label, position = 'bottom-start', ...menuProps } = props;
const {
buttonComponent,
children,
label,
position = 'bottom-start',
...menuProps
} = props;
const [opened, setOpened] = useState(false);
const id = useId();

Expand All @@ -27,12 +35,16 @@ export function DropdownButton(props: IDropdownButtonProps): ReactElement {
{...menuProps}
>
<Menu.Target>
<Button
data-testid="button"
rightIcon={opened ? <CaretUp /> : <CaretDown />}
>
{label}
</Button>
{buttonComponent ? (
buttonComponent
) : (
<Button
data-testid="button"
rightIcon={opened ? <CaretUp /> : <CaretDown />}
>
{label}
</Button>
)}
</Menu.Target>
<Menu.Dropdown data-testid="dropdown">{children}</Menu.Dropdown>
</Menu>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { ReactNode } from 'react';

import { Tabs } from '@mantine/core';

// eslint-disable-next-line react-refresh/only-export-components
function Content({ children }: { children: ReactNode }): ReactNode {
return <div style={{ paddingTop: 20 }}>{children}</div>;
}

export const contents = (
<>
<Tabs.Panel value="1">
<Content>First Content</Content>
</Tabs.Panel>
<Tabs.Panel value="2">
<Content>Second Content</Content>
</Tabs.Panel>
<Tabs.Panel value="3">
<Content>Third Content</Content>
</Tabs.Panel>
<Tabs.Panel value="4">
<Content>Fourth Content</Content>
</Tabs.Panel>
<Tabs.Panel value="5">
<Content>Overflow 1 Content</Content>
</Tabs.Panel>
<Tabs.Panel value="6">
<Content>Overflow 2 Content</Content>
</Tabs.Panel>
</>
);

export const tabs = [
<Tabs.Tab key={1} value="1">
First
</Tabs.Tab>,
<Tabs.Tab key={2} data-testid="test-tab" value="2">
Second
</Tabs.Tab>,
<Tabs.Tab key={3} value="3">
Third
</Tabs.Tab>,
<Tabs.Tab key={4} value="4">
Fourth
</Tabs.Tab>,
<Tabs.Tab key={5} value="5">
Overflow 1
</Tabs.Tab>,
<Tabs.Tab key={6} value="6">
Overflow 2
</Tabs.Tab>,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Meta, StoryObj } from '@storybook/react';
import type { ReactNode } from 'react';

import { sleep } from '@smile/react-front-kit-shared/src/storybook-utils';
import { expect } from '@storybook/jest';
import { userEvent, within } from '@storybook/testing-library';

import { ResponsiveTabs as Cmp } from './ResponsiveTabs';
import { contents, tabs } from './ResponsiveTabs.mock';

const meta = {
component: Cmp,
tags: ['autodocs'],
title: '3-custom/Components/ResponsiveTabs',
} satisfies Meta<typeof Cmp>;

export default meta;
type IStory = StoryObj<typeof meta>;

function Wrapper({ children }: { children: ReactNode }): ReactNode {
return (
<div
style={{
border: '1px solid darkgrey',
height: 200,
margin: 'auto',
overflow: 'hidden',
resize: 'both',
width: 320,
}}
>
<div style={{ margin: 20 }}>{children}</div>
</div>
);
}

export const ResponsiveTabs: IStory = {
args: {
children: contents,
defaultValue: '1',
tabs,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText('First Content')).toBeVisible();
await userEvent.click(canvas.getByTestId('test-tab'));
await sleep(200);
await expect(canvas.getByText('Second Content')).toBeVisible();
},
render: ({ children, ...props }) => (
<Wrapper>
<Cmp {...props}>{children}</Cmp>
</Wrapper>
),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { renderWithProviders } from '@smile/react-front-kit-shared/src/test-utils';

import { ResponsiveTabs } from './ResponsiveTabs';
import { contents, tabs } from './ResponsiveTabs.mock';

describe('ResponsiveTabs', () => {
it('matches snapshot', () => {
const { container } = renderWithProviders(
<ResponsiveTabs tabs={tabs}>{contents}</ResponsiveTabs>,
);
expect(container).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { TabsProps } from '@mantine/core';
import type { ReactElement, ReactNode } from 'react';

import { ActionIcon, Tabs } from '@mantine/core';
import { useId } from '@mantine/hooks';
import { createStyles } from '@mantine/styles';
import { CaretDoubleRight } from '@phosphor-icons/react';
import { createRef, useEffect, useState } from 'react';

import { DropdownButton } from '../DropdownButton/DropdownButton';

const useStyles = createStyles(() => ({
dropdown: {
display: 'flex',
flexDirection: 'column',
},
tabs: {
flexWrap: 'nowrap',
},
}));

export interface IResponsiveTabs extends TabsProps {
children: ReactNode;
tabs: ReactElement[];
}

export function ResponsiveTabs(props: IResponsiveTabs): ReactNode {
const { children, tabs, ...tabsProps } = props;
const [overflowIndex, setOverflowIndex] = useState<number>(tabs.length);
const tabsRef = createRef<HTMLDivElement>();
const overflowButtonId = useId();
const { classes } = useStyles();

// TODO: useElementSize (https://v6.mantine.dev/hooks/use-element-size/) to re-calculate overflow on resize

Check warning on line 34 in packages/react-front-kit/src/Components/ResponsiveTabs/ResponsiveTabs.tsx

View workflow job for this annotation

GitHub Actions / test

Unexpected 'todo' comment: 'TODO: useElementSize...'
useEffect(() => {
const parentElement = tabsRef.current;
if (parentElement) {
const tabElements = Array.from(
parentElement.children,
) as HTMLButtonElement[];
const overflowIndex = tabElements.findIndex((el) => {
return (
el.id !== overflowButtonId &&
(el.offsetLeft - parentElement.offsetWidth + 30 >
parentElement.offsetWidth ||
el.offsetTop - parentElement.offsetTop > parentElement.offsetHeight)
);
});
setOverflowIndex(overflowIndex);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<Tabs {...tabsProps} style={{ overflow: 'hidden' }}>
<Tabs.List ref={tabsRef} className={classes.tabs}>
{tabs.slice(0, overflowIndex)}
{Boolean(overflowIndex < tabs.length) && (
<DropdownButton
buttonComponent={
<ActionIcon>
<CaretDoubleRight />
</ActionIcon>
}
id={overflowButtonId}
>
<div className={classes.dropdown}>
{tabs.slice(overflowIndex, tabs.length)}
</div>
</DropdownButton>
)}
</Tabs.List>
{children}
</Tabs>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ResponsiveTabs matches snapshot 1`] = `
<div>
<div
class="mantine-Tabs-root mantine-ro6hk9"
style="overflow: hidden;"
>
<div
aria-orientation="horizontal"
class="mantine-Tabs-tabsList mantine-i7gwaj"
role="tablist"
>
<button
aria-controls="mantine-xj4wuvfjs-dropdown"
aria-expanded="false"
aria-haspopup="menu"
class="mantine-UnstyledButton-root mantine-ActionIcon-root mantine-3zan65"
id="mantine-xj4wuvfjs-target"
type="button"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 256 256"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M141.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L124.69,128,50.34,53.66A8,8,0,0,1,61.66,42.34l80,80A8,8,0,0,1,141.66,133.66Zm80-11.32-80-80a8,8,0,0,0-11.32,11.32L204.69,128l-74.35,74.34a8,8,0,0,0,11.32,11.32l80-80A8,8,0,0,0,221.66,122.34Z"
/>
</svg>
</button>
</div>
<div
aria-labelledby="mantine-ocee5lhcn-tab-1"
class="mantine-Tabs-panel mantine-18ozoug"
id="mantine-ocee5lhcn-panel-1"
role="tabpanel"
>
<div
style="padding-top: 20px;"
>
First Content
</div>
</div>
<div
aria-labelledby="mantine-ocee5lhcn-tab-2"
class="mantine-Tabs-panel mantine-18ozoug"
id="mantine-ocee5lhcn-panel-2"
role="tabpanel"
>
<div
style="padding-top: 20px;"
>
Second Content
</div>
</div>
<div
aria-labelledby="mantine-ocee5lhcn-tab-3"
class="mantine-Tabs-panel mantine-18ozoug"
id="mantine-ocee5lhcn-panel-3"
role="tabpanel"
>
<div
style="padding-top: 20px;"
>
Third Content
</div>
</div>
<div
aria-labelledby="mantine-ocee5lhcn-tab-4"
class="mantine-Tabs-panel mantine-18ozoug"
id="mantine-ocee5lhcn-panel-4"
role="tabpanel"
>
<div
style="padding-top: 20px;"
>
Fourth Content
</div>
</div>
<div
aria-labelledby="mantine-ocee5lhcn-tab-5"
class="mantine-Tabs-panel mantine-18ozoug"
id="mantine-ocee5lhcn-panel-5"
role="tabpanel"
>
<div
style="padding-top: 20px;"
>
Overflow 1 Content
</div>
</div>
<div
aria-labelledby="mantine-ocee5lhcn-tab-6"
class="mantine-Tabs-panel mantine-18ozoug"
id="mantine-ocee5lhcn-panel-6"
role="tabpanel"
>
<div
style="padding-top: 20px;"
>
Overflow 2 Content
</div>
</div>
</div>
</div>
`;

0 comments on commit d1d960f

Please sign in to comment.