mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
fix(tabs): tabs in modal (#5705)
* fix(tabs): tabs in modal * refactor(tabs): revise tabs cursor * refactor(theme): remove unused styles * fix(tabs): revise cursor rendering logic * chore(docs): remove motionProps * refactor(tabs): cursor styles * refactor(tabs): cursor styles * fix(tabs): avoid transition on variant change * refactor(theme): cursor styles * chore(changeset): include theme package * chore(tabs): bump peerDeps on theme pkg * fix(tabs): revise underlined cursor styles
This commit is contained in:
parent
7537226b54
commit
6eba109a7f
6
.changeset/late-trainers-switch.md
Normal file
6
.changeset/late-trainers-switch.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
"@heroui/tabs": patch
|
||||
"@heroui/theme": patch
|
||||
---
|
||||
|
||||
fix tabs in modal (#5657)
|
||||
@ -318,12 +318,6 @@ You can customize the `Tabs` component by passing custom Tailwind CSS classes to
|
||||
description: "Whether tabs are activated automatically on focus or manually.",
|
||||
default: "automatic"
|
||||
},
|
||||
{
|
||||
attribute: "motionProps",
|
||||
type: "MotionProps",
|
||||
description: "The props to modify the cursor framer motion animation. Use the variants API to create your own animation.",
|
||||
default: "-"
|
||||
},
|
||||
{
|
||||
attribute: "disableCursorAnimation",
|
||||
type: "boolean",
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
"react": ">=18 || >=19.0.0-rc.0",
|
||||
"react-dom": ">=18 || >=19.0.0-rc.0",
|
||||
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
|
||||
"@heroui/theme": ">=2.4.17",
|
||||
"@heroui/theme": ">=2.4.22",
|
||||
"@heroui/system": ">=2.4.18"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@ -9,8 +9,6 @@ import scrollIntoView from "scroll-into-view-if-needed";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
import {useTab} from "@react-aria/tabs";
|
||||
import {useHover} from "@react-aria/interactions";
|
||||
import {m, domMax, LazyMotion} from "framer-motion";
|
||||
import {useIsMounted} from "@heroui/use-is-mounted";
|
||||
|
||||
export interface TabItemProps<T extends object = object> extends BaseTabItemProps<T> {
|
||||
item: Node<T>;
|
||||
@ -19,9 +17,6 @@ export interface TabItemProps<T extends object = object> extends BaseTabItemProp
|
||||
listRef?: ValuesType["listRef"];
|
||||
classNames?: ValuesType["classNames"];
|
||||
isDisabled?: ValuesType["isDisabled"];
|
||||
motionProps?: ValuesType["motionProps"];
|
||||
disableAnimation?: ValuesType["disableAnimation"];
|
||||
disableCursorAnimation?: ValuesType["disableCursorAnimation"];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -37,9 +32,6 @@ const Tab = forwardRef<"button", TabItemProps>((props, ref) => {
|
||||
isDisabled: isDisabledProp,
|
||||
listRef,
|
||||
slots,
|
||||
motionProps,
|
||||
disableAnimation,
|
||||
disableCursorAnimation,
|
||||
shouldSelectOnPressUp,
|
||||
tabRef,
|
||||
...otherProps
|
||||
@ -72,12 +64,6 @@ const Tab = forwardRef<"button", TabItemProps>((props, ref) => {
|
||||
|
||||
const tabStyles = clsx(classNames?.tab, className);
|
||||
|
||||
const [, isMounted] = useIsMounted({
|
||||
rerender: true,
|
||||
});
|
||||
|
||||
const isInModal = domRef?.current?.closest('[aria-modal="true"]') !== null;
|
||||
|
||||
const handleClick = () => {
|
||||
if (!domRef?.current || !listRef?.current) return;
|
||||
|
||||
@ -98,6 +84,7 @@ const Tab = forwardRef<"button", TabItemProps>((props, ref) => {
|
||||
data-focus-visible={dataAttr(isFocusVisible)}
|
||||
data-hover={dataAttr(isHovered)}
|
||||
data-hover-unselected={dataAttr((isHovered || isPressed) && !isSelected)}
|
||||
data-key={key}
|
||||
data-pressed={dataAttr(isPressed)}
|
||||
data-selected={dataAttr(isSelected)}
|
||||
data-slot="tab"
|
||||
@ -122,24 +109,6 @@ const Tab = forwardRef<"button", TabItemProps>((props, ref) => {
|
||||
title={otherProps?.titleValue}
|
||||
type={Component === "button" ? "button" : undefined}
|
||||
>
|
||||
{isSelected && !disableAnimation && !disableCursorAnimation && isMounted && !isInModal ? (
|
||||
// use synchronous loading for domMax here
|
||||
// since lazy loading produces different behaviour
|
||||
<LazyMotion features={domMax}>
|
||||
<m.span
|
||||
className={slots.cursor({class: classNames?.cursor})}
|
||||
data-slot="cursor"
|
||||
layoutDependency={false}
|
||||
layoutId="cursor"
|
||||
transition={{
|
||||
type: "spring",
|
||||
bounce: 0.15,
|
||||
duration: 0.5,
|
||||
}}
|
||||
{...motionProps}
|
||||
/>
|
||||
</LazyMotion>
|
||||
) : null}
|
||||
<div
|
||||
className={slots.tabContent({
|
||||
class: classNames?.tabContent,
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import type {ForwardedRef, ReactElement} from "react";
|
||||
import type {UseTabsProps} from "./use-tabs";
|
||||
|
||||
import {useId} from "react";
|
||||
import {LayoutGroup} from "framer-motion";
|
||||
import {useRef, useMemo} from "react";
|
||||
import {forwardRef} from "@heroui/system";
|
||||
|
||||
import {useTabs} from "./use-tabs";
|
||||
@ -26,53 +25,145 @@ const Tabs = forwardRef(function Tabs<T extends object>(
|
||||
getBaseProps,
|
||||
getTabListProps,
|
||||
getWrapperProps,
|
||||
getTabCursorProps,
|
||||
} = useTabs<T>({
|
||||
...props,
|
||||
ref,
|
||||
});
|
||||
|
||||
const layoutId = useId();
|
||||
|
||||
const isInModal = domRef?.current?.closest('[aria-modal="true"]') !== null;
|
||||
|
||||
const layoutGroupEnabled = !props.disableAnimation && !props.disableCursorAnimation && !isInModal;
|
||||
|
||||
const tabsProps = {
|
||||
state,
|
||||
listRef: values.listRef,
|
||||
slots: values.slots,
|
||||
classNames: values.classNames,
|
||||
isDisabled: values.isDisabled,
|
||||
motionProps: values.motionProps,
|
||||
disableAnimation: values.disableAnimation,
|
||||
shouldSelectOnPressUp: values.shouldSelectOnPressUp,
|
||||
disableCursorAnimation: values.disableCursorAnimation,
|
||||
};
|
||||
|
||||
const tabs = [...state.collection].map((item) => (
|
||||
<Tab key={item.key} item={item} {...tabsProps} {...item.props} />
|
||||
));
|
||||
|
||||
const renderTabs = (
|
||||
<>
|
||||
<div {...getBaseProps()}>
|
||||
<Component {...getTabListProps()}>
|
||||
{layoutGroupEnabled ? <LayoutGroup id={layoutId}>{tabs}</LayoutGroup> : tabs}
|
||||
</Component>
|
||||
</div>
|
||||
{[...state.collection].map((item) => {
|
||||
return (
|
||||
<TabPanel
|
||||
key={item.key}
|
||||
classNames={values.classNames}
|
||||
destroyInactiveTabPanel={destroyInactiveTabPanel}
|
||||
slots={values.slots}
|
||||
state={values.state}
|
||||
tabKey={item.key}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
const selectedItem = state.selectedItem;
|
||||
const selectedKey = selectedItem?.key;
|
||||
const prevSelectedKey = useRef<typeof selectedKey>(undefined);
|
||||
const prevVariant = useRef(props?.variant);
|
||||
const variant = props?.variant;
|
||||
const isVertical = props?.isVertical;
|
||||
|
||||
const getCursorStyles = (tabRect: DOMRect, relativeLeft: number, relativeTop: number) => {
|
||||
const baseStyles = {
|
||||
left: `${relativeLeft}px`,
|
||||
width: `${tabRect.width}px`,
|
||||
};
|
||||
|
||||
if (variant === "underlined") {
|
||||
return {
|
||||
left: `${relativeLeft + tabRect.width * 0.1}px`,
|
||||
top: `${relativeTop + tabRect.height - 2}px`,
|
||||
width: `${tabRect.width * 0.8}px`,
|
||||
height: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (variant === "bordered") {
|
||||
const borderWidth = 2;
|
||||
|
||||
return {
|
||||
...baseStyles,
|
||||
top: `${relativeTop - borderWidth}px`,
|
||||
width: `${tabRect.width - borderWidth}px`,
|
||||
height: `${tabRect.height}px`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...baseStyles,
|
||||
top: `${relativeTop}px`,
|
||||
height: `${tabRect.height}px`,
|
||||
};
|
||||
};
|
||||
|
||||
const updateCursorPosition = (
|
||||
node: HTMLSpanElement,
|
||||
selectedTab: HTMLElement,
|
||||
parentRect: DOMRect,
|
||||
) => {
|
||||
const tabRect = selectedTab.getBoundingClientRect();
|
||||
const relativeLeft = tabRect.left - parentRect.left;
|
||||
const relativeTop = tabRect.top - parentRect.top;
|
||||
|
||||
const styles = getCursorStyles(tabRect, relativeLeft, relativeTop);
|
||||
|
||||
node.style.left = styles.left;
|
||||
node.style.top = styles.top;
|
||||
node.style.width = styles.width;
|
||||
node.style.height = styles.height;
|
||||
};
|
||||
|
||||
const handleCursorRef = (node: HTMLSpanElement | null) => {
|
||||
if (!node) return;
|
||||
|
||||
const selectedTab = domRef.current?.querySelector(`[data-key="${selectedKey}"]`) as HTMLElement;
|
||||
|
||||
if (!selectedTab || !domRef.current) return;
|
||||
|
||||
const shouldDisableTransition =
|
||||
prevSelectedKey.current === undefined || prevVariant.current !== variant;
|
||||
|
||||
node.style.transition = shouldDisableTransition ? "none" : "";
|
||||
|
||||
prevSelectedKey.current = selectedKey;
|
||||
prevVariant.current = variant;
|
||||
|
||||
const parentRect = domRef.current.getBoundingClientRect();
|
||||
|
||||
updateCursorPosition(node, selectedTab, parentRect);
|
||||
};
|
||||
|
||||
const renderTabs = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<div {...getBaseProps()}>
|
||||
<Component {...getTabListProps()}>
|
||||
{!values.disableAnimation && !values.disableCursorAnimation && selectedKey != null && (
|
||||
<span {...getTabCursorProps()} ref={handleCursorRef} />
|
||||
)}
|
||||
{tabs}
|
||||
</Component>
|
||||
</div>
|
||||
{[...state.collection].map((item) => {
|
||||
return (
|
||||
<TabPanel
|
||||
key={item.key}
|
||||
classNames={values.classNames}
|
||||
destroyInactiveTabPanel={destroyInactiveTabPanel}
|
||||
slots={values.slots}
|
||||
state={values.state}
|
||||
tabKey={item.key}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
),
|
||||
[
|
||||
Component,
|
||||
getBaseProps,
|
||||
getTabListProps,
|
||||
getTabCursorProps,
|
||||
tabs,
|
||||
selectedKey,
|
||||
state.collection,
|
||||
values.disableAnimation,
|
||||
values.disableCursorAnimation,
|
||||
values.classNames,
|
||||
values.slots,
|
||||
values.state,
|
||||
destroyInactiveTabPanel,
|
||||
domRef,
|
||||
variant,
|
||||
isVertical,
|
||||
],
|
||||
);
|
||||
|
||||
if ("placement" in props || "isVertical" in props) {
|
||||
|
||||
@ -5,7 +5,6 @@ import type {TabListState, TabListStateOptions} from "@react-stately/tabs";
|
||||
import type {AriaTabListProps} from "@react-aria/tabs";
|
||||
import type {CollectionProps} from "@heroui/aria-utils";
|
||||
import type {CollectionChildren} from "@react-types/shared";
|
||||
import type {HTMLMotionProps} from "framer-motion";
|
||||
import type {HTMLHeroUIProps, PropGetter} from "@heroui/system";
|
||||
|
||||
import {mapPropsVariants, useProviderContext} from "@heroui/system";
|
||||
@ -22,10 +21,6 @@ export interface Props extends Omit<HTMLHeroUIProps, "children"> {
|
||||
* Ref to the DOM node.
|
||||
*/
|
||||
ref?: ReactRef<HTMLElement | null>;
|
||||
/**
|
||||
* The props to modify the cursor motion animation. Use the `variants` API to create your own animation.
|
||||
*/
|
||||
motionProps?: Omit<HTMLMotionProps<"span">, "ref">;
|
||||
/**
|
||||
* Whether the tabs selection should occur on press up instead of press down.
|
||||
* @default true
|
||||
@ -81,7 +76,6 @@ export type ValuesType<T = object> = {
|
||||
listRef?: RefObject<HTMLElement>;
|
||||
shouldSelectOnPressUp?: boolean;
|
||||
classNames?: SlotsToClasses<TabsSlots>;
|
||||
motionProps?: Omit<HTMLMotionProps<"span">, "ref">;
|
||||
disableAnimation?: boolean;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
@ -98,7 +92,6 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
|
||||
classNames,
|
||||
children,
|
||||
disableCursorAnimation,
|
||||
motionProps,
|
||||
isVertical = false,
|
||||
shouldSelectOnPressUp = true,
|
||||
destroyInactiveTabPanel = true,
|
||||
@ -136,7 +129,6 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
|
||||
state,
|
||||
slots,
|
||||
classNames,
|
||||
motionProps,
|
||||
disableAnimation,
|
||||
listRef: domRef,
|
||||
shouldSelectOnPressUp,
|
||||
@ -147,7 +139,6 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
|
||||
state,
|
||||
slots,
|
||||
domRef,
|
||||
motionProps,
|
||||
disableAnimation,
|
||||
disableCursorAnimation,
|
||||
shouldSelectOnPressUp,
|
||||
@ -192,6 +183,16 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
|
||||
[domRef, tabListProps, classNames, slots],
|
||||
);
|
||||
|
||||
const getTabCursorProps: PropGetter = useCallback(
|
||||
(props) => ({
|
||||
"data-slot": "cursor",
|
||||
className: slots.cursor({
|
||||
class: clsx(classNames?.cursor, props?.className),
|
||||
}),
|
||||
}),
|
||||
[classNames, slots],
|
||||
);
|
||||
|
||||
return {
|
||||
Component,
|
||||
domRef,
|
||||
@ -201,6 +202,7 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
|
||||
getBaseProps,
|
||||
getTabListProps,
|
||||
getWrapperProps,
|
||||
getTabCursorProps,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ const tabs = tv({
|
||||
slots: {
|
||||
base: "inline-flex",
|
||||
tabList: [
|
||||
"relative",
|
||||
"flex",
|
||||
"p-1",
|
||||
"h-fit",
|
||||
@ -63,7 +64,15 @@ const tabs = tv({
|
||||
"text-default-500",
|
||||
"group-data-[selected=true]:text-foreground",
|
||||
],
|
||||
cursor: ["absolute", "z-0", "bg-white"],
|
||||
cursor: [
|
||||
"absolute",
|
||||
"z-0",
|
||||
"bg-white",
|
||||
"will-change-[transform,width,height]",
|
||||
"transition-[left,top,width,height]",
|
||||
"duration-250",
|
||||
"ease-out",
|
||||
],
|
||||
panel: [
|
||||
"py-3",
|
||||
"px-1",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user