Skip to content

Commit bae4fc3

Browse files
feat(Tabs): added animations (#11767)
* feat(Tabs): added animations * Updated snapshots * Added popperProps and updated cypress tests * Fixed docs error * Testing cypress update for test failure * Added wait call in test * Upated cypress tests * Adjusted cypress tests again * Skip cypress tests * Added animation class from core bump * Bumped Core to prerelease 20
1 parent 5c6511c commit bae4fc3

File tree

16 files changed

+251
-110
lines changed

16 files changed

+251
-110
lines changed

packages/react-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"tslib": "^2.8.1"
5555
},
5656
"devDependencies": {
57-
"@patternfly/patternfly": "6.3.0-prerelease.15",
57+
"@patternfly/patternfly": "6.3.0-prerelease.20",
5858
"case-anything": "^3.1.2",
5959
"css": "^3.0.0",
6060
"fs-extra": "^11.3.0"

packages/react-core/src/components/Tabs/OverflowTab.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,31 @@ import { TabsContext } from './TabsContext';
88
import { TabProps } from './Tab';
99
import { TabTitleText } from './TabTitleText';
1010

11+
export interface HorizontalOverflowPopperProps {
12+
/** Vertical direction of the popper. If enableFlip is set to true, this will set the initial direction before the popper flips. */
13+
direction?: 'up' | 'down';
14+
/** Horizontal position of the popper */
15+
position?: 'right' | 'left' | 'center' | 'start' | 'end';
16+
/** Custom width of the popper. If the value is "trigger", it will set the width to the select toggle's width */
17+
width?: string | 'trigger';
18+
/** Minimum width of the popper. If the value is "trigger", it will set the min width to the select toggle's width */
19+
minWidth?: string | 'trigger';
20+
/** Maximum width of the popper. If the value is "trigger", it will set the max width to the select toggle's width */
21+
maxWidth?: string | 'trigger';
22+
/** Enable to flip the popper when it reaches the boundary */
23+
enableFlip?: boolean;
24+
/** The container to append the select to. Defaults to document.body.
25+
* If your select is being cut off you can append it to an element higher up the DOM tree.
26+
* Some examples:
27+
* appendTo="inline"
28+
* appendTo={() => document.body}
29+
* appendTo={document.getElementById('target')}
30+
*/
31+
appendTo?: HTMLElement | (() => HTMLElement) | 'inline';
32+
/** Flag to prevent the popper from overflowing its container and becoming partially obscured. */
33+
preventOverflow?: boolean;
34+
}
35+
1136
export interface OverflowTabProps extends React.HTMLProps<HTMLLIElement> {
1237
/** Additional classes added to the overflow tab */
1338
className?: string;
@@ -25,6 +50,8 @@ export interface OverflowTabProps extends React.HTMLProps<HTMLLIElement> {
2550
shouldPreventScrollOnItemFocus?: boolean;
2651
/** Time in ms to wait before firing the toggles' focus event. Defaults to 0 */
2752
focusTimeoutDelay?: number;
53+
/** Additional props to spread to the popper menu. */
54+
popperProps?: HorizontalOverflowPopperProps;
2855
}
2956

3057
export const OverflowTab: React.FunctionComponent<OverflowTabProps> = ({
@@ -36,6 +63,7 @@ export const OverflowTab: React.FunctionComponent<OverflowTabProps> = ({
3663
zIndex = 9999,
3764
shouldPreventScrollOnItemFocus = true,
3865
focusTimeoutDelay = 0,
66+
popperProps,
3967
...props
4068
}: OverflowTabProps) => {
4169
const menuRef = useRef<HTMLDivElement>(undefined);
@@ -148,6 +176,7 @@ export const OverflowTab: React.FunctionComponent<OverflowTabProps> = ({
148176
minWidth="revert"
149177
appendTo={overflowLIRef.current}
150178
zIndex={zIndex}
179+
{...popperProps}
151180
/>
152181
</Fragment>
153182
);

packages/react-core/src/components/Tabs/Tab.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useContext, forwardRef } from 'react';
1+
import { useContext, forwardRef, useEffect } from 'react';
22
import styles from '@patternfly/react-styles/css/components/Tabs/tabs';
33
import { OUIAProps } from '../../helpers';
44
import { TabButton } from './TabButton';
@@ -75,7 +75,7 @@ const TabBase: React.FunctionComponent<TabProps> = ({
7575
}),
7676
{}
7777
);
78-
const { mountOnEnter, localActiveKey, unmountOnExit, uniqueId, handleTabClick, handleTabClose } =
78+
const { mountOnEnter, localActiveKey, unmountOnExit, uniqueId, setAccentStyles, handleTabClick, handleTabClose } =
7979
useContext(TabsContext);
8080
let ariaControls = tabContentId ? `${tabContentId}` : `pf-tab-section-${eventKey}-${childId || uniqueId}`;
8181
if ((mountOnEnter || unmountOnExit) && eventKey !== localActiveKey) {
@@ -116,6 +116,10 @@ const TabBase: React.FunctionComponent<TabProps> = ({
116116
</TabButton>
117117
);
118118

119+
useEffect(() => {
120+
setAccentStyles(true);
121+
}, [title, actions]);
122+
119123
return (
120124
<li
121125
className={css(

packages/react-core/src/components/Tabs/Tabs.tsx

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,36 @@ import { PickOptional } from '../../helpers/typeUtils';
55
import AngleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-left-icon';
66
import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon';
77
import PlusIcon from '@patternfly/react-icons/dist/esm/icons/plus-icon';
8-
import { getUniqueId, isElementInView, formatBreakpointMods, getLanguageDirection } from '../../helpers/util';
8+
import {
9+
getUniqueId,
10+
isElementInView,
11+
formatBreakpointMods,
12+
getLanguageDirection,
13+
getInlineStartProperty
14+
} from '../../helpers/util';
915
import { TabContent } from './TabContent';
1016
import { TabProps } from './Tab';
1117
import { TabsContextProvider } from './TabsContext';
12-
import { OverflowTab } from './OverflowTab';
18+
import { OverflowTab, HorizontalOverflowPopperProps } from './OverflowTab';
1319
import { Button } from '../Button';
1420
import { getOUIAProps, OUIAProps, getDefaultOUIAId, canUseDOM } from '../../helpers';
1521
import { GenerateId } from '../../helpers/GenerateId/GenerateId';
22+
import linkAccentLength from '@patternfly/react-tokens/dist/esm/c_tabs_link_accent_length';
23+
import linkAccentStart from '@patternfly/react-tokens/dist/esm/c_tabs_link_accent_start';
1624

1725
export enum TabsComponent {
1826
div = 'div',
1927
nav = 'nav'
2028
}
21-
2229
export interface HorizontalOverflowObject {
2330
/** Flag which shows the count of overflowing tabs when enabled */
2431
showTabCount?: boolean;
2532
/** The text which displays when an overflowing tab isn't selected */
2633
defaultTitleText?: string;
2734
/** The aria label applied to the button which toggles the tab overflow menu */
2835
toggleAriaLabel?: string;
36+
/** Additional props to spread to the popper menu. */
37+
popperProps?: HorizontalOverflowPopperProps;
2938
}
3039

3140
type TabElement = React.ReactElement<TabProps, React.JSXElementConstructor<TabProps>>;
@@ -139,6 +148,9 @@ interface TabsState {
139148
uncontrolledIsExpandedLocal: boolean;
140149
ouiaStateId: string;
141150
overflowingTabCount: number;
151+
isInitializingAccent: boolean;
152+
currentLinkAccentLength: string;
153+
currentLinkAccentStart: string;
142154
}
143155

144156
class Tabs extends Component<TabsProps, TabsState> {
@@ -158,7 +170,10 @@ class Tabs extends Component<TabsProps, TabsState> {
158170
uncontrolledActiveKey: this.props.defaultActiveKey,
159171
uncontrolledIsExpandedLocal: this.props.defaultIsExpanded,
160172
ouiaStateId: getDefaultOUIAId(Tabs.displayName),
161-
overflowingTabCount: 0
173+
overflowingTabCount: 0,
174+
isInitializingAccent: true,
175+
currentLinkAccentLength: linkAccentLength.value,
176+
currentLinkAccentStart: linkAccentStart.value
162177
};
163178

164179
if (this.props.isVertical && this.props.expandable !== undefined) {
@@ -328,30 +343,68 @@ class Tabs extends Component<TabsProps, TabsState> {
328343
}
329344
};
330345

346+
setAccentStyles = (shouldInitializeStyle?: boolean) => {
347+
const currentItem = this.tabList.current.querySelector('li.pf-m-current') as HTMLElement;
348+
if (!currentItem) {
349+
return;
350+
}
351+
352+
const { isVertical } = this.props;
353+
const { offsetWidth, offsetHeight, offsetTop } = currentItem;
354+
const lengthValue = isVertical ? offsetHeight : offsetWidth;
355+
const startValue = isVertical ? offsetTop : getInlineStartProperty(currentItem, this.tabList.current);
356+
this.setState({
357+
currentLinkAccentLength: `${lengthValue}px`,
358+
currentLinkAccentStart: `${startValue}px`,
359+
...(shouldInitializeStyle && { isInitializingAccent: true })
360+
});
361+
362+
setTimeout(() => {
363+
this.setState({ isInitializingAccent: false });
364+
}, 0);
365+
};
366+
367+
handleResize = () => {
368+
this.handleScrollButtons();
369+
this.setAccentStyles();
370+
};
371+
331372
componentDidMount() {
332373
if (!this.props.isVertical) {
333374
if (canUseDOM) {
334-
window.addEventListener('resize', this.handleScrollButtons, false);
375+
window.addEventListener('resize', this.handleResize, false);
335376
}
336377
this.direction = getLanguageDirection(this.tabList.current);
337378
// call the handle resize function to check if scroll buttons should be shown
338379
this.handleScrollButtons();
339380
}
381+
382+
this.setAccentStyles(true);
340383
}
341384

342385
componentWillUnmount() {
343386
if (!this.props.isVertical) {
344387
if (canUseDOM) {
345-
window.removeEventListener('resize', this.handleScrollButtons, false);
388+
window.removeEventListener('resize', this.handleResize, false);
346389
}
347390
}
348391
clearTimeout(this.scrollTimeout);
349392
this.leftScrollButtonRef.current?.removeEventListener('transitionend', this.hideScrollButtons);
350393
}
351394

352395
componentDidUpdate(prevProps: TabsProps, prevState: TabsState) {
353-
const { activeKey, mountOnEnter, isOverflowHorizontal, children } = this.props;
354-
const { shownKeys, overflowingTabCount, enableScrollButtons } = this.state;
396+
this.direction = getLanguageDirection(this.tabList.current);
397+
const { activeKey, mountOnEnter, isOverflowHorizontal, children, defaultActiveKey } = this.props;
398+
const { shownKeys, overflowingTabCount, enableScrollButtons, uncontrolledActiveKey } = this.state;
399+
const isOnCloseUpdate = !!prevProps.onClose !== !!this.props.onClose;
400+
if (
401+
(defaultActiveKey !== undefined && prevState.uncontrolledActiveKey !== uncontrolledActiveKey) ||
402+
(defaultActiveKey === undefined && prevProps.activeKey !== activeKey) ||
403+
isOnCloseUpdate
404+
) {
405+
this.setAccentStyles(isOnCloseUpdate);
406+
}
407+
355408
if (prevProps.activeKey !== activeKey && mountOnEnter && shownKeys.indexOf(activeKey) < 0) {
356409
this.setState({
357410
shownKeys: shownKeys.concat(activeKey)
@@ -364,6 +417,7 @@ class Tabs extends Component<TabsProps, TabsState> {
364417
Children.toArray(prevProps.children).length !== Children.toArray(children).length
365418
) {
366419
this.handleScrollButtons();
420+
this.setAccentStyles(true);
367421
}
368422

369423
const currentOverflowingTabCount = this.countOverflowingElements(this.tabList.current);
@@ -380,8 +434,6 @@ class Tabs extends Component<TabsProps, TabsState> {
380434
} else if (prevState.enableScrollButtons && !enableScrollButtons) {
381435
this.setState({ showScrollButtons: false });
382436
}
383-
384-
this.direction = getLanguageDirection(this.tabList.current);
385437
}
386438

387439
static getDerivedStateFromProps(nextProps: TabsProps, prevState: TabsState) {
@@ -450,7 +502,10 @@ class Tabs extends Component<TabsProps, TabsState> {
450502
shownKeys,
451503
uncontrolledActiveKey,
452504
uncontrolledIsExpandedLocal,
453-
overflowingTabCount
505+
overflowingTabCount,
506+
isInitializingAccent,
507+
currentLinkAccentLength,
508+
currentLinkAccentStart
454509
} = this.state;
455510
const filteredChildren = Children.toArray(children)
456511
.filter((child): child is TabElement => isValidElement(child))
@@ -485,6 +540,7 @@ class Tabs extends Component<TabsProps, TabsState> {
485540
unmountOnExit,
486541
localActiveKey,
487542
uniqueId,
543+
setAccentStyles: this.setAccentStyles,
488544
handleTabClick: (...args) => this.handleTabClick(...args),
489545
handleTabClose: onClose
490546
}}
@@ -493,6 +549,7 @@ class Tabs extends Component<TabsProps, TabsState> {
493549
aria-label={ariaLabel}
494550
className={css(
495551
styles.tabs,
552+
styles.modifiers.animateCurrent,
496553
isFilled && styles.modifiers.fill,
497554
isSubtab && styles.modifiers.subtab,
498555
isVertical && styles.modifiers.vertical,
@@ -505,10 +562,12 @@ class Tabs extends Component<TabsProps, TabsState> {
505562
formatBreakpointMods(inset, styles),
506563
variantStyle[variant],
507564
hasOverflowTab && styles.modifiers.overflow,
565+
isInitializingAccent && styles.modifiers.initializingAccent,
508566
className
509567
)}
510568
{...getOUIAProps(Tabs.displayName, ouiaId !== undefined ? ouiaId : this.state.ouiaStateId, ouiaSafe)}
511569
id={id && id}
570+
style={{ [linkAccentLength.name]: currentLinkAccentLength, [linkAccentStart.name]: currentLinkAccentStart }}
512571
{...props}
513572
>
514573
{expandable && isVertical && (

packages/react-core/src/components/Tabs/TabsContext.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface TabsContextProps {
66
unmountOnExit: boolean;
77
localActiveKey: string | number;
88
uniqueId: string;
9+
setAccentStyles: (shouldInitializeStyles?: boolean) => void;
910
handleTabClick: (
1011
event: React.MouseEvent<HTMLElement, MouseEvent>,
1112
eventKey: number | string,
@@ -24,6 +25,7 @@ export const TabsContext = createContext<TabsContextProps>({
2425
unmountOnExit: false,
2526
localActiveKey: '',
2627
uniqueId: '',
28+
setAccentStyles: () => null,
2729
handleTabClick: () => null,
2830
handleTabClose: undefined
2931
});

packages/react-core/src/components/Tabs/__tests__/Tabs.test.tsx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { render, screen } from '@testing-library/react';
1+
import { render, screen, act } from '@testing-library/react';
22
import { Tabs } from '../Tabs';
3+
import styles from '@patternfly/react-styles/css/components/Tabs/tabs';
34
import { Tab } from '../Tab';
45
import { TabTitleText } from '../TabTitleText';
56
import { TabTitleIcon } from '../TabTitleIcon';
@@ -8,6 +9,47 @@ import { TabContentBody } from '../TabContentBody';
89

910
jest.mock('../../../helpers/GenerateId/GenerateId');
1011

12+
test(`Renders with classes ${styles.tabs} and ${styles.modifiers.animateCurrent} by default`, () => {
13+
render(
14+
<Tabs role="region">
15+
<Tab title="Test title" eventKey={0}>
16+
Tab Content
17+
</Tab>
18+
</Tabs>
19+
);
20+
21+
expect(screen.getByRole('region')).toHaveClass(`${styles.tabs} ${styles.modifiers.animateCurrent}`);
22+
});
23+
24+
test(`Renders with class ${styles.modifiers.initializingAccent} when component initially mounts`, () => {
25+
render(
26+
<Tabs role="region">
27+
<Tab title="Test title" eventKey={0}>
28+
Tab Content
29+
</Tab>
30+
</Tabs>
31+
);
32+
33+
expect(screen.getByRole('region')).toHaveClass(styles.modifiers.initializingAccent);
34+
});
35+
36+
test(`Does not render with class ${styles.modifiers.initializingAccent} when component is finished mounting`, () => {
37+
jest.useFakeTimers();
38+
render(
39+
<Tabs role="region">
40+
<Tab title="Test title" eventKey={0}>
41+
Tab Content
42+
</Tab>
43+
</Tabs>
44+
);
45+
46+
act(() => {
47+
jest.advanceTimersByTime(500);
48+
});
49+
expect(screen.getByRole('region')).not.toHaveClass(styles.modifiers.initializingAccent);
50+
jest.useRealTimers();
51+
});
52+
1153
test('should render simple tabs', () => {
1254
const { asFragment } = render(
1355
<Tabs id="simpleTabs">

0 commit comments

Comments
 (0)