Skip to content

Commit ad68648

Browse files
author
Eric Olkowski
committed
feat(Tabs): added animations
1 parent e8c5538 commit ad68648

4 files changed

Lines changed: 102 additions & 11 deletions

File tree

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: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,22 @@ 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';
1218
import { OverflowTab } 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',
@@ -139,6 +147,9 @@ interface TabsState {
139147
uncontrolledIsExpandedLocal: boolean;
140148
ouiaStateId: string;
141149
overflowingTabCount: number;
150+
isInitializingAccent: boolean;
151+
currentLinkAccentLength: string;
152+
currentLinkAccentStart: string;
142153
}
143154

144155
class Tabs extends Component<TabsProps, TabsState> {
@@ -158,7 +169,10 @@ class Tabs extends Component<TabsProps, TabsState> {
158169
uncontrolledActiveKey: this.props.defaultActiveKey,
159170
uncontrolledIsExpandedLocal: this.props.defaultIsExpanded,
160171
ouiaStateId: getDefaultOUIAId(Tabs.displayName),
161-
overflowingTabCount: 0
172+
overflowingTabCount: 0,
173+
isInitializingAccent: true,
174+
currentLinkAccentLength: linkAccentLength.value,
175+
currentLinkAccentStart: linkAccentStart.value
162176
};
163177

164178
if (this.props.isVertical && this.props.expandable !== undefined) {
@@ -328,30 +342,68 @@ class Tabs extends Component<TabsProps, TabsState> {
328342
}
329343
};
330344

345+
setAccentStyles = (shouldInitializeStyle?: boolean) => {
346+
const currentItem = this.tabList.current.querySelector('li.pf-m-current') as HTMLElement;
347+
if (!currentItem) {
348+
return;
349+
}
350+
351+
const { isVertical } = this.props;
352+
const { offsetWidth, offsetHeight, offsetTop } = currentItem;
353+
const lengthValue = isVertical ? offsetHeight : offsetWidth;
354+
const startValue = isVertical ? offsetTop : getInlineStartProperty(currentItem, this.tabList.current);
355+
this.setState({
356+
currentLinkAccentLength: `${lengthValue}px`,
357+
currentLinkAccentStart: `${startValue}px`,
358+
...(shouldInitializeStyle && { isInitializingAccent: true })
359+
});
360+
361+
setTimeout(() => {
362+
this.setState({ isInitializingAccent: false });
363+
}, 0);
364+
};
365+
366+
handleResize = () => {
367+
this.handleScrollButtons();
368+
this.setAccentStyles();
369+
};
370+
331371
componentDidMount() {
332372
if (!this.props.isVertical) {
333373
if (canUseDOM) {
334-
window.addEventListener('resize', this.handleScrollButtons, false);
374+
window.addEventListener('resize', this.handleResize, false);
335375
}
336376
this.direction = getLanguageDirection(this.tabList.current);
337377
// call the handle resize function to check if scroll buttons should be shown
338378
this.handleScrollButtons();
339379
}
380+
381+
this.setAccentStyles(true);
340382
}
341383

342384
componentWillUnmount() {
343385
if (!this.props.isVertical) {
344386
if (canUseDOM) {
345-
window.removeEventListener('resize', this.handleScrollButtons, false);
387+
window.removeEventListener('resize', this.handleResize, false);
346388
}
347389
}
348390
clearTimeout(this.scrollTimeout);
349391
this.leftScrollButtonRef.current?.removeEventListener('transitionend', this.hideScrollButtons);
350392
}
351393

352394
componentDidUpdate(prevProps: TabsProps, prevState: TabsState) {
353-
const { activeKey, mountOnEnter, isOverflowHorizontal, children } = this.props;
354-
const { shownKeys, overflowingTabCount, enableScrollButtons } = this.state;
395+
this.direction = getLanguageDirection(this.tabList.current);
396+
const { activeKey, mountOnEnter, isOverflowHorizontal, children, defaultActiveKey } = this.props;
397+
const { shownKeys, overflowingTabCount, enableScrollButtons, uncontrolledActiveKey } = this.state;
398+
const isOnCloseUpdate = !!prevProps.onClose !== !!this.props.onClose;
399+
if (
400+
(defaultActiveKey !== undefined && prevState.uncontrolledActiveKey !== uncontrolledActiveKey) ||
401+
(defaultActiveKey === undefined && prevProps.activeKey !== activeKey) ||
402+
isOnCloseUpdate
403+
) {
404+
this.setAccentStyles(isOnCloseUpdate);
405+
}
406+
355407
if (prevProps.activeKey !== activeKey && mountOnEnter && shownKeys.indexOf(activeKey) < 0) {
356408
this.setState({
357409
shownKeys: shownKeys.concat(activeKey)
@@ -364,6 +416,7 @@ class Tabs extends Component<TabsProps, TabsState> {
364416
Children.toArray(prevProps.children).length !== Children.toArray(children).length
365417
) {
366418
this.handleScrollButtons();
419+
this.setAccentStyles(true);
367420
}
368421

369422
const currentOverflowingTabCount = this.countOverflowingElements(this.tabList.current);
@@ -380,8 +433,6 @@ class Tabs extends Component<TabsProps, TabsState> {
380433
} else if (prevState.enableScrollButtons && !enableScrollButtons) {
381434
this.setState({ showScrollButtons: false });
382435
}
383-
384-
this.direction = getLanguageDirection(this.tabList.current);
385436
}
386437

387438
static getDerivedStateFromProps(nextProps: TabsProps, prevState: TabsState) {
@@ -450,7 +501,10 @@ class Tabs extends Component<TabsProps, TabsState> {
450501
shownKeys,
451502
uncontrolledActiveKey,
452503
uncontrolledIsExpandedLocal,
453-
overflowingTabCount
504+
overflowingTabCount,
505+
isInitializingAccent,
506+
currentLinkAccentLength,
507+
currentLinkAccentStart
454508
} = this.state;
455509
const filteredChildren = Children.toArray(children)
456510
.filter((child): child is TabElement => isValidElement(child))
@@ -485,6 +539,7 @@ class Tabs extends Component<TabsProps, TabsState> {
485539
unmountOnExit,
486540
localActiveKey,
487541
uniqueId,
542+
setAccentStyles: this.setAccentStyles,
488543
handleTabClick: (...args) => this.handleTabClick(...args),
489544
handleTabClose: onClose
490545
}}
@@ -505,10 +560,12 @@ class Tabs extends Component<TabsProps, TabsState> {
505560
formatBreakpointMods(inset, styles),
506561
variantStyle[variant],
507562
hasOverflowTab && styles.modifiers.overflow,
563+
isInitializingAccent && styles.modifiers.initializingAccent,
508564
className
509565
)}
510566
{...getOUIAProps(Tabs.displayName, ouiaId !== undefined ? ouiaId : this.state.ouiaStateId, ouiaSafe)}
511567
id={id && id}
568+
style={{ [linkAccentLength.name]: currentLinkAccentLength, [linkAccentStart.name]: currentLinkAccentStart }}
512569
{...props}
513570
>
514571
{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/helpers/util.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,3 +562,31 @@ export const getReferenceElement = (refProp: HTMLElement | (() => HTMLElement) |
562562

563563
return refProp?.current;
564564
};
565+
566+
/**
567+
* Gets the [client|offset|scroll]Left property of an element, and determines whether it needs to be
568+
* adjusted for an RTL direction.
569+
*
570+
* @param {HTMLElement} targetElement - Element to get the inline-start property of.
571+
* @param {HTMLElement} ancestorElement - Ancestor element to base the inline-start calculation off of when the direction is RTL.
572+
* @param {'client' | 'offset' | 'scroll'} inlineType - The inline-start property type to base calculations on.
573+
* @returns {number} - The value of the inline-start property.
574+
*/
575+
export const getInlineStartProperty = (
576+
targetElement: HTMLElement,
577+
ancestorElement: HTMLElement,
578+
inlineType: 'client' | 'offset' | 'scroll' = 'offset'
579+
) => {
580+
if (!targetElement) {
581+
return;
582+
}
583+
584+
const inlineProperty: 'offsetLeft' | 'clientLeft' | 'scrollLeft' = `${inlineType}Left`;
585+
const isRTL = getLanguageDirection(targetElement) === 'rtl';
586+
if (!isRTL) {
587+
return targetElement[inlineProperty];
588+
}
589+
590+
const widthProperty: 'offsetWidth' | 'clientWidth' | 'scrollWidth' = `${inlineType}Width`;
591+
return ancestorElement[widthProperty] - (targetElement[inlineProperty] + targetElement[widthProperty]);
592+
};

0 commit comments

Comments
 (0)