diff --git a/.fatherrc.js b/.fatherrc.ts similarity index 100% rename from .fatherrc.js rename to .fatherrc.ts diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..d0a77842 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged \ No newline at end of file diff --git a/docs/demo/renderTabBar-use-panes.md b/docs/demo/renderTabBar-use-panes.md deleted file mode 100644 index 616e1cfc..00000000 --- a/docs/demo/renderTabBar-use-panes.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: enderTabBar-use-panes -nav: - title: Demo - path: /demo ---- - - diff --git a/docs/examples/animated.tsx b/docs/examples/animated.tsx index c28d62fa..f6118acb 100644 --- a/docs/examples/animated.tsx +++ b/docs/examples/animated.tsx @@ -1,6 +1,6 @@ import React from 'react'; import Tabs from '../../src'; -import type { CSSMotionProps } from 'rc-motion'; +import type { CSSMotionProps } from '@rc-component/motion'; import '../../assets/index.less'; import './animated.less'; diff --git a/docs/examples/basic.tsx b/docs/examples/basic.tsx index b97a0962..7c460422 100644 --- a/docs/examples/basic.tsx +++ b/docs/examples/basic.tsx @@ -38,7 +38,11 @@ export default () => { } const onTabClick = (key: string) => { - console.log('key', key); + console.log('onTabClick', key); + }; + + const onChange = (key: string) => { + console.log('onChange', key); }; return ( @@ -46,6 +50,7 @@ export default () => { diff --git a/docs/examples/renderTabBar-use-panes.tsx b/docs/examples/renderTabBar-use-panes.tsx deleted file mode 100644 index a976effe..00000000 --- a/docs/examples/renderTabBar-use-panes.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import Tabs from '../../src'; -import '../../assets/index.less'; - -const renderTabBar = props => { - return ( -
- {props.panes.map(pane => { - const { key } = pane; - return {key}; - })} -
- ); -}; - -export default () => { - return ( -
- -
- ); -}; diff --git a/package.json b/package.json index 8816f77c..664b2aad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "rc-tabs", - "version": "15.5.1", + "name": "@rc-component/tabs", + "version": "1.3.0", "description": "tabs ui component for react", "keywords": [ "react", @@ -32,59 +32,59 @@ "docs:deploy": "gh-pages -d .doc", "lint": "eslint src/ docs/examples/ --ext .tsx,.ts,.jsx,.js", "now-build": "npm run build", - "prepublishOnly": "npm run lint && npm run test && npm run compile && np --yolo --no-publish", + "prepublishOnly": "npm run lint && npm run test && npm run compile && rc-np", "start": "dumi dev", - "test": "rc-test" + "test": "rc-test", + "prepare": "husky && dumi setup" }, "dependencies": { - "@babel/runtime": "^7.11.2", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.2.0", + "@rc-component/motion": "^1.1.3", "classnames": "2.x", - "rc-dropdown": "~4.2.0", - "rc-menu": "~9.16.0", - "rc-motion": "^2.6.2", - "rc-resize-observer": "^1.0.0", - "rc-util": "^5.34.1" + "@rc-component/dropdown": "~1.0.0", + "@rc-component/menu": "~1.0.0" }, "devDependencies": { - "@rc-component/father-plugin": "^1.0.0", - "@rc-component/trigger": "^2.0.0", + "@typescript-eslint/eslint-plugin": "^5.59.7", + "@typescript-eslint/parser": "^5.59.7", + "@rc-component/father-plugin": "^2.0.0", + "@rc-component/np": "^1.0.3", + "@rc-component/trigger": "^3.0.0", "@testing-library/jest-dom": "^6.1.4", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", "@types/classnames": "^2.2.10", "@types/enzyme": "^3.10.5", "@types/jest": "^29.4.0", - "@types/keyv": "4.2.0", "@types/react": "^18.2.42", "@types/react-dom": "^18.0.11", "@umijs/fabric": "^4.0.1", - "coveralls": "^3.0.6", "cross-env": "^7.0.2", "dumi": "^2.0.0", "eslint": "^8.54.0", "eslint-plugin-jest": "^28.9.0", "eslint-plugin-unicorn": "^56.0.1", - "fastclick": "~1.0.6", "father": "^4.0.0", "gh-pages": "^6.1.0", - "history": "^5.3.0", - "immutability-helper": "^3.0.1", "less": "^4.1.3", - "np": "^10.0.2", - "preact-compat": "^3.16.0", + "lint-staged": "^15.5.0", + "prettier": "^3.5.3", "rc-test": "^7.0.14", "react": "^18.0.0", "react-dnd": "^10.0.0", "react-dnd-html5-backend": "^10.0.0", "react-dom": "^18.0.0", "react-sticky": "^6.0.3", - "sortablejs": "^1.7.0", "typescript": "^5.3.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" }, + "lint-staged": { + "*": "prettier --write --ignore-unknown" + }, "engines": { "node": ">=8.x" } diff --git a/src/TabNavList/OperationNode.tsx b/src/TabNavList/OperationNode.tsx index 87a44c80..5b58db4e 100644 --- a/src/TabNavList/OperationNode.tsx +++ b/src/TabNavList/OperationNode.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; -import Dropdown from 'rc-dropdown'; -import Menu, { MenuItem } from 'rc-menu'; +import Dropdown from '@rc-component/dropdown'; +import Menu, { MenuItem } from '@rc-component/menu'; import KeyCode from 'rc-util/lib/KeyCode'; import * as React from 'react'; import { useEffect, useState } from 'react'; @@ -26,6 +26,7 @@ export interface OperationNodeProps { tabMoving?: boolean; getPopupContainer?: (node: HTMLElement) => HTMLElement; popupClassName?: string; + popupStyle?: React.CSSProperties; } const OperationNode = React.forwardRef((props, ref) => { @@ -45,6 +46,7 @@ const OperationNode = React.forwardRef((prop onTabClick, getPopupContainer, popupClassName, + popupStyle, } = props; // ======================== Dropdown ======================== const [open, setOpen] = useState(false); @@ -182,7 +184,7 @@ const OperationNode = React.forwardRef((prop moreStyle.order = 1; } - const overlayClassName = classNames({ + const overlayClassName = classNames(popupClassName, { [`${dropdownPrefix}-rtl`]: rtl, }); @@ -192,7 +194,8 @@ const OperationNode = React.forwardRef((prop overlay={menu} visible={tabs.length ? open : false} onVisibleChange={setOpen} - overlayClassName={classNames(overlayClassName, popupClassName)} + overlayClassName={overlayClassName} + overlayStyle={popupStyle} mouseEnterDelay={0.1} mouseLeaveDelay={0.1} getPopupContainer={getPopupContainer} diff --git a/src/TabNavList/TabNode.tsx b/src/TabNavList/TabNode.tsx index d3c0d944..0c1e34fd 100644 --- a/src/TabNavList/TabNode.tsx +++ b/src/TabNavList/TabNode.tsx @@ -24,6 +24,7 @@ export interface TabNodeProps { onFocus: React.FocusEventHandler; onBlur: React.FocusEventHandler; style?: React.CSSProperties; + className?: string; } const TabNode: React.FC = props => { @@ -44,6 +45,7 @@ const TabNode: React.FC = props => { onMouseDown, onMouseUp, style, + className, tabCount, currentPosition, } = props; @@ -81,7 +83,7 @@ const TabNode: React.FC = props => {
> & TabNavListProps; // We have to create a TabNavList components. const TabNavListWrapper: React.FC = ({ renderTabBar, ...restProps }) => { - const { tabs } = React.useContext(TabContext); if (renderTabBar) { - const tabNavBarProps = { - ...restProps, - // Legacy support. We do not use this actually - panes: tabs.map(({ label, key, ...restTabProps }) => ( - - )), - }; - - return renderTabBar(tabNavBarProps, TabNavList); + return renderTabBar(restProps, TabNavList); } return ; diff --git a/src/TabNavList/index.tsx b/src/TabNavList/index.tsx index 332974da..1caca657 100644 --- a/src/TabNavList/index.tsx +++ b/src/TabNavList/index.tsx @@ -30,6 +30,7 @@ import AddButton from './AddButton'; import ExtraContent from './ExtraContent'; import OperationNode from './OperationNode'; import TabNode from './TabNode'; +import type { SemanticName } from '../Tabs'; export interface TabNavListProps { id: string; @@ -55,6 +56,8 @@ export interface TabNavListProps { size?: GetIndicatorSize; align?: 'start' | 'center' | 'end'; }; + classNames?: Partial>; + styles?: Partial>; } const getTabSize = (tab: HTMLElement, containerRect: { left: number; top: number }) => { @@ -109,6 +112,8 @@ const TabNavList = React.forwardRef((props, ref onTabClick, onTabScroll, indicator, + classNames: tabsClassNames, + styles, } = props; const { prefixCls, tabs } = React.useContext(TabContext); @@ -370,8 +375,9 @@ const TabNavList = React.forwardRef((props, ref // Enter & Space case 'Enter': case 'Space': { + console.log('press', code); e.preventDefault(); - onTabClick(focusKey, e); + onTabClick(activeKey, e); break; } // Backspace @@ -417,8 +423,9 @@ const TabNavList = React.forwardRef((props, ref prefixCls={prefixCls} key={key} tab={tab} + className={tabsClassNames?.item} /* first node should not have margin left */ - style={i === 0 ? undefined : tabNodeStyle} + style={i === 0 ? styles?.item : { ...tabNodeStyle, ...styles?.item }} closable={tab.closable} editable={editable} active={key === activeKey} @@ -567,8 +574,8 @@ const TabNavList = React.forwardRef((props, ref ref={useComposeRef(ref, containerRef)} role="tablist" aria-orientation={tabPositionTopOrBottom ? 'horizontal' : 'vertical'} - className={classNames(`${prefixCls}-nav`, className)} - style={style} + className={classNames(`${prefixCls}-nav`, className, tabsClassNames?.header)} + style={{ ...styles?.header, ...style }} onKeyDown={() => { // No need animation when use keyboard doLockAnimation(); @@ -607,10 +614,10 @@ const TabNavList = React.forwardRef((props, ref }} />
@@ -624,6 +631,7 @@ const TabNavList = React.forwardRef((props, ref prefixCls={prefixCls} tabs={hiddenTabs} className={!hasDropdown && operationsHiddenClassName} + popupStyle={styles?.popup} tabMoving={!!lockAnimation} /> diff --git a/src/TabPanelList/TabPane.tsx b/src/TabPanelList/TabPane.tsx index 510520d7..d1b803d3 100644 --- a/src/TabPanelList/TabPane.tsx +++ b/src/TabPanelList/TabPane.tsx @@ -23,11 +23,13 @@ export interface TabPaneProps { const TabPane = React.forwardRef((props, ref) => { const { prefixCls, className, style, id, active, tabKey, children } = props; + const hasContent = React.Children.count(children) > 0; + return (
= props => { - const { id, activeKey, animated, tabPosition, destroyInactiveTabPane } = props; + const { + id, + activeKey, + animated, + tabPosition, + destroyInactiveTabPane, + contentStyle, + contentClassName, + } = props; const { prefixCls, tabs } = React.useContext(TabContext); const tabPaneAnimated = animated.tabPane; @@ -54,8 +64,8 @@ const TabPanelList: React.FC = props => { tabKey={key} animated={tabPaneAnimated} active={active} - style={{ ...paneStyle, ...motionStyle }} - className={classNames(paneClassName, motionClassName)} + style={{ ...contentStyle, ...paneStyle, ...motionStyle }} + className={classNames(contentClassName, paneClassName, motionClassName)} ref={ref} /> )} diff --git a/src/Tabs.tsx b/src/Tabs.tsx index 7d6df768..144dd3b7 100644 --- a/src/Tabs.tsx +++ b/src/Tabs.tsx @@ -34,11 +34,15 @@ import type { // Used for accessibility let uuid = 0; +export type SemanticName = 'popup' | 'item' | 'indicator' | 'content' | 'header'; + export interface TabsProps extends Omit, 'onChange' | 'children'> { prefixCls?: string; className?: string; style?: React.CSSProperties; + classNames?: Partial>; + styles?: Partial>; id?: string; items?: Tab[]; @@ -99,6 +103,8 @@ const Tabs = React.forwardRef((props, ref) => { getPopupContainer, popupClassName, indicator, + classNames: tabsClassNames, + styles, ...restProps } = props; const tabs = React.useMemo( @@ -178,10 +184,11 @@ const Tabs = React.forwardRef((props, ref) => { onTabScroll, extra: tabBarExtraContent, style: tabBarStyle, - panes: null, getPopupContainer, - popupClassName, + popupClassName: classNames(popupClassName, tabsClassNames?.popup), indicator, + styles, + classNames: tabsClassNames, }; return ( @@ -205,6 +212,8 @@ const Tabs = React.forwardRef((props, ref) => {
diff --git a/src/interface.ts b/src/interface.ts index ea7958af..6c62e24e 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,15 +1,15 @@ -import type { CSSMotionProps } from 'rc-motion'; +import type { CSSMotionProps } from '@rc-component/motion'; +import { DropdownProps } from '@rc-component/dropdown/lib/Dropdown'; import type React from 'react'; import type { TabNavListProps } from './TabNavList'; import type { TabPaneProps } from './TabPanelList/TabPane'; -import { DropdownProps } from 'rc-dropdown/lib/Dropdown'; export type TriggerProps = { trigger?: 'hover' | 'click'; -} +}; export type moreIcon = React.ReactNode; export type MoreProps = { - icon?: moreIcon, + icon?: moreIcon; } & Omit; export type SizeInfo = [width: number, height: number]; @@ -45,14 +45,12 @@ type RenderTabBarProps = { mobile: boolean; editable: EditableConfig; locale: TabsLocale; - more: MoreProps, + more: MoreProps; tabBarGutter: number; onTabClick: (key: string, e: React.MouseEvent | React.KeyboardEvent) => void; onTabScroll: OnTabScroll; extra: TabBarExtraContent; style: React.CSSProperties; - /** @deprecated It do not pass real TabPane node. Only for compatible usage. */ - panes: React.ReactNode; }; export type RenderTabBar = ( diff --git a/tests/accessibility.test.tsx b/tests/accessibility.test.tsx index 25081cda..809a2409 100644 --- a/tests/accessibility.test.tsx +++ b/tests/accessibility.test.tsx @@ -91,9 +91,10 @@ describe('Tabs.Accessibility', () => { it('should activate tab on Enter/Space', async () => { const onTabClick = jest.fn(); + const onChange = jest.fn(); const user = userEvent.setup(); - render(createTabs({ onTabClick })); + render(createTabs({ onTabClick, onChange })); // jump to first tab await user.tab(); @@ -101,6 +102,7 @@ describe('Tabs.Accessibility', () => { // activate tab await user.keyboard(' '); expect(onTabClick).toHaveBeenCalledTimes(1); + expect(onChange).not.toHaveBeenCalled(); // move focus to second tab await user.keyboard('{ArrowRight}'); @@ -108,6 +110,7 @@ describe('Tabs.Accessibility', () => { // activate tab await user.keyboard('{Enter}'); expect(onTabClick).toHaveBeenCalledTimes(2); + expect(onChange).not.toHaveBeenCalled(); }); it('should not navigate to disabled tabs', async () => { @@ -268,4 +271,16 @@ describe('Tabs.Accessibility', () => { const firstTab = getByRole('tab', { name: /Tab1/i }); expect(firstTab).toHaveFocus(); }); + + it('should not focus on tab panel when it is empty', async () => { + const user = userEvent.setup(); + const { getByRole } = render( + , + ); + + const tabPanel = getByRole('tabpanel', { name: /Tab1/i }); + await user.tab(); + await user.tab(); + expect(tabPanel).not.toHaveFocus(); + }); }); diff --git a/tests/index.test.tsx b/tests/index.test.tsx index db5bc7d9..81cb8c99 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -310,23 +310,6 @@ describe('Tabs.Basic', () => { expect(container.querySelector('.my-node')).toBeTruthy(); expect(renderTabBar).toHaveBeenCalled(); }); - it('has panes property in props', () => { - const renderTabBar = props => { - return ( -
- {props.panes.map(pane => ( - - tab - - ))} -
- ); - }; - const { container } = render(getTabs({ renderTabBar })); - expect(container.querySelector('[data-key="light"]')).toBeTruthy(); - expect(container.querySelector('[data-key="bamboo"]')).toBeTruthy(); - expect(container.querySelector('[data-key="cute"]')).toBeTruthy(); - }); }); it('destroyInactiveTabPane', () => { @@ -706,4 +689,40 @@ describe('Tabs.Basic', () => { expect(parseInt(startBar.style.top)).toBeLessThanOrEqual(parseInt(centerBar.style.top)); expect(parseInt(centerBar.style.top)).toBeLessThanOrEqual(parseInt(endBar.style.top)); }); + it('support classnames and styles', () => { + const customClassNames = { + indicator: 'custom-indicator', + item: 'custom-item', + content: 'custom-content', + header: 'custom-header', + }; + const customStyles = { + indicator: { background: 'red' }, + item: { color: 'blue' }, + content: { background: 'green' }, + header: { background: 'yellow' }, + }; + const { container } = render( + , + ); + const indicator = container.querySelector('.rc-tabs-ink-bar') as HTMLElement; + const item = container.querySelector('.rc-tabs-tab') as HTMLElement; + const content = container.querySelector('.rc-tabs-tabpane') as HTMLElement; + const header = container.querySelector('.rc-tabs-nav') as HTMLElement; + + expect(indicator).toHaveClass('custom-indicator'); + expect(item).toHaveClass('custom-item'); + expect(content).toHaveClass('custom-content'); + expect(header).toHaveClass('custom-header'); + + expect(indicator).toHaveStyle({ background: 'red' }); + expect(item).toHaveStyle({ color: 'blue' }); + expect(content).toHaveStyle({ background: 'green' }); + expect(header).toHaveStyle({ background: 'yellow' }); + }); }); diff --git a/tests/overflow.test.tsx b/tests/overflow.test.tsx index 509bbae6..cc8165e4 100644 --- a/tests/overflow.test.tsx +++ b/tests/overflow.test.tsx @@ -95,12 +95,14 @@ describe('Tabs.Overflow', () => { it('should open dropdown on click when moreTrigger is set to click', () => { jest.useFakeTimers(); const onChange = jest.fn(); - const { container, unmount } = render(getTabs({ onChange, more: {icon: '...', trigger: 'click'} })); + const { container, unmount } = render( + getTabs({ onChange, more: { icon: '...', trigger: 'click' } }), + ); triggerResize(container); act(() => { jest.runAllTimers(); }); - const button = container.querySelector('.rc-tabs-nav-more') + const button = container.querySelector('.rc-tabs-nav-more'); fireEvent.click(button); act(() => { jest.runAllTimers(); @@ -504,7 +506,13 @@ describe('Tabs.Overflow', () => { it('should support popupClassName', () => { jest.useFakeTimers(); - const { container } = render(getTabs({ popupClassName: 'custom-popup' })); + const { container } = render( + getTabs({ + popupClassName: 'custom-popup', + classNames: { popup: 'classnames-popup' }, + styles: { popup: { color: 'red' } }, + }), + ); triggerResize(container); act(() => { @@ -516,6 +524,8 @@ describe('Tabs.Overflow', () => { jest.runAllTimers(); }); expect(document.querySelector('.rc-tabs-dropdown')).toHaveClass('custom-popup'); + expect(document.querySelector('.rc-tabs-dropdown')).toHaveClass('classnames-popup'); + expect(document.querySelector('.rc-tabs-dropdown')).toHaveStyle('color: red'); }); it('correct handle decimal', () => { diff --git a/tsconfig.json b/tsconfig.json index aeeacaa4..7a55add9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,9 @@ ], "rc-tabs": [ "src/" + ], + "@rc-component/tabs": [ + "src/" ] } },