diff --git a/.dumirc.ts b/.dumirc.ts index d31eaee7..65a8e909 100644 --- a/.dumirc.ts +++ b/.dumirc.ts @@ -1,8 +1,22 @@ import { defineConfig } from 'dumi'; +import path from 'path'; + +const basePath = process.env.GH_PAGES ? '/tabs/' : '/'; +const publicPath = basePath; export default defineConfig({ + alias: { + '@rc-component/tabs$': path.resolve('src'), + '@rc-component/tabs/es': path.resolve('src'), + '@rc-component/tabs/assets': path.resolve('assets'), + }, + mfsu: false, + favicons: ['https://avatars0.githubusercontent.com/u/9441414?s=200&v=4'], themeConfig: { name: 'Tabs', + logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', }, - mfsu: false, + outputPath: 'docs-dist', + base: basePath, + publicPath, }); diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 33b1999c..758659af 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,15 +1,2 @@ -# These are supported funding model platforms - -github: ant-design # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: ant-design # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -polar: # Replace with a single Polar username -buy_me_a_coffee: # Replace with a single Buy Me a Coffee username -thanks_dev: # Replace with a single thanks.dev username -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] +github: ant-design +open_collective: ant-design diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 94da832d..3b730ef9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,45 +1,19 @@ version: 2 updates: -- package-ecosystem: npm - directory: "/" - schedule: - interval: daily - time: "21:00" - open-pull-requests-limit: 10 - ignore: - - dependency-name: np - versions: - - 7.2.0 - - 7.3.0 - - 7.4.0 - - dependency-name: "@types/jest" - versions: - - 26.0.20 - - 26.0.21 - - 26.0.22 - - dependency-name: react-dnd - versions: - - 11.1.3 - - 13.0.0 - - 13.1.0 - - 13.1.1 - - 14.0.1 - - dependency-name: "@types/react-dom" - versions: - - 17.0.0 - - 17.0.1 - - 17.0.2 - - dependency-name: "@types/react" - versions: - - 17.0.0 - - 17.0.1 - - 17.0.2 - - 17.0.3 - - dependency-name: react-dnd-html5-backend - versions: - - 11.1.3 - - 12.1.0 - - 12.1.1 - - dependency-name: less - versions: - - 4.1.0 + - package-ecosystem: npm + directory: '/' + schedule: + interval: weekly + day: monday + time: '21:00' + timezone: Asia/Shanghai + open-pull-requests-limit: 10 + + - package-ecosystem: github-actions + directory: '/' + schedule: + interval: weekly + day: monday + time: '21:00' + timezone: Asia/Shanghai + open-pull-requests-limit: 10 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..82242dfd --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,43 @@ +name: 'CodeQL' + +on: + push: + branches: ['master'] + pull_request: + branches: ['master'] + schedule: + - cron: '17 10 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [javascript] + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Initialize CodeQL + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e + with: + category: '/language:${{ matrix.language }}' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 5735e2d2..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,6 +0,0 @@ -name: ✅ test -on: [push, pull_request] -jobs: - test: - uses: react-component/rc-test/.github/workflows/test.yml@main - secrets: inherit \ No newline at end of file diff --git a/.github/workflows/react-component-ci.yml b/.github/workflows/react-component-ci.yml new file mode 100644 index 00000000..1ed64209 --- /dev/null +++ b/.github/workflows/react-component-ci.yml @@ -0,0 +1,12 @@ +name: ✅ test +on: + push: + branches: [master] + pull_request: + branches: [master] +permissions: + contents: read +jobs: + test: + uses: react-component/rc-test/.github/workflows/test-utoo.yml@main + secrets: inherit diff --git a/.github/workflows/react-doctor.yml b/.github/workflows/react-doctor.yml new file mode 100644 index 00000000..097eb883 --- /dev/null +++ b/.github/workflows/react-doctor.yml @@ -0,0 +1,27 @@ +name: React Doctor + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + push: + branches: [master] + +permissions: + contents: read + pull-requests: write + issues: write + statuses: write + +concurrency: + group: react-doctor-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + react-doctor: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + persist-credentials: false + - uses: millionco/react-doctor@0b4f4f4bd248a154e64eb508a48347f71154b3f3 diff --git a/.github/workflows/surge-preview.yml b/.github/workflows/surge-preview.yml new file mode 100644 index 00000000..4c2f17ed --- /dev/null +++ b/.github/workflows/surge-preview.yml @@ -0,0 +1,54 @@ +name: Surge Preview + +on: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: write + checks: write + +jobs: + preview: + runs-on: ubuntu-latest + concurrency: + group: surge-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + env: + PREVIEW: true + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Check Surge token + id: surge-token + env: + SURGE_TOKEN: ${{ secrets.SURGE_TOKEN }} + run: | + if [ -n "$SURGE_TOKEN" ]; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + else + echo "enabled=false" >> "$GITHUB_OUTPUT" + fi + - name: Build preview + if: ${{ steps.surge-token.outputs.enabled == 'true' }} + run: | + npm install + npm run build + - uses: afc163/surge-preview@bf90a5a86111f6311ca42f0a5a0f80fb0fb03cec + if: ${{ steps.surge-token.outputs.enabled == 'true' }} + env: + SURGE_TOKEN: ${{ secrets.SURGE_TOKEN }} + with: + surge_token: ${{ env.SURGE_TOKEN }} + github_token: ${{ secrets.GITHUB_TOKEN }} + dist: docs-dist + failOnError: false + setCommitStatus: false + - name: Skip Surge preview + if: ${{ steps.surge-token.outputs.enabled != 'true' }} + run: echo "SURGE_TOKEN is not configured; skip Surge preview." diff --git a/.gitignore b/.gitignore index 0d12566f..dc304eff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,7 @@ -.storybook *.iml *.log *.log.* .idea/ -.vscode .ipr .iws *~ @@ -28,17 +26,24 @@ assets/**/*.css build lib es -coverage +/coverage yarn.lock package-lock.json pnpm-lock.yaml +.doc +docs-dist +.vercel +.storybook # umi .umi .umi-production .umi-test .env.local -.dumi +.dumi/ bun.lockb -bun.lock \ No newline at end of file +bun.lock +coverage +.dumi/tmp +.dumi/tmp-production diff --git a/.husky/pre-commit b/.husky/pre-commit index d0a77842..2312dc58 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -npx lint-staged \ No newline at end of file +npx lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..c466d872 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,14 @@ +node_modules +coverage +docs-dist +dist +es +lib +.dumi/tmp +.dumi/tmp-production +.vercel +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lockb +*.log diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..bd0a1f72 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-present react-component + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 76ab4366..8b89fc66 100644 --- a/README.md +++ b/README.md @@ -1,185 +1,129 @@ -# rc-tabs +
+

@rc-component/tabs

+

Ant Design Part of the Ant Design ecosystem.

+

📑 Flexible React tabs with overflow, editable items, custom tab bars, and accessible panels.

---- +

+ NPM version + npm downloads + build status + Codecov + bundle size + dumi +

+
-React Tabs component. +

English | 简体中文

-[![NPM version][npm-image]][npm-url] [![dumi](https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square)](https://github.com/umijs/dumi) [![build status][github-actions-image]][github-actions-url] [![Test coverage][codecov-image]][codecov-url] [![npm download][download-image]][download-url] [![bundle size][bundlephobia-image]][bundlephobia-url] -[npm-image]: http://img.shields.io/npm/v/rc-tabs.svg?style=flat-square -[npm-url]: http://npmjs.org/package/rc-tabs -[github-actions-image]: https://github.com/react-component/tabs/workflows/CI/badge.svg -[github-actions-url]: https://github.com/react-component/tabs/actions -[codecov-image]: https://img.shields.io/codecov/c/github/react-component/tabs/master.svg?style=flat-square -[codecov-url]: https://codecov.io/gh/react-component/tabs/branch/master -[download-image]: https://img.shields.io/npm/dm/rc-tabs.svg?style=flat-square -[download-url]: https://npmjs.org/package/rc-tabs -[bundlephobia-url]: https://bundlephobia.com/result?p=rc-tabs -[bundlephobia-image]: https://badgen.net/bundlephobia/minzip/rc-tabs +## Highlights -## Screenshot +- Supports top, bottom, left, and right tab positions with RTL layouts. +- Handles overflow with a dropdown operation menu. +- Supports editable tabs, extra tab bar content, indicators, and custom tab bars. +- Provides semantic `classNames` and `styles` slots for panel and navigation customization. - +## Install -## Example - -http://localhost:8000/examples - -online example: https://tabs.react-component.now.sh/ - -## install - -[![rc-tabs](https://nodei.co/npm/rc-tabs.png)](https://npmjs.org/package/rc-tabs) +```bash +npm install @rc-component/tabs +``` -## Feature +## Usage -### Keyboard +```tsx pure +import Tabs from '@rc-component/tabs'; +import '@rc-component/tabs/assets/index.css'; -- left and up: tabs to previous tab -- right and down: tabs to next tab +const items = [ + { key: 'overview', label: 'Overview', children: 'Overview content' }, + { key: 'settings', label: 'Settings', children: 'Settings content' }, +]; -## Usage +export default () => ; +``` -```tsx | pure -import Tabs from 'rc-tabs'; -import ReactDom from 'react-dom'; +Online preview: https://tabs.react-component.vercel.app/ -const callback = key => { - console.log(key); -}; +## Examples -const items = [ - { - key: '1', - label: 'Google', - children: ( -
-

Lorem Ipsum is simply dummy text of the printing and typesetting

-
- ), - }, - { - key: '2', - label:

Amazon

, - children: - 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit...', - disabled: true, - }, - { - key: '3', - label:

Twitter

, - children: ( -
- "There is no one who loves pain itself, who seeks after it and wants to have it, simply - because it is pain..." -
- ), - }, -]; +Run the local dumi site: -ReactDom.render( - , - root, -); +```bash +npm install +npm start ``` +Then open `http://localhost:8000`. + ## API ### Tabs -| name | type | default | description | -| --- | --- | --- | --- | -| prefixCls | string | `'rc-tabs'` | prefix class name, use to custom style | -| className | string | - | to define a class name for an element | -| style | CSS properties | - | object with css properties for styling | -| items | TabItem[] | [] | configure tab content | -| id | string | - | unique identifier | -| defaultActiveKey | string | - | initial active tabPanel's key if activeKey is absent | -| activeKey | string | - | current active tabPanel's key | -| direction | `'ltr' or 'rtl'` | `'ltr'` | Layout direction of tabs component | -| animated | boolean \| { inkBar: boolean, tabPane: boolean } | `{ inkBar: true, tabPane: false }` | config animation | -| renderTabBar | (props, TabBarComponent) => ReactElement | - | How to render tab bar | -| tabBarExtraContent | ReactNode \| `{ left: ReactNode, right: ReactNode }` | - | config extra content | -| tabBarGutter | number | 0 | config tab bar gutter | -| tabBarPosition | `'left' \| 'right' \| 'top' \| 'bottom'` | `'top'` | tab nav 's position | -| tabBarStyle | style | - | tab nav style | -| tabPosition | `'left' or 'right' or 'top' or 'bottom'` | `'top'` | tab nav 's position | -| destroyOnHidden | boolean | false | whether destroy inactive TabPane when change tab | -| onChange | (key) => void | - | called when tabPanel is changed | -| onTabClick | (key) => void | - | called when tab click | -| onTabScroll | ({ direction }) => void | - | called when tab scroll | -| editable | { onEdit(type: 'add' \| 'remove', info: { key, event }), showAdd: boolean, removeIcon: ReactNode, addIcon: ReactNode } | - | config tab editable | -| locale | { dropdownAriaLabel: string, removeAriaLabel: string, addAriaLabel: string } | - | Accessibility locale help text | -| moreIcon | ReactNode | - | collapse icon | - -### TabItem - -| name | type | default | description | +| Name | Type | Default | Description | | --- | --- | --- | --- | -| key | string | - | corresponding to activeKey, should be unique | -| label | string | - | TabPane's head display text | -| tab | ReactNode | - | current tab's title corresponding to current tabPane | -| className | string | - | to define a class name for an element | -| style | CSS properties | - | object with css properties for styling | -| disabled | boolean | false | set TabPane disabled | -| children | ReactNode | - | TabPane's head display content | -| forceRender | boolean | false | forced render of content in tabs, not lazy render after clicking on tabs | -| closable | boolean | false | closable feature of tab item | -| closeIcon | ReactNode | - | Config close icon | -| prefixCls | string | `'rc-tabs-tab'` | prefix class name, use to custom style | -| id | string | - | unique identifier | -| animated | boolean \| { inkBar: boolean, tabPane: boolean } | `{ inkBar: true, tabPane: false }` | config animation | -| destroyOnHidden | boolean | false | whether destroy inactive TabPane when change tab | -| active | boolean | false | active feature of tab item | -| tabKey | string | - | key linked to tab | - -### TabPane(support in older versions) - -| name | type | default | description | +| `activeKey` | string | - | Controlled active tab key. | +| `animated` | boolean \| AnimatedConfig | `{ inkBar: true, tabPane: false }` | Animation config. | +| `className` | string | - | Additional class name. | +| `classNames` | `Partial>` | - | Semantic class names. | +| `defaultActiveKey` | string | - | Initial active tab key. | +| `destroyOnHidden` | boolean | false | Destroy inactive tab panels. | +| `direction` | `'ltr' \| 'rtl'` | `'ltr'` | Layout direction. | +| `editable` | EditableConfig | - | Editable tab configuration. | +| `getPopupContainer` | `(node: HTMLElement) => HTMLElement` | - | Popup container resolver. | +| `id` | string | - | Root id. | +| `indicator` | `{ size?: GetIndicatorSize; align?: 'start' \| 'center' \| 'end' }` | - | Indicator size and alignment. | +| `items` | Tab[] | [] | Tab items. | +| `locale` | TabsLocale | - | Accessibility locale text. | +| `more` | MoreProps | - | Overflow dropdown config. | +| `onChange` | `(activeKey: string) => void` | - | Triggered when active tab changes. | +| `onTabClick` | `(activeKey, event) => void` | - | Triggered when a tab is clicked. | +| `onTabScroll` | `({ direction }) => void` | - | Triggered when tab navigation scrolls. | +| `prefixCls` | string | `'rc-tabs'` | Prefix class name. | +| `renderTabBar` | RenderTabBar | - | Custom tab bar renderer. | +| `style` | React.CSSProperties | - | Root style. | +| `styles` | `Partial>` | - | Semantic styles. | +| `tabBarExtraContent` | React.ReactNode \| TabBarExtraMap | - | Extra content beside the tab bar. | +| `tabBarGutter` | number | 0 | Gap between tabs. | +| `tabBarStyle` | React.CSSProperties | - | Tab bar style. | +| `tabPosition` | `'left' \| 'right' \| 'top' \| 'bottom'` | `'top'` | Tab position. | + +### Tab + +| Name | Type | Default | Description | | --- | --- | --- | --- | -| destroyOnHidden | boolean | false | whether destroy inactive TabPane when change tab | -| key | string | - | corresponding to activeKey, should be unique | -| forceRender | boolean | false | forced render of content in tabs, not lazy render after clicking on tabs | -| tab | ReactNode | - | current tab's title corresponding to current tabPane | -| closeIcon | ReactNode | - | Config close icon | +| `children` | React.ReactNode | - | Tab panel content. | +| `className` | string | - | Panel class name. | +| `closable` | boolean | - | Whether the tab can be closed in editable mode. | +| `closeIcon` | React.ReactNode | - | Custom close icon. | +| `destroyOnHidden` | boolean | false | Destroy inactive panel. | +| `disabled` | boolean | false | Disable the tab. | +| `forceRender` | boolean | false | Render panel before it becomes active. | +| `key` | string | - | Required unique tab key. | +| `label` | React.ReactNode | - | Tab label. | +| `style` | React.CSSProperties | - | Panel style. | ## Development -``` +```bash npm install npm start -``` - -## Test Case - -``` npm test -npm run chrome-test +npm run tsc +npm run compile +npm run build ``` -## Coverage +The dumi site runs at `http://localhost:8000` by default. -``` -npm run coverage +## Release + +```bash +npm run prepublishOnly ``` -open coverage/ dir +The release flow is handled by `@rc-component/np` through the `rc-np` command after the package build. ## License -rc-tabs is released under the MIT license. - -## FAQ - -### Responsive Tabs - -There are 3 cases when handling responsive tabs: ![image](https://user-images.githubusercontent.com/27722486/156315099-7e6eda9d-ab77-4b16-9b49-1727c5ec8b26.png) - -We get hidden tabs through [useVisibleRange.ts](https://github.com/react-component/tabs/blob/master/src/hooks/useVisibleRange.ts). If enconter the third case, in order to make tabs responsive, some tabs should be hidden. So we minus `addSize` when calculating `basicSize` manully, even though there's no addNode in container. In this way, case 3 turns to case 2, tabs become stable again. +@rc-component/tabs is released under the [MIT](./LICENSE) license. diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 00000000..c6d5c424 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,129 @@ +
+

@rc-component/tabs

+

Ant Design Ant Design 生态的一部分。

+

📑 React 标签页组件,支持可编辑标签、溢出滚动和自定义渲染。

+ +

+ NPM version + npm downloads + build status + Codecov + bundle size + dumi +

+
+ +

English | 简体中文

+ + +## 特性 + +- 支持 RTL 布局的顶部、底部、左侧和右侧选项卡位置。 +- 使用下拉操作菜单处理溢出。 +- 支持可编辑选项卡、额外选项卡栏内容、指示器和自定义选项卡栏。 +- 为面板和导航自定义提供语义 `classNames` 和 `styles` 插槽。 + +## 安装 + +```bash +npm install @rc-component/tabs +``` + +## 使用 + +```tsx pure +import Tabs from '@rc-component/tabs'; +import '@rc-component/tabs/assets/index.css'; + +const items = [ + { key: 'overview', label: 'Overview', children: 'Overview content' }, + { key: 'settings', label: 'Settings', children: 'Settings content' }, +]; + +export default () => ; +``` + +在线预览:https://tabs.react-component.vercel.app/ + +## 示例 + +运行本地 dumi 站点: + +```bash +npm install +npm start +``` + +然后打开 `http://localhost:8000`。 + +## API + +### Tabs + +| 名称 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `activeKey` | string | - | 受控的活动 Tab 键。 | +| `animated` | boolean \| AnimatedConfig | `{ inkBar: true, tabPane: false }` | 动画配置。 | +| `className` | string | - | 附加 className。 | +| `classNames` | `Partial>` | - | 语义化类名。 | +| `defaultActiveKey` | string | - | 初始活动 Tab 键。 | +| `destroyOnHidden` | boolean | false | 销毁非活动标签页面板。 | +| `direction` | `'ltr' \| 'rtl'` | `'ltr'` | 布局方向。 | +| `editable` | EditableConfig | - | 可编辑标签页配置。 | +| `getPopupContainer` | `(node: HTMLElement) => HTMLElement` | - | 弹层容器解析器。 | +| `id` | string | - | 根 ID。 | +| `indicator` | `{ size?: GetIndicatorSize; align?: 'start' \| 'center' \| 'end' }` | - | 指示器尺寸和对齐方式。 | +| `items` | Tab[] | [] | 选项卡项目。 | +| `locale` | TabsLocale | - | 无障碍本地化文本。 | +| `more` | MoreProps | - | 溢出下拉菜单配置。 | +| `onChange` | `(activeKey: string) => void` | - | 当活动选项卡更改时触发。 | +| `onTabClick` | `(activeKey, event) => void` | - | 单击选项卡时触发。 | +| `onTabScroll` | `({ direction }) => void` | - | 当选项卡导航滚动时触发。 | +| `prefixCls` | string | `'rc-tabs'` | 前缀 className。 | +| `renderTabBar` | RenderTabBar | - | 自定义标签栏渲染函数。 | +| `style` | React.CSSProperties | - | 根样式。 | +| `styles` | `Partial>` | - | 语义化样式。 | +| `tabBarExtraContent` | React.ReactNode \| TabBarExtraMap | - | 标签栏旁的额外内容。 | +| `tabBarGutter` | number | 0 | 选项卡之间的间隙。 | +| `tabBarStyle` | React.CSSProperties | - | 标签栏样式。 | +| `tabPosition` | `'left' \| 'right' \| 'top' \| 'bottom'` | `'top'` | 标签页位置。 | + +### Tab + +| 名称 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `children` | React.ReactNode | - | 选项卡面板内容。 | +| `className` | string | - | 面板 className。 | +| `closable` | boolean | - | 是否可以在可编辑模式下关闭选项卡。 | +| `closeIcon` | React.ReactNode | - | 自定义关闭图标。 | +| `destroyOnHidden` | boolean | false | 销毁非活动面板。 | +| `disabled` | boolean | false | 禁用该选项卡。 | +| `forceRender` | boolean | false | 在面板变为活动状态之前渲染面板。 | +| `key` | string | - | 需要唯一的 Tab 键。 | +| `label` | React.ReactNode | - | Tab 标签内容。 | +| `style` | React.CSSProperties | - | 面板样式。 | + +## 本地开发 + +```bash +npm install +npm start +npm test +npm run tsc +npm run compile +npm run build +``` + +dumi 站点默认运行在 `http://localhost:8000`。 + +## 发布 + +```bash +npm run prepublishOnly +``` + +包构建完成后,发布流程由 `@rc-component/np` 通过 `rc-np` 命令处理。 + +## 许可证 + +@rc-component/tabs 基于 [MIT](./LICENSE) 许可证发布。 diff --git a/docs/examples/animated.tsx b/docs/examples/animated.tsx index f6118acb..be380713 100644 --- a/docs/examples/animated.tsx +++ b/docs/examples/animated.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Tabs from '../../src'; +import Tabs from '@rc-component/tabs'; 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 80546a2c..12328d58 100644 --- a/docs/examples/basic.tsx +++ b/docs/examples/basic.tsx @@ -1,6 +1,6 @@ import React from 'react'; import '../../assets/index.less'; -import Tabs from '../../src'; +import Tabs from '@rc-component/tabs'; export default () => { const [destroy, setDestroy] = React.useState(false); diff --git a/docs/examples/dynamic-extra.tsx b/docs/examples/dynamic-extra.tsx index 9db9721d..abc8c13b 100644 --- a/docs/examples/dynamic-extra.tsx +++ b/docs/examples/dynamic-extra.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import Tabs from '../../src'; -import type { TabsProps } from '../../src'; +import Tabs from '@rc-component/tabs'; +import type { TabsProps } from '@rc-component/tabs'; import '../../assets/index.less'; const items: TabsProps['items'] = []; @@ -16,22 +16,20 @@ export default () => { const extra = React.useMemo(() => { if (key === '0') { - return ( -
额外内容
- ) - } - return null - }, [key]) + return
额外内容
; + } + return null; + }, [key]); return (
- setKey(curKey)} - tabBarExtraContent={extra} - defaultActiveKey="8" - items={items} + onChange={curKey => setKey(curKey)} + tabBarExtraContent={extra} + defaultActiveKey="8" + items={items} />
); -}; \ No newline at end of file +}; diff --git a/docs/examples/editable.tsx b/docs/examples/editable.tsx index 409e0130..63efb3cf 100644 --- a/docs/examples/editable.tsx +++ b/docs/examples/editable.tsx @@ -1,5 +1,5 @@ import React, { useRef } from 'react'; -import Tabs from '../../src'; +import Tabs from '@rc-component/tabs'; import '../../assets/index.less'; export default () => { diff --git a/docs/examples/extra.tsx b/docs/examples/extra.tsx index 143fe53a..77890baa 100644 --- a/docs/examples/extra.tsx +++ b/docs/examples/extra.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import Tabs from '../../src'; -import type { TabsProps } from '../../src'; +import Tabs from '@rc-component/tabs'; +import type { TabsProps } from '@rc-component/tabs'; import '../../assets/index.less'; const items: TabsProps['items'] = []; diff --git a/docs/examples/indicator.tsx b/docs/examples/indicator.tsx index 90642451..d4d95131 100644 --- a/docs/examples/indicator.tsx +++ b/docs/examples/indicator.tsx @@ -1,7 +1,7 @@ import React from 'react'; import '../../assets/index.less'; -import type { TabsProps } from '../../src'; -import Tabs from '../../src'; +import type { TabsProps } from '@rc-component/tabs'; +import Tabs from '@rc-component/tabs'; const items: TabsProps['items'] = [ { diff --git a/docs/examples/mix.tsx b/docs/examples/mix.tsx index 1621db20..d066d781 100644 --- a/docs/examples/mix.tsx +++ b/docs/examples/mix.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import Tabs from '../../src'; -import type { TabsProps } from '../../src'; +import Tabs from '@rc-component/tabs'; +import type { TabsProps } from '@rc-component/tabs'; import '../../assets/index.less'; function getTabPanes(count = 50) { diff --git a/docs/examples/overflow.tsx b/docs/examples/overflow.tsx index b1b271f2..4ed9e71f 100644 --- a/docs/examples/overflow.tsx +++ b/docs/examples/overflow.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import Tabs from '../../src'; -import type { TabsProps } from '../../src'; +import Tabs from '@rc-component/tabs'; +import type { TabsProps } from '@rc-component/tabs'; import '../../assets/index.less'; const items: TabsProps['items'] = []; diff --git a/docs/examples/position.tsx b/docs/examples/position.tsx index f49d7b50..d1d070ad 100644 --- a/docs/examples/position.tsx +++ b/docs/examples/position.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import Tabs from '../../src'; +import Tabs from '@rc-component/tabs'; import '../../assets/index.less'; export default () => { diff --git a/docs/examples/renderTabBar-dragable.tsx b/docs/examples/renderTabBar-dragable.tsx index 9d4a38f5..5f526b8b 100644 --- a/docs/examples/renderTabBar-dragable.tsx +++ b/docs/examples/renderTabBar-dragable.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { DndProvider, DragSource, DropTarget } from 'react-dnd'; import HTML5Backend from 'react-dnd-html5-backend'; -import Tabs from '../../src'; -import type { TabsProps } from '../../src'; +import Tabs from '@rc-component/tabs'; +import type { TabsProps } from '@rc-component/tabs'; import '../../assets/index.less'; // Drag & Drop node diff --git a/docs/examples/renderTabBar-sticky.tsx b/docs/examples/renderTabBar-sticky.tsx index bd70e7a0..9a9da6f5 100644 --- a/docs/examples/renderTabBar-sticky.tsx +++ b/docs/examples/renderTabBar-sticky.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { StickyContainer, Sticky } from 'react-sticky'; -import Tabs from '../../src'; +import Tabs from '@rc-component/tabs'; import '../../assets/index.less'; const renderTabBar = (props, DefaultTabBar) => ( diff --git a/docs/index.md b/docs/index.md index eea4076d..ee9df64b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,5 @@ --- -title: rc-tabs +title: '@rc-component/tabs' --- - \ No newline at end of file + diff --git a/package.json b/package.json index 346b949c..3c8ecf3e 100644 --- a/package.json +++ b/package.json @@ -1,41 +1,44 @@ { "name": "@rc-component/tabs", "version": "1.11.0", - "description": "tabs ui component for react", + "description": "Tabs UI component for React", "keywords": [ "react", "react-component", "react-tabs" ], - "homepage": "http://github.com/react-component/tabs", + "homepage": "https://react-component.github.io/tabs", "bugs": { - "url": "http://github.com/react-component/tabs/issues" + "url": "https://github.com/react-component/tabs/issues" }, "repository": { "type": "git", - "url": "git@github.com:react-component/tabs.git" + "url": "https://github.com/react-component/tabs.git" }, "license": "MIT", "author": "yiminghe@gmail.com", "main": "./lib/index", "module": "./es/index", "files": [ + "assets/*.css", "lib", - "es", - "assets/index.css" + "es" ], "scripts": { - "build": "dumi build", + "build": "npm run compile && npm run docs:build", "compile": "father build && npm run compile:style", "compile:style": "lessc --js assets/index.less assets/index.css", - "coverage": "father test --coverage", - "docs:deploy": "gh-pages -d dist", + "coverage": "rc-test --coverage", + "docs:build": "dumi build", + "docs:deploy": "gh-pages -d docs-dist", + "gh-pages": "npm run compile && cross-env GH_PAGES=1 npm run docs:build && npm run docs:deploy", "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 && rc-np", + "prepublishOnly": "npm run compile && rc-np", + "prettier": "prettier --write --ignore-unknown .", "start": "dumi dev", "test": "rc-test", - "prepare": "husky && dumi setup" + "tsc": "tsc --noEmit", + "prepare": "husky" }, "dependencies": { "@rc-component/dropdown": "~1.0.0", @@ -47,42 +50,48 @@ }, "devDependencies": { "@rc-component/father-plugin": "^2.2.0", - "@rc-component/np": "^1.0.3", - "@testing-library/jest-dom": "^6.1.4", - "@testing-library/react": "^16.0.1", - "@testing-library/user-event": "^14.5.2", + "@rc-component/np": "^1.0.4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^15.0.7", + "@testing-library/user-event": "14.5.2", "@types/enzyme": "^3.10.5", - "@types/jest": "^30.0.0", - "@types/node": "^24.5.2", - "@types/react": "^18.2.42", + "@types/jest": "^29.5.14", + "@types/node": "^26.0.1", + "@types/react": "^18.3.31", "@types/react-dom": "^18.3.7", - "@typescript-eslint/eslint-plugin": "^5.59.7", - "@typescript-eslint/parser": "^5.59.7", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", "@umijs/fabric": "^4.0.1", - "dumi": "^2.0.0", - "eslint": "^8.54.0", - "father": "^4.0.0", - "gh-pages": "^6.1.0", + "dumi": "^2.4.35", + "eslint": "^8.57.1", + "father": "^4.6.23", + "gh-pages": "^6.3.0", "husky": "^9.1.7", - "less": "^4.1.3", - "lint-staged": "^16.2.3", - "prettier": "^3.5.3", - "rc-test": "^7.0.14", - "react": "^18.0.0", + "less": "^4.6.7", + "lint-staged": "^16.4.0", + "prettier": "^3.9.0", + "rc-test": "^7.1.3", + "react": "^18.3.1", "react-dnd": "^10.0.0", "react-dnd-html5-backend": "^10.0.0", - "react-dom": "^18.0.0", + "react-dom": "^18.3.1", "react-sticky": "^6.0.3", - "typescript": "^5.3.2" + "typescript": "^5.9.3", + "cross-env": "^10.1.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" }, - "lint-staged": { - "*": "prettier --write --ignore-unknown" - }, "engines": { "node": ">=8.x" + }, + "types": "./es/index.d.ts", + "style": "./assets/index.css", + "publishConfig": { + "access": "public" + }, + "lint-staged": { + "*": "prettier --write --ignore-unknown" } } diff --git a/tests/accessibility.test.tsx b/tests/accessibility.test.tsx index fbee0fa6..385d889c 100644 --- a/tests/accessibility.test.tsx +++ b/tests/accessibility.test.tsx @@ -139,22 +139,30 @@ describe('Tabs.Accessibility', () => { it('should distinguish between keyboard and mouse navigation', async () => { const user = userEvent.setup(); - const { getByRole } = render(createTabs()); + const { getByRole, unmount } = render(createTabs()); const secondTab = getByRole('tab', { name: /Tab2/i }); - const fourthTab = getByRole('tab', { name: /Tab4/i }); // mouse click should not add focus style - await user.click(secondTab); + fireEvent.mouseDown(secondTab); + fireEvent.click(secondTab); + fireEvent.mouseUp(secondTab); expect(secondTab.parentElement).not.toHaveClass('rc-tabs-tab-focus'); - // clear focus - await user.click(document.body); + unmount(); + + const { getByRole: getByRoleWithKeyboard } = render(createTabs()); + const firstTab = getByRoleWithKeyboard('tab', { name: /Tab1/i }); + const secondKeyboardTab = getByRoleWithKeyboard('tab', { name: /Tab2/i }); + const fourthTab = getByRoleWithKeyboard('tab', { name: /Tab4/i }); // keyboard navigation should add focus style await user.tab(); // default focus active tab - expect(secondTab.parentElement).toHaveClass('rc-tabs-tab-focus'); + expect(firstTab.parentElement).toHaveClass('rc-tabs-tab-focus'); + + await user.keyboard('{ArrowRight}'); + expect(secondKeyboardTab.parentElement).toHaveClass('rc-tabs-tab-focus'); await user.keyboard('{ArrowRight}'); // skip disabled tab diff --git a/tsconfig.json b/tsconfig.json index 7a55add9..0dfc8d55 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,17 +18,31 @@ "@@/*": [ ".dumi/tmp/*" ], - "rc-tabs": [ - "src/" - ], "@rc-component/tabs": [ - "src/" + "src/index.ts" + ], + "@rc-component/tabs/es": [ + "src" + ], + "@rc-component/tabs/es/*": [ + "src/*" + ], + "@rc-component/tabs/assets/*": [ + "assets/*" ] - } + }, + "ignoreDeprecations": "5.0" }, "include": [ ".dumirc.ts", - "**/*.ts", - "**/*.tsx" + ".fatherrc.ts", + "src", + "tests", + "docs/examples" + ], + "exclude": [ + "docs-dist", + "lib", + "es" ] -} \ No newline at end of file +} diff --git a/vercel.json b/vercel.json new file mode 100644 index 00000000..5f9139ef --- /dev/null +++ b/vercel.json @@ -0,0 +1,6 @@ +{ + "framework": "umijs", + "installCommand": "npm install", + "buildCommand": "npm run build", + "outputDirectory": "docs-dist" +}