Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/famous-panthers-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nextui-org/tabs": patch
---

Add placement and isVertical prop
4 changes: 4 additions & 0 deletions apps/docs/content/components/tabs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import icons from "./icons";
import form from "./form";
import controlled from "./controlled";
import customStyles from "./custom-styles";
import placement from "./placement";
import vertical from "./vertical";

export const tabsContent = {
usage,
Expand All @@ -24,4 +26,6 @@ export const tabsContent = {
form,
controlled,
customStyles,
placement,
vertical,
};
54 changes: 54 additions & 0 deletions apps/docs/content/components/tabs/placement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const App = `import {Tabs, Tab, Card, CardBody, RadioGroup, Radio} from "@nextui-org/react";

export default function App() {
const [placement, setPlacement] = React.useState("top");
return (
<div className="flex flex-col px-4">
<RadioGroup
className="mb-4"
label="Placement"
orientation="top"
value={placement}
onValueChange={(value) => setPlacement(value)}
>
<Radio value="top">top</Radio>
<Radio value="bottom">bottom</Radio>
<Radio value="start">start</Radio>
<Radio value="end">end</Radio>
</RadioGroup>
<div className="flex w-full flex-col">
<Tabs aria-label="Options" placement={placement}>
<Tab key="photos" title="Photos">
<Card>
<CardBody>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</CardBody>
</Card>
</Tab>
<Tab key="music" title="Music">
<Card>
<CardBody>
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
</CardBody>
</Card>
</Tab>
<Tab key="videos" title="Videos">
<Card>
<CardBody>
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</CardBody>
</Card>
</Tab>
</Tabs>
</div>
</div>
);
}`;

const react = {
"/App.jsx": App,
};

export default {
...react,
};
45 changes: 45 additions & 0 deletions apps/docs/content/components/tabs/vertical.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const App = `import {Tabs, Tab, Card, CardBody, Switch} from "@nextui-org/react";

export default function App() {
const [isVertical, setIsVertical] = React.useState(true);
return (
<div className="flex flex-col px-4">
<Switch className="mb-4" isSelected={isVertical} onValueChange={setIsVertical}>
Vertical
</Switch>
<div className="flex w-full flex-col">
<Tabs aria-label="Options" isVertical={isVertical}>
<Tab key="photos" title="Photos">
<Card>
<CardBody>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</CardBody>
</Card>
</Tab>
<Tab key="music" title="Music">
<Card>
<CardBody>
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
</CardBody>
</Card>
</Tab>
<Tab key="videos" title="Videos">
<Card>
<CardBody>
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</CardBody>
</Card>
</Tab>
</Tabs>
</div>
</div>
);
}`;

const react = {
"/App.jsx": App,
};

export default {
...react,
};
16 changes: 15 additions & 1 deletion apps/docs/content/docs/components/tabs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,18 @@ You can use the `onSelectionChange` and `selectedKey` props to control the selec

<CodeDemo title="Controlled" files={tabsContent.controlled} />

### Placement

You can change the position of the tabs by using the `placement` prop. The default value is `top`.

<CodeDemo title="Placement" files={tabsContent.placement} />

### Vertical

Change the orientation of the tabs it will invalidate the placement prop when the value is `true`.

<CodeDemo title="Vertical" files={tabsContent.vertical} />

### Links

Tabs items can be rendered as links by passing the `href` prop to the `Tab` component. By
Expand Down Expand Up @@ -237,7 +249,9 @@ You can customize the `Tabs` component by passing custom Tailwind CSS classes to
| disableCursorAnimation | `boolean` | Whether the cursor should be hidden. | `false` |
| isDisabled | `boolean` | Whether the tab list should be disabled. | `false` |
| disableAnimation | `boolean` | Whether the tab list should be animated. | `false` |
| classNames | `Record<"base"| "tabList"| "tab"| "tabContent"| "cursor" | "panel", string>` | Allows to set custom class names for the card slots. | - |
| classNames | `Record<"base"| "tabList"| "tab"| "tabContent"| "cursor" | "panel", string>` | Allows to set custom class names for the card slots. | - |
| placement | `top` \| `bottom` \| `start` \| `end` | The position of the tabs. | `top` |
| isVertical | `boolean` | Whether the tabs are vertical. | `false` |

### Tabs Events

Expand Down
84 changes: 83 additions & 1 deletion packages/components/tabs/__tests__/tabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {act, render} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {focus} from "@nextui-org/test-utils";

import {Tabs, Tab} from "../src";
import {Tabs, Tab, TabsProps} from "../src";

type Item = {
id: string;
Expand All @@ -29,6 +29,22 @@ let tabs: Item[] = [
},
];

function getPlacementTemplate(position: TabsProps["placement"]) {
return (
<Tabs aria-label="Tabs static test" data-testid="tabWrapper" placement={position}>
<Tab key="item1" title="Item 1">
<div>Content 1</div>
</Tab>
<Tab key="item2" title="Item 2">
<div>Content 2</div>
</Tab>
<Tab key="item3" title="Item 3">
<div>Content 3</div>
</Tab>
</Tabs>
);
}

// e.g. console.error Warning: Function components cannot be given refs.
// Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
const spy = jest.spyOn(console, "error").mockImplementation(() => {});
Expand Down Expand Up @@ -236,4 +252,70 @@ describe("Tabs", () => {

expect(tab2).toHaveAttribute("aria-selected", "false");
});

it("should change the position of the tabs", () => {
const wrapper = render(getPlacementTemplate("top"));

const tabWrapper = wrapper.getByTestId("tabWrapper").parentNode;

expect(tabWrapper).toHaveAttribute("data-placement", "top");
expect(tabWrapper).toHaveAttribute("data-vertical", "horizontal");

// Test bottom position
wrapper.rerender(getPlacementTemplate("bottom"));

expect(tabWrapper).toHaveAttribute("data-placement", "bottom");
expect(tabWrapper).toHaveAttribute("data-vertical", "horizontal");

// Test start position
wrapper.rerender(getPlacementTemplate("start"));

expect(tabWrapper).toHaveAttribute("data-placement", "start");
expect(tabWrapper).toHaveAttribute("data-vertical", "vertical");

// Test end position
wrapper.rerender(getPlacementTemplate("end"));

expect(tabWrapper).toHaveAttribute("data-placement", "end");
expect(tabWrapper).toHaveAttribute("data-vertical", "vertical");
});

it("should change the orientation of the tabs", () => {
const wrapper = render(
<Tabs isVertical aria-label="Tabs static test" data-testid="tabWrapper">
<Tab key="item1" title="Item 1">
<div>Content 1</div>
</Tab>
<Tab key="item2" title="Item 2">
<div>Content 2</div>
</Tab>
<Tab key="item3" title="Item 3">
<div>Content 3</div>
</Tab>
</Tabs>,
);

const tabWrapper = wrapper.getByTestId("tabWrapper").parentNode;

expect(tabWrapper).toHaveAttribute("data-placement", "start");
expect(tabWrapper).toHaveAttribute("data-vertical", "vertical");

// Test horizontal orientation
wrapper.rerender(
<Tabs aria-label="Tabs static test" data-testid="tabWrapper" isVertical={false}>
<Tab key="item1" title="Item 1">
<div>Content 1</div>
</Tab>
<Tab key="item2" title="Item 2">
<div>Content 2</div>
</Tab>
<Tab key="item3" title="Item 3">
<div>Content 3</div>
</Tab>
</Tabs>,
);

expect(tabWrapper).toHaveAttribute("data-placement", "top");
expect(tabWrapper).toHaveAttribute("data-vertical", "horizontal");
});
});
10 changes: 8 additions & 2 deletions packages/components/tabs/src/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import TabPanel from "./tab-panel";
interface Props<T> extends UseTabsProps<T> {}

function Tabs<T extends object>(props: Props<T>, ref: ForwardedRef<HTMLDivElement>) {
const {Component, values, state, getBaseProps, getTabListProps} = useTabs<T>({
const {Component, values, state, getBaseProps, getTabListProps, getWrapperProps} = useTabs<T>({
...props,
ref,
});
Expand All @@ -34,7 +34,7 @@ function Tabs<T extends object>(props: Props<T>, ref: ForwardedRef<HTMLDivElemen
<Tab key={item.key} item={item} {...tabsProps} {...item.props} />
));

return (
const renderTabs = (
<>
<div {...getBaseProps()}>
<Component {...getTabListProps()}>
Expand All @@ -49,6 +49,12 @@ function Tabs<T extends object>(props: Props<T>, ref: ForwardedRef<HTMLDivElemen
/>
</>
);

if ("placement" in props || "isVertical" in props) {
return <div {...getWrapperProps()}>{renderTabs}</div>;
}

return renderTabs;
}

export type TabsProps<T = object> = Props<T> & {ref?: Ref<HTMLElement>};
Expand Down
31 changes: 28 additions & 3 deletions packages/components/tabs/src/use-tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ export interface Props extends Omit<HTMLNextUIProps, "children"> {
* ``
*/
classNames?: SlotsToClasses<TabsSlots>;
/**
* The position of the tabs.
* @default 'top'
*/
placement?: "top" | "bottom" | "start" | "end";
/**
* Whether the tabs are vertical it will invalidate the placement prop when the value is true.
* @default false
*/
isVertical?: boolean;
}

export type UseTabsProps<T> = Props &
Expand Down Expand Up @@ -77,8 +87,9 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
classNames,
children,
disableCursorAnimation,
shouldSelectOnPressUp = true,
motionProps,
isVertical = false,
shouldSelectOnPressUp = true,
...otherProps
} = props;

Expand All @@ -91,15 +102,16 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
children: children as CollectionChildren<T>,
...otherProps,
});
const {tabListProps} = useTabList<T>(otherProps, state, domRef);
const {tabListProps} = useTabList<T>(otherProps as AriaTabListProps<T>, state, domRef);

const slots = useMemo(
() =>
tabs({
...variantProps,
className,
...(isVertical ? {placement: "start"} : {}),
}),
[objectToDeps(variantProps), className],
[objectToDeps(variantProps), className, isVertical],
);

const baseStyles = clsx(classNames?.base, className);
Expand Down Expand Up @@ -143,6 +155,18 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
[baseStyles, otherProps, slots],
);

const placement = (variantProps as Props).placement ?? (isVertical ? "start" : "top");
const getWrapperProps: PropGetter = useCallback(
(props) => ({
"data-slot": "tabWrapper",
className: slots.wrapper({class: clsx(classNames?.wrapper, props?.className)}),
"data-placement": placement,
"data-vertical":
isVertical || placement === "start" || placement === "end" ? "vertical" : "horizontal",
}),
[classNames, slots, placement, isVertical],
);

const getTabListProps: PropGetter = useCallback(
(props) => ({
ref: domRef,
Expand All @@ -160,6 +184,7 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
values,
getBaseProps,
getTabListProps,
getWrapperProps,
};
}

Expand Down
32 changes: 32 additions & 0 deletions packages/components/tabs/stories/tabs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,38 @@ export const ManualKeyboardActivation = {
},
};

export const Placement = {
render: StaticTemplate,

args: {
placement: "top",
},
argTypes: {
placement: {
options: ["top", "bottom", "start", "end"],
control: {
type: "inline-radio",
},
},
isVertical: {
type: "boolean",
},
},
};

export const Vertical = {
render: StaticTemplate,

args: {
isVertical: true,
},
argTypes: {
isVertical: {
type: "boolean",
},
},
};

export const DisabledItems = {
render: StaticTemplate,

Expand Down
Loading