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.",
|
description: "Whether tabs are activated automatically on focus or manually.",
|
||||||
default: "automatic"
|
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",
|
attribute: "disableCursorAnimation",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
|
|||||||
@ -37,7 +37,7 @@
|
|||||||
"react": ">=18 || >=19.0.0-rc.0",
|
"react": ">=18 || >=19.0.0-rc.0",
|
||||||
"react-dom": ">=18 || >=19.0.0-rc.0",
|
"react-dom": ">=18 || >=19.0.0-rc.0",
|
||||||
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
|
"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"
|
"@heroui/system": ">=2.4.18"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -9,8 +9,6 @@ import scrollIntoView from "scroll-into-view-if-needed";
|
|||||||
import {useFocusRing} from "@react-aria/focus";
|
import {useFocusRing} from "@react-aria/focus";
|
||||||
import {useTab} from "@react-aria/tabs";
|
import {useTab} from "@react-aria/tabs";
|
||||||
import {useHover} from "@react-aria/interactions";
|
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> {
|
export interface TabItemProps<T extends object = object> extends BaseTabItemProps<T> {
|
||||||
item: Node<T>;
|
item: Node<T>;
|
||||||
@ -19,9 +17,6 @@ export interface TabItemProps<T extends object = object> extends BaseTabItemProp
|
|||||||
listRef?: ValuesType["listRef"];
|
listRef?: ValuesType["listRef"];
|
||||||
classNames?: ValuesType["classNames"];
|
classNames?: ValuesType["classNames"];
|
||||||
isDisabled?: ValuesType["isDisabled"];
|
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,
|
isDisabled: isDisabledProp,
|
||||||
listRef,
|
listRef,
|
||||||
slots,
|
slots,
|
||||||
motionProps,
|
|
||||||
disableAnimation,
|
|
||||||
disableCursorAnimation,
|
|
||||||
shouldSelectOnPressUp,
|
shouldSelectOnPressUp,
|
||||||
tabRef,
|
tabRef,
|
||||||
...otherProps
|
...otherProps
|
||||||
@ -72,12 +64,6 @@ const Tab = forwardRef<"button", TabItemProps>((props, ref) => {
|
|||||||
|
|
||||||
const tabStyles = clsx(classNames?.tab, className);
|
const tabStyles = clsx(classNames?.tab, className);
|
||||||
|
|
||||||
const [, isMounted] = useIsMounted({
|
|
||||||
rerender: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isInModal = domRef?.current?.closest('[aria-modal="true"]') !== null;
|
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (!domRef?.current || !listRef?.current) return;
|
if (!domRef?.current || !listRef?.current) return;
|
||||||
|
|
||||||
@ -98,6 +84,7 @@ const Tab = forwardRef<"button", TabItemProps>((props, ref) => {
|
|||||||
data-focus-visible={dataAttr(isFocusVisible)}
|
data-focus-visible={dataAttr(isFocusVisible)}
|
||||||
data-hover={dataAttr(isHovered)}
|
data-hover={dataAttr(isHovered)}
|
||||||
data-hover-unselected={dataAttr((isHovered || isPressed) && !isSelected)}
|
data-hover-unselected={dataAttr((isHovered || isPressed) && !isSelected)}
|
||||||
|
data-key={key}
|
||||||
data-pressed={dataAttr(isPressed)}
|
data-pressed={dataAttr(isPressed)}
|
||||||
data-selected={dataAttr(isSelected)}
|
data-selected={dataAttr(isSelected)}
|
||||||
data-slot="tab"
|
data-slot="tab"
|
||||||
@ -122,24 +109,6 @@ const Tab = forwardRef<"button", TabItemProps>((props, ref) => {
|
|||||||
title={otherProps?.titleValue}
|
title={otherProps?.titleValue}
|
||||||
type={Component === "button" ? "button" : undefined}
|
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
|
<div
|
||||||
className={slots.tabContent({
|
className={slots.tabContent({
|
||||||
class: classNames?.tabContent,
|
class: classNames?.tabContent,
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import type {ForwardedRef, ReactElement} from "react";
|
import type {ForwardedRef, ReactElement} from "react";
|
||||||
import type {UseTabsProps} from "./use-tabs";
|
import type {UseTabsProps} from "./use-tabs";
|
||||||
|
|
||||||
import {useId} from "react";
|
import {useRef, useMemo} from "react";
|
||||||
import {LayoutGroup} from "framer-motion";
|
|
||||||
import {forwardRef} from "@heroui/system";
|
import {forwardRef} from "@heroui/system";
|
||||||
|
|
||||||
import {useTabs} from "./use-tabs";
|
import {useTabs} from "./use-tabs";
|
||||||
@ -26,53 +25,145 @@ const Tabs = forwardRef(function Tabs<T extends object>(
|
|||||||
getBaseProps,
|
getBaseProps,
|
||||||
getTabListProps,
|
getTabListProps,
|
||||||
getWrapperProps,
|
getWrapperProps,
|
||||||
|
getTabCursorProps,
|
||||||
} = useTabs<T>({
|
} = useTabs<T>({
|
||||||
...props,
|
...props,
|
||||||
ref,
|
ref,
|
||||||
});
|
});
|
||||||
|
|
||||||
const layoutId = useId();
|
|
||||||
|
|
||||||
const isInModal = domRef?.current?.closest('[aria-modal="true"]') !== null;
|
|
||||||
|
|
||||||
const layoutGroupEnabled = !props.disableAnimation && !props.disableCursorAnimation && !isInModal;
|
|
||||||
|
|
||||||
const tabsProps = {
|
const tabsProps = {
|
||||||
state,
|
state,
|
||||||
listRef: values.listRef,
|
listRef: values.listRef,
|
||||||
slots: values.slots,
|
slots: values.slots,
|
||||||
classNames: values.classNames,
|
classNames: values.classNames,
|
||||||
isDisabled: values.isDisabled,
|
isDisabled: values.isDisabled,
|
||||||
motionProps: values.motionProps,
|
|
||||||
disableAnimation: values.disableAnimation,
|
|
||||||
shouldSelectOnPressUp: values.shouldSelectOnPressUp,
|
shouldSelectOnPressUp: values.shouldSelectOnPressUp,
|
||||||
disableCursorAnimation: values.disableCursorAnimation,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs = [...state.collection].map((item) => (
|
const tabs = [...state.collection].map((item) => (
|
||||||
<Tab key={item.key} item={item} {...tabsProps} {...item.props} />
|
<Tab key={item.key} item={item} {...tabsProps} {...item.props} />
|
||||||
));
|
));
|
||||||
|
|
||||||
const renderTabs = (
|
const selectedItem = state.selectedItem;
|
||||||
<>
|
const selectedKey = selectedItem?.key;
|
||||||
<div {...getBaseProps()}>
|
const prevSelectedKey = useRef<typeof selectedKey>(undefined);
|
||||||
<Component {...getTabListProps()}>
|
const prevVariant = useRef(props?.variant);
|
||||||
{layoutGroupEnabled ? <LayoutGroup id={layoutId}>{tabs}</LayoutGroup> : tabs}
|
const variant = props?.variant;
|
||||||
</Component>
|
const isVertical = props?.isVertical;
|
||||||
</div>
|
|
||||||
{[...state.collection].map((item) => {
|
const getCursorStyles = (tabRect: DOMRect, relativeLeft: number, relativeTop: number) => {
|
||||||
return (
|
const baseStyles = {
|
||||||
<TabPanel
|
left: `${relativeLeft}px`,
|
||||||
key={item.key}
|
width: `${tabRect.width}px`,
|
||||||
classNames={values.classNames}
|
};
|
||||||
destroyInactiveTabPanel={destroyInactiveTabPanel}
|
|
||||||
slots={values.slots}
|
if (variant === "underlined") {
|
||||||
state={values.state}
|
return {
|
||||||
tabKey={item.key}
|
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) {
|
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 {AriaTabListProps} from "@react-aria/tabs";
|
||||||
import type {CollectionProps} from "@heroui/aria-utils";
|
import type {CollectionProps} from "@heroui/aria-utils";
|
||||||
import type {CollectionChildren} from "@react-types/shared";
|
import type {CollectionChildren} from "@react-types/shared";
|
||||||
import type {HTMLMotionProps} from "framer-motion";
|
|
||||||
import type {HTMLHeroUIProps, PropGetter} from "@heroui/system";
|
import type {HTMLHeroUIProps, PropGetter} from "@heroui/system";
|
||||||
|
|
||||||
import {mapPropsVariants, useProviderContext} 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 to the DOM node.
|
||||||
*/
|
*/
|
||||||
ref?: ReactRef<HTMLElement | null>;
|
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.
|
* Whether the tabs selection should occur on press up instead of press down.
|
||||||
* @default true
|
* @default true
|
||||||
@ -81,7 +76,6 @@ export type ValuesType<T = object> = {
|
|||||||
listRef?: RefObject<HTMLElement>;
|
listRef?: RefObject<HTMLElement>;
|
||||||
shouldSelectOnPressUp?: boolean;
|
shouldSelectOnPressUp?: boolean;
|
||||||
classNames?: SlotsToClasses<TabsSlots>;
|
classNames?: SlotsToClasses<TabsSlots>;
|
||||||
motionProps?: Omit<HTMLMotionProps<"span">, "ref">;
|
|
||||||
disableAnimation?: boolean;
|
disableAnimation?: boolean;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
};
|
};
|
||||||
@ -98,7 +92,6 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
|
|||||||
classNames,
|
classNames,
|
||||||
children,
|
children,
|
||||||
disableCursorAnimation,
|
disableCursorAnimation,
|
||||||
motionProps,
|
|
||||||
isVertical = false,
|
isVertical = false,
|
||||||
shouldSelectOnPressUp = true,
|
shouldSelectOnPressUp = true,
|
||||||
destroyInactiveTabPanel = true,
|
destroyInactiveTabPanel = true,
|
||||||
@ -136,7 +129,6 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
|
|||||||
state,
|
state,
|
||||||
slots,
|
slots,
|
||||||
classNames,
|
classNames,
|
||||||
motionProps,
|
|
||||||
disableAnimation,
|
disableAnimation,
|
||||||
listRef: domRef,
|
listRef: domRef,
|
||||||
shouldSelectOnPressUp,
|
shouldSelectOnPressUp,
|
||||||
@ -147,7 +139,6 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
|
|||||||
state,
|
state,
|
||||||
slots,
|
slots,
|
||||||
domRef,
|
domRef,
|
||||||
motionProps,
|
|
||||||
disableAnimation,
|
disableAnimation,
|
||||||
disableCursorAnimation,
|
disableCursorAnimation,
|
||||||
shouldSelectOnPressUp,
|
shouldSelectOnPressUp,
|
||||||
@ -192,6 +183,16 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
|
|||||||
[domRef, tabListProps, classNames, slots],
|
[domRef, tabListProps, classNames, slots],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getTabCursorProps: PropGetter = useCallback(
|
||||||
|
(props) => ({
|
||||||
|
"data-slot": "cursor",
|
||||||
|
className: slots.cursor({
|
||||||
|
class: clsx(classNames?.cursor, props?.className),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
[classNames, slots],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Component,
|
Component,
|
||||||
domRef,
|
domRef,
|
||||||
@ -201,6 +202,7 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
|
|||||||
getBaseProps,
|
getBaseProps,
|
||||||
getTabListProps,
|
getTabListProps,
|
||||||
getWrapperProps,
|
getWrapperProps,
|
||||||
|
getTabCursorProps,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@ const tabs = tv({
|
|||||||
slots: {
|
slots: {
|
||||||
base: "inline-flex",
|
base: "inline-flex",
|
||||||
tabList: [
|
tabList: [
|
||||||
|
"relative",
|
||||||
"flex",
|
"flex",
|
||||||
"p-1",
|
"p-1",
|
||||||
"h-fit",
|
"h-fit",
|
||||||
@ -63,7 +64,15 @@ const tabs = tv({
|
|||||||
"text-default-500",
|
"text-default-500",
|
||||||
"group-data-[selected=true]:text-foreground",
|
"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: [
|
panel: [
|
||||||
"py-3",
|
"py-3",
|
||||||
"px-1",
|
"px-1",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user