Skip to content

Commit

Permalink
feat: custom indicator length (#671)
Browse files Browse the repository at this point in the history
* feat: custom indicator length

* fix: useEffect deps

* test: add test case

* chore: rename

* chore: update test case
  • Loading branch information
MadCcc authored Aug 24, 2023
1 parent 61e2aec commit b9902be
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 48 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ es
coverage
yarn.lock
package-lock.json
pnpm-lock.yaml

# umi
.umi
Expand Down
8 changes: 8 additions & 0 deletions docs/demo/indicator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: Indicator
nav:
title: Demo
path: /demo
---

<code src="../examples/indicator.tsx"></code>
35 changes: 35 additions & 0 deletions docs/examples/indicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import Tabs from'../../src';
import '../../assets/index.less';

export default () => {
const [destroy, setDestroy] = React.useState(false);
const [items, setItems] = React.useState([
{
label: 'Light',
key: 'light',
children: 'Light!',
},
{
label: 'Bamboo',
key: 'bamboo',
children: 'Bamboo!',
},
{
label: 'Cute',
key: 'cute',
children: 'Cute!',
disabled: true,
},
]);

if (destroy) {
return null;
}

return (
<React.StrictMode>
<Tabs tabBarExtraContent="extra" items={items} getIndicatorLength={(origin) => origin - 16} />
</React.StrictMode>
);
};
47 changes: 11 additions & 36 deletions src/TabNavList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import classNames from 'classnames';
import ResizeObserver from 'rc-resize-observer';
import useEvent from 'rc-util/lib/hooks/useEvent';
import raf from 'rc-util/lib/raf';
import { useComposeRef } from 'rc-util/lib/ref';
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
Expand All @@ -27,6 +26,8 @@ import AddButton from './AddButton';
import ExtraContent from './ExtraContent';
import OperationNode from './OperationNode';
import TabNode from './TabNode';
import useIndicator from '../hooks/useIndicator';
import type { GetIndicatorLength } from '../hooks/useIndicator';

export interface TabNavListProps {
id: string;
Expand All @@ -49,6 +50,7 @@ export interface TabNavListProps {
children?: (node: React.ReactElement) => React.ReactElement;
getPopupContainer?: (node: HTMLElement) => HTMLElement;
popupClassName?: string;
indicatorLength?: GetIndicatorLength;
}

const getSize = (refObj: React.RefObject<HTMLElement>): SizeInfo => {
Expand Down Expand Up @@ -80,6 +82,7 @@ function TabNavList(props: TabNavListProps, ref: React.Ref<HTMLDivElement>) {
children,
onTabClick,
onTabScroll,
indicatorLength,
} = props;
const containerRef = useRef<HTMLDivElement>();
const extraLeftRef = useRef<HTMLDivElement>();
Expand Down Expand Up @@ -361,41 +364,13 @@ function TabNavList(props: TabNavListProps, ref: React.Ref<HTMLDivElement>) {
const hiddenTabs = [...startHiddenTabs, ...endHiddenTabs];

// =================== Link & Operations ===================
const [inkStyle, setInkStyle] = useState<React.CSSProperties>();

const activeTabOffset = tabOffsets.get(activeKey);

// Delay set ink style to avoid remove tab blink
const inkBarRafRef = useRef<number>();
function cleanInkBarRaf() {
raf.cancel(inkBarRafRef.current);
}

useEffect(() => {
const newInkStyle: React.CSSProperties = {};

if (activeTabOffset) {
if (tabPositionTopOrBottom) {
if (rtl) {
newInkStyle.right = activeTabOffset.right;
} else {
newInkStyle.left = activeTabOffset.left;
}

newInkStyle.width = activeTabOffset.width;
} else {
newInkStyle.top = activeTabOffset.top;
newInkStyle.height = activeTabOffset.height;
}
}

cleanInkBarRaf();
inkBarRafRef.current = raf(() => {
setInkStyle(newInkStyle);
});

return cleanInkBarRaf;
}, [activeTabOffset, tabPositionTopOrBottom, rtl]);
const { style: indicatorStyle } = useIndicator({
activeTabOffset,
horizontal: tabPositionTopOrBottom,
rtl,
indicatorLength,
})

// ========================= Effect ========================
useEffect(() => {
Expand Down Expand Up @@ -485,7 +460,7 @@ function TabNavList(props: TabNavListProps, ref: React.Ref<HTMLDivElement>) {
className={classNames(`${prefixCls}-ink-bar`, {
[`${prefixCls}-ink-bar-animated`]: animated.inkBar,
})}
style={inkStyle}
style={indicatorStyle}
/>
</div>
</ResizeObserver>
Expand Down
17 changes: 10 additions & 7 deletions src/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ import isMobile from 'rc-util/lib/isMobile';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import TabPanelList from './TabPanelList';
import type {
TabPosition,
RenderTabBar,
TabsLocale,
EditableConfig,
AnimatedConfig,
EditableConfig,
OnTabScroll,
RenderTabBar,
Tab,
TabBarExtraContent,
TabPosition,
TabsLocale,
} from './interface';
import TabContext from './TabContext';
import TabNavListWrapper from './TabNavList/Wrapper';
import useAnimateConfig from './hooks/useAnimateConfig';
import type { GetIndicatorLength } from './hooks/useIndicator';

/**
* Should added antd:
Expand Down Expand Up @@ -68,6 +69,9 @@ export interface TabsProps
moreTransitionName?: string;

popupClassName?: string;

// Indicator
indicatorLength?: GetIndicatorLength;
}

function Tabs(
Expand Down Expand Up @@ -95,6 +99,7 @@ function Tabs(
onTabScroll,
getPopupContainer,
popupClassName,
indicatorLength,
...restProps
}: TabsProps,
ref: React.Ref<HTMLDivElement>,
Expand Down Expand Up @@ -166,8 +171,6 @@ function Tabs(
mobile,
};

let tabNavBar: React.ReactElement;

const tabNavBarProps = {
...sharedProps,
editable,
Expand All @@ -182,6 +185,7 @@ function Tabs(
panes: null,
getPopupContainer,
popupClassName,
indicatorLength,
};

return (
Expand All @@ -201,7 +205,6 @@ function Tabs(
)}
{...restProps}
>
{tabNavBar}
<TabNavListWrapper {...tabNavBarProps} renderTabBar={renderTabBar} />
<TabPanelList
destroyInactiveTabPane={destroyInactiveTabPane}
Expand Down
73 changes: 73 additions & 0 deletions src/hooks/useIndicator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { useEffect, useRef, useState } from 'react';
import raf from 'rc-util/lib/raf';
import { TabOffset } from '../interface';

export type GetIndicatorLength = number | ((origin: number) => number);

export type UseIndicator = (options: {
activeTabOffset: TabOffset,
horizontal: boolean;
rtl: boolean;
indicatorLength: GetIndicatorLength;
}) => {
style: React.CSSProperties;
}

const useIndicator: UseIndicator = ({
activeTabOffset,
horizontal,
rtl,
indicatorLength,
}) => {
const [inkStyle, setInkStyle] = useState<React.CSSProperties>();
const inkBarRafRef = useRef<number>();

const getLength = (origin: number) => {
if (typeof indicatorLength === 'function') {
return indicatorLength(origin);
}
if (typeof indicatorLength === 'number') {
return indicatorLength;
}
return origin;
}

// Delay set ink style to avoid remove tab blink
function cleanInkBarRaf() {
raf.cancel(inkBarRafRef.current);
}

useEffect(() => {
const newInkStyle: React.CSSProperties = {};

if (activeTabOffset) {
if (horizontal) {
if (rtl) {
newInkStyle.right = activeTabOffset.right + activeTabOffset.width / 2;
newInkStyle.transform = 'translateX(50%)';
} else {
newInkStyle.left = activeTabOffset.left + activeTabOffset.width / 2;
newInkStyle.transform = 'translateX(-50%)';
}
newInkStyle.width = getLength(activeTabOffset.width);
} else {
newInkStyle.top = activeTabOffset.top + activeTabOffset.height / 2;
newInkStyle.transform = 'translateY(-50%)';
newInkStyle.height = getLength(activeTabOffset.height);
}
}

cleanInkBarRaf();
inkBarRafRef.current = raf(() => {
setInkStyle(newInkStyle);
});

return cleanInkBarRaf;
}, [activeTabOffset, horizontal, rtl, indicatorLength]);

return {
style: inkStyle,
}
}

export default useIndicator;
12 changes: 8 additions & 4 deletions tests/common/util.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ export interface HackInfo {
add?: number;
more?: number;
extra?: number;
dropdown?: number;
}

export function getOffsetSizeFunc(info: HackInfo = {}) {
return function getOffsetSize() {
const { container = 50, extra = 10, tabNode = 20, add = 10, more = 10 } = info;
const { container = 50, extra = 10, tabNode = 20, add = 10, more = 10, dropdown = 10 } = info;

if (this.classList.contains('rc-tabs-nav')) {
return container;
Expand Down Expand Up @@ -70,9 +71,9 @@ export function getOffsetSizeFunc(info: HackInfo = {}) {
// if (this.className.includes('rc-tabs-nav-more')) {
// return info.more || 10;
// }
// if (this.className.includes('rc-tabs-dropdown')) {
// return info.dropdown || 10;
// }
if (this.className.includes('rc-tabs-dropdown')) {
return dropdown;
}

throw new Error(`className not match ${this.className}`);
};
Expand All @@ -81,6 +82,9 @@ export function getOffsetSizeFunc(info: HackInfo = {}) {
export function btnOffsetPosition() {
// eslint-disable-next-line @typescript-eslint/no-invalid-this
const btn = this as HTMLButtonElement;
if (!btn.parentNode) {
return 0;
}
const btnList = Array.from(btn.parentNode.childNodes).filter(ele =>
(ele as HTMLElement).className.includes('rc-tabs-tab'),
);
Expand Down
12 changes: 11 additions & 1 deletion tests/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import React from 'react';
import Tabs from '../src';
import type { TabsProps } from '../src/Tabs';
import type { HackInfo } from './common/util';
import { getOffsetSizeFunc } from './common/util';
import { getOffsetSizeFunc, waitFakeTimer } from './common/util';

global.animated = null;

Expand Down Expand Up @@ -583,4 +583,14 @@ describe('Tabs.Basic', () => {
it('key could be number', () => {
render(<Tabs items={[{key: 1 as any, label: 'test'}]} />)
})

it('support getIndicatorLength', async () => {
const { container, rerender } = render(getTabs({ indicatorLength: 10 }));
await waitFakeTimer();
expect(container.querySelector('.rc-tabs-ink-bar')).toHaveStyle({ width: '10px' });

rerender(getTabs({ indicatorLength: (origin) => origin - 2 }));
await waitFakeTimer();
expect(container.querySelector('.rc-tabs-ink-bar')).toHaveStyle({ width: '18px' });
})
});

1 comment on commit b9902be

@vercel
Copy link

@vercel vercel bot commented on b9902be Aug 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

tabs – ./

tabs-git-master-react-component.vercel.app
tabs-react-component.vercel.app

Please sign in to comment.