@patternfly/react-core 4.227.8 → 4.228.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/esm/components/Menu/MenuItem.d.ts +3 -1
  3. package/dist/esm/components/Menu/MenuItem.d.ts.map +1 -1
  4. package/dist/esm/components/Menu/MenuItem.js +7 -5
  5. package/dist/esm/components/Menu/MenuItem.js.map +1 -1
  6. package/dist/esm/components/MenuToggle/MenuToggle.d.ts +6 -12
  7. package/dist/esm/components/MenuToggle/MenuToggle.d.ts.map +1 -1
  8. package/dist/esm/components/MenuToggle/MenuToggle.js +12 -8
  9. package/dist/esm/components/MenuToggle/MenuToggle.js.map +1 -1
  10. package/dist/esm/components/Nav/NavItem.d.ts.map +1 -1
  11. package/dist/esm/components/Nav/NavItem.js +5 -1
  12. package/dist/esm/components/Nav/NavItem.js.map +1 -1
  13. package/dist/esm/components/Tabs/OverflowTab.d.ts +16 -0
  14. package/dist/esm/components/Tabs/OverflowTab.d.ts.map +1 -0
  15. package/dist/esm/components/Tabs/OverflowTab.js +74 -0
  16. package/dist/esm/components/Tabs/OverflowTab.js.map +1 -0
  17. package/dist/esm/components/Tabs/Tabs.d.ts +12 -0
  18. package/dist/esm/components/Tabs/Tabs.d.ts.map +1 -1
  19. package/dist/esm/components/Tabs/Tabs.js +36 -13
  20. package/dist/esm/components/Tabs/Tabs.js.map +1 -1
  21. package/dist/esm/components/TextInputGroup/TextInputGroup.d.ts +2 -0
  22. package/dist/esm/components/TextInputGroup/TextInputGroup.d.ts.map +1 -1
  23. package/dist/esm/components/TextInputGroup/TextInputGroup.js +2 -2
  24. package/dist/esm/components/TextInputGroup/TextInputGroup.js.map +1 -1
  25. package/dist/esm/components/TextInputGroup/TextInputGroupMain.d.ts +1 -1
  26. package/dist/esm/components/TextInputGroup/TextInputGroupMain.d.ts.map +1 -1
  27. package/dist/esm/components/TreeView/TreeView.d.ts +2 -0
  28. package/dist/esm/components/TreeView/TreeView.d.ts.map +1 -1
  29. package/dist/esm/components/TreeView/TreeView.js +3 -3
  30. package/dist/esm/components/TreeView/TreeView.js.map +1 -1
  31. package/dist/esm/components/TreeView/TreeViewListItem.d.ts +2 -0
  32. package/dist/esm/components/TreeView/TreeViewListItem.d.ts.map +1 -1
  33. package/dist/esm/components/TreeView/TreeViewListItem.js +23 -14
  34. package/dist/esm/components/TreeView/TreeViewListItem.js.map +1 -1
  35. package/dist/js/components/Menu/MenuItem.d.ts +3 -1
  36. package/dist/js/components/Menu/MenuItem.d.ts.map +1 -1
  37. package/dist/js/components/Menu/MenuItem.js +7 -5
  38. package/dist/js/components/Menu/MenuItem.js.map +1 -1
  39. package/dist/js/components/MenuToggle/MenuToggle.d.ts +6 -12
  40. package/dist/js/components/MenuToggle/MenuToggle.d.ts.map +1 -1
  41. package/dist/js/components/MenuToggle/MenuToggle.js +12 -8
  42. package/dist/js/components/MenuToggle/MenuToggle.js.map +1 -1
  43. package/dist/js/components/Nav/NavItem.d.ts.map +1 -1
  44. package/dist/js/components/Nav/NavItem.js +5 -1
  45. package/dist/js/components/Nav/NavItem.js.map +1 -1
  46. package/dist/js/components/Tabs/OverflowTab.d.ts +16 -0
  47. package/dist/js/components/Tabs/OverflowTab.d.ts.map +1 -0
  48. package/dist/js/components/Tabs/OverflowTab.js +78 -0
  49. package/dist/js/components/Tabs/OverflowTab.js.map +1 -0
  50. package/dist/js/components/Tabs/Tabs.d.ts +12 -0
  51. package/dist/js/components/Tabs/Tabs.d.ts.map +1 -1
  52. package/dist/js/components/Tabs/Tabs.js +36 -13
  53. package/dist/js/components/Tabs/Tabs.js.map +1 -1
  54. package/dist/js/components/TextInputGroup/TextInputGroup.d.ts +2 -0
  55. package/dist/js/components/TextInputGroup/TextInputGroup.d.ts.map +1 -1
  56. package/dist/js/components/TextInputGroup/TextInputGroup.js +2 -2
  57. package/dist/js/components/TextInputGroup/TextInputGroup.js.map +1 -1
  58. package/dist/js/components/TextInputGroup/TextInputGroupMain.d.ts +1 -1
  59. package/dist/js/components/TextInputGroup/TextInputGroupMain.d.ts.map +1 -1
  60. package/dist/js/components/TreeView/TreeView.d.ts +2 -0
  61. package/dist/js/components/TreeView/TreeView.d.ts.map +1 -1
  62. package/dist/js/components/TreeView/TreeView.js +3 -3
  63. package/dist/js/components/TreeView/TreeView.js.map +1 -1
  64. package/dist/js/components/TreeView/TreeViewListItem.d.ts +2 -0
  65. package/dist/js/components/TreeView/TreeViewListItem.d.ts.map +1 -1
  66. package/dist/js/components/TreeView/TreeViewListItem.js +23 -14
  67. package/dist/js/components/TreeView/TreeViewListItem.js.map +1 -1
  68. package/dist/styles/base-no-reset.css +9 -0
  69. package/dist/styles/base.css +9 -0
  70. package/dist/umd/react-core.css +767 -777
  71. package/dist/umd/react-core.js +159 -52
  72. package/dist/umd/react-core.min.css +507 -517
  73. package/dist/umd/react-core.min.js +5 -5
  74. package/package.json +6 -6
  75. package/src/components/Menu/MenuItem.tsx +11 -5
  76. package/src/components/MenuToggle/MenuToggle.tsx +63 -25
  77. package/src/components/Nav/NavItem.tsx +6 -0
  78. package/src/components/Tabs/OverflowTab.tsx +141 -0
  79. package/src/components/Tabs/Tabs.tsx +72 -25
  80. package/src/components/Tabs/examples/Tabs.md +6 -1
  81. package/src/components/Tabs/examples/TabsHorizontalOverflow.tsx +67 -0
  82. package/src/components/TextInputGroup/TextInputGroup.tsx +9 -1
  83. package/src/components/TextInputGroup/TextInputGroupMain.tsx +1 -1
  84. package/src/components/TreeView/TreeView.tsx +5 -0
  85. package/src/components/TreeView/TreeViewListItem.tsx +31 -17
  86. package/src/components/TreeView/__tests__/__snapshots__/TreeView.test.tsx.snap +299 -299
  87. package/src/components/TreeView/examples/TreeView.md +125 -7
  88. package/src/demos/ComposableMenu/ComposableMenu.md +8 -0
  89. package/src/demos/ComposableMenu/examples/ComposableTypeaheadSelect.tsx +196 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patternfly/react-core",
3
- "version": "4.227.8",
3
+ "version": "4.228.1",
4
4
  "description": "This library provides a set of common React components for use with the PatternFly reference implementation.",
5
5
  "main": "dist/js/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -34,16 +34,16 @@
34
34
  "generate": "node scripts/copyStyles.js"
35
35
  },
36
36
  "dependencies": {
37
- "@patternfly/react-icons": "^4.78.8",
38
- "@patternfly/react-styles": "^4.77.8",
39
- "@patternfly/react-tokens": "^4.79.8",
37
+ "@patternfly/react-icons": "^4.79.1",
38
+ "@patternfly/react-styles": "^4.78.1",
39
+ "@patternfly/react-tokens": "^4.80.1",
40
40
  "focus-trap": "6.9.2",
41
41
  "react-dropzone": "9.0.0",
42
42
  "tippy.js": "5.1.2",
43
43
  "tslib": "^2.0.0"
44
44
  },
45
45
  "devDependencies": {
46
- "@patternfly/patternfly": "4.204.3",
46
+ "@patternfly/patternfly": "4.206.1",
47
47
  "@rollup/plugin-commonjs": "^21.0.0",
48
48
  "@rollup/plugin-node-resolve": "^13.0.0",
49
49
  "@rollup/plugin-replace": "^3.0.0",
@@ -60,5 +60,5 @@
60
60
  "react": "^16.8.0 || ^17.0.0",
61
61
  "react-dom": "^16.8.0 || ^17.0.0"
62
62
  },
63
- "gitHead": "09b80fb1de7a57f0917554a36d2e7422d70f551f"
63
+ "gitHead": "f6c791bad9fa1bc817102fea17d5ba7243cd6121"
64
64
  }
@@ -50,6 +50,8 @@ export interface MenuItemProps extends Omit<React.HTMLProps<HTMLLIElement>, 'onC
50
50
  isExternalLink?: boolean;
51
51
  /** Flag indicating if the option is selected */
52
52
  isSelected?: boolean;
53
+ /** Flag indicating the item is focused */
54
+ isFocused?: boolean;
53
55
  /** @beta Flyout menu */
54
56
  flyoutMenu?: React.ReactElement;
55
57
  /** @beta Callback function when mouse leaves trigger */
@@ -88,6 +90,7 @@ const MenuItemBase: React.FunctionComponent<MenuItemProps> = ({
88
90
  isDisabled = false,
89
91
  isExternalLink = false,
90
92
  isSelected = null,
93
+ isFocused,
91
94
  icon,
92
95
  actions,
93
96
  onShowFlyout,
@@ -189,11 +192,12 @@ const MenuItemBase: React.FunctionComponent<MenuItemProps> = ({
189
192
  }
190
193
  }, [flyoutVisible, flyoutTarget]);
191
194
 
192
- const handleFlyout = (event: React.KeyboardEvent) => {
193
- const key = event.key;
195
+ const handleFlyout = (event: React.KeyboardEvent | React.MouseEvent) => {
196
+ const key = (event as React.KeyboardEvent).key;
194
197
  const target = event.target;
198
+ const type = event.type;
195
199
 
196
- if (key === ' ' || key === 'Enter' || key === 'ArrowRight') {
200
+ if (key === ' ' || key === 'Enter' || key === 'ArrowRight' || type === 'click') {
197
201
  event.stopPropagation();
198
202
  if (!flyoutVisible) {
199
203
  showFlyout(true);
@@ -248,7 +252,7 @@ const MenuItemBase: React.FunctionComponent<MenuItemProps> = ({
248
252
  if (isOnPath) {
249
253
  additionalProps['aria-expanded'] = true;
250
254
  } else if (hasFlyout) {
251
- additionalProps['aria-haspopup'] = true;
255
+ additionalProps['aria-haspopup'] = 'menu';
252
256
  additionalProps['aria-expanded'] = flyoutVisible;
253
257
  }
254
258
  const getAriaCurrent = () => {
@@ -290,6 +294,7 @@ const MenuItemBase: React.FunctionComponent<MenuItemProps> = ({
290
294
  _isOnPath && styles.modifiers.currentPath,
291
295
  isLoadButton && styles.modifiers.load,
292
296
  isLoading && styles.modifiers.loading,
297
+ isFocused && styles.modifiers.focus,
293
298
  className
294
299
  )}
295
300
  onMouseOver={onMouseOver}
@@ -305,12 +310,13 @@ const MenuItemBase: React.FunctionComponent<MenuItemProps> = ({
305
310
  className={css(styles.menuItem, getIsSelected() && !hasCheck && styles.modifiers.selected, className)}
306
311
  aria-current={getAriaCurrent()}
307
312
  {...(!hasCheck && { disabled: isDisabled })}
308
- {...(!hasCheck && { role: 'menuitem' })}
313
+ {...(!hasCheck && !flyoutMenu && { role: 'menuitem' })}
309
314
  ref={innerRef}
310
315
  {...(!hasCheck && {
311
316
  onClick: (event: any) => {
312
317
  onItemSelect(event, onSelect);
313
318
  _drill && _drill();
319
+ flyoutMenu && handleFlyout(event);
314
320
  }
315
321
  })}
316
322
  {...(hasCheck && { htmlFor: randomId })}
@@ -4,8 +4,10 @@ import { css } from '@patternfly/react-styles';
4
4
  import CaretDownIcon from '@patternfly/react-icons/dist/esm/icons/caret-down-icon';
5
5
  import { BadgeProps } from '../Badge';
6
6
 
7
+ export type MenuToggleElement = HTMLDivElement | HTMLButtonElement;
8
+
7
9
  export interface MenuToggleProps
8
- extends Omit<React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, 'ref'> {
10
+ extends Omit<React.DetailedHTMLProps<React.HTMLAttributes<MenuToggleElement>, MenuToggleElement>, 'ref'> {
9
11
  /** Content rendered inside the toggle */
10
12
  children?: React.ReactNode;
11
13
  /** Additional classes added to the toggle */
@@ -19,18 +21,18 @@ export interface MenuToggleProps
19
21
  /** Flag indicating the toggle takes up the full width of its parent */
20
22
  isFullWidth?: boolean;
21
23
  /** Variant styles of the menu toggle */
22
- variant?: 'default' | 'plain' | 'primary' | 'plainText' | 'secondary';
24
+ variant?: 'default' | 'plain' | 'primary' | 'plainText' | 'secondary' | 'typeahead';
23
25
  /** Optional icon rendered inside the toggle, before the children content */
24
26
  icon?: React.ReactNode;
25
27
  /** Optional badge rendered inside the toggle, after the children content */
26
28
  badge?: BadgeProps | React.ReactNode;
27
29
  /** Forwarded ref */
28
- innerRef?: React.Ref<HTMLButtonElement>;
30
+ innerRef?: React.Ref<MenuToggleElement>;
29
31
  }
30
32
 
31
33
  export class MenuToggleBase extends React.Component<MenuToggleProps> {
32
34
  displayName = 'MenuToggleBase';
33
- static defaultProps = {
35
+ static defaultProps: MenuToggleProps = {
34
36
  className: '',
35
37
  isExpanded: false,
36
38
  isDisabled: false,
@@ -51,24 +53,63 @@ export class MenuToggleBase extends React.Component<MenuToggleProps> {
51
53
  isFullWidth,
52
54
  variant,
53
55
  innerRef,
54
- ...props
56
+ onClick,
57
+ 'aria-label': ariaLabel,
58
+ ...otherProps
55
59
  } = this.props;
56
-
57
60
  const isPlain = variant === 'plain';
58
61
  const isPlainText = variant === 'plainText';
62
+ const isTypeahead = variant === 'typeahead';
63
+ const toggleControls = (
64
+ <span className={css(styles.menuToggleControls)}>
65
+ <span className={css(styles.menuToggleToggleIcon)}>
66
+ <CaretDownIcon aria-hidden />
67
+ </span>
68
+ </span>
69
+ );
59
70
 
60
71
  const content = (
61
- <React.Fragment>
72
+ <>
62
73
  {icon && <span className={css(styles.menuToggleIcon)}>{icon}</span>}
63
- <span className="pf-c-menu-toggle__text">{children}</span>
64
- {badge && <span className={css(styles.menuToggleCount)}>{badge as React.ReactNode}</span>}
65
- <span className={css(styles.menuToggleControls)}>
66
- <span className={css(styles.menuToggleToggleIcon)}>
67
- <CaretDownIcon aria-hidden />
68
- </span>
69
- </span>
70
- </React.Fragment>
74
+ {isTypeahead ? children : <span className="pf-c-menu-toggle__text">{children}</span>}
75
+ {badge && <span className={css(styles.menuToggleCount)}>{badge}</span>}
76
+ {isTypeahead ? (
77
+ <button
78
+ type="button"
79
+ className={css(styles.menuToggleButton)}
80
+ aria-expanded={isExpanded}
81
+ onClick={onClick}
82
+ aria-label="Menu toggle"
83
+ >
84
+ {toggleControls}
85
+ </button>
86
+ ) : (
87
+ toggleControls
88
+ )}
89
+ </>
71
90
  );
91
+
92
+ const componentProps = {
93
+ className: css(
94
+ styles.menuToggle,
95
+ isExpanded && styles.modifiers.expanded,
96
+ variant === 'primary' && styles.modifiers.primary,
97
+ (isPlain || isPlainText) && styles.modifiers.plain,
98
+ isPlainText && styles.modifiers.text,
99
+ isFullHeight && styles.modifiers.fullHeight,
100
+ isTypeahead && styles.modifiers.typeahead,
101
+ isFullWidth && styles.modifiers.fullWidth,
102
+ className
103
+ ),
104
+ children: isPlain ? children : content,
105
+ ...(isDisabled && { disabled: true }),
106
+ ...otherProps
107
+ };
108
+
109
+ if (isTypeahead) {
110
+ return <div ref={innerRef as React.Ref<HTMLDivElement>} {...componentProps} />;
111
+ }
112
+
72
113
  return (
73
114
  <button
74
115
  className={css(
@@ -82,21 +123,18 @@ export class MenuToggleBase extends React.Component<MenuToggleProps> {
82
123
  isFullWidth && styles.modifiers.fullWidth,
83
124
  className
84
125
  )}
126
+ ref={innerRef as React.Ref<HTMLButtonElement>}
85
127
  type="button"
86
- aria-expanded={false}
87
- ref={innerRef}
88
- {...(isExpanded && { 'aria-expanded': true })}
89
- {...(isDisabled && { disabled: true })}
90
- {...props}
91
- >
92
- {isPlain && children}
93
- {!isPlain && content}
94
- </button>
128
+ aria-label={ariaLabel}
129
+ aria-expanded={isExpanded}
130
+ onClick={onClick}
131
+ {...componentProps}
132
+ />
95
133
  );
96
134
  }
97
135
  }
98
136
 
99
- export const MenuToggle = React.forwardRef((props: MenuToggleProps, ref: React.Ref<HTMLButtonElement>) => (
137
+ export const MenuToggle = React.forwardRef((props: MenuToggleProps, ref: React.Ref<MenuToggleElement>) => (
100
138
  <MenuToggleBase innerRef={ref} {...props} />
101
139
  ));
102
140
 
@@ -150,6 +150,11 @@ export const NavItem: React.FunctionComponent<NavItemProps> = ({
150
150
  </span>
151
151
  );
152
152
 
153
+ const ariaFlyoutProps = {
154
+ 'aria-haspopup': 'menu',
155
+ 'aria-expanded': flyoutVisible
156
+ };
157
+
153
158
  const renderDefaultLink = (context: any): React.ReactNode => {
154
159
  const preventLinkDefault = preventDefault || !to;
155
160
  return (
@@ -164,6 +169,7 @@ export const NavItem: React.FunctionComponent<NavItemProps> = ({
164
169
  )}
165
170
  aria-current={isActive ? 'page' : null}
166
171
  tabIndex={isNavOpen ? null : '-1'}
172
+ {...(hasFlyout && { ...ariaFlyoutProps })}
167
173
  {...props}
168
174
  >
169
175
  {children}
@@ -0,0 +1,141 @@
1
+ import React from 'react';
2
+ import styles from '@patternfly/react-styles/css/components/Tabs/tabs';
3
+ import { css } from '@patternfly/react-styles';
4
+ import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon';
5
+ import { Popper } from '../../helpers';
6
+ import { Menu, MenuContent, MenuList, MenuItem } from '../Menu';
7
+ import { TabsContext } from './TabsContext';
8
+ import { TabProps } from './Tab';
9
+ import { TabTitleText } from './TabTitleText';
10
+
11
+ export interface OverflowTabProps extends React.HTMLProps<HTMLLIElement> {
12
+ /** Additional classes added to the overflow tab */
13
+ className?: string;
14
+ /** The tabs that should be displayed in the menu */
15
+ overflowingTabs?: TabProps[];
16
+ /** Flag which shows the count of overflowing tabs when enabled */
17
+ showTabCount?: boolean;
18
+ /** The text which displays when an overflowing tab isn't selected */
19
+ defaultTitleText?: string;
20
+ /** The aria label applied to the button which toggles the tab overflow menu */
21
+ toggleAriaLabel?: string;
22
+ }
23
+
24
+ export const OverflowTab: React.FunctionComponent<OverflowTabProps> = ({
25
+ className,
26
+ overflowingTabs = [],
27
+ showTabCount,
28
+ defaultTitleText = 'More',
29
+ toggleAriaLabel,
30
+ ...props
31
+ }: OverflowTabProps) => {
32
+ const menuRef = React.useRef<HTMLDivElement>();
33
+ const overflowTabRef = React.useRef<HTMLButtonElement>();
34
+ const overflowLIRef = React.useRef<HTMLLIElement>();
35
+
36
+ const [isExpanded, setIsExpanded] = React.useState(false);
37
+
38
+ const { localActiveKey, handleTabClick } = React.useContext(TabsContext);
39
+
40
+ const closeMenu = () => {
41
+ setIsExpanded(false);
42
+ overflowTabRef.current.focus();
43
+ };
44
+
45
+ const handleMenuKeys = (ev: KeyboardEvent) => {
46
+ const menuContainsEventTarget = menuRef?.current?.contains(ev.target as Node);
47
+ if (isExpanded && menuContainsEventTarget && ev.key === 'Escape') {
48
+ closeMenu();
49
+ }
50
+ };
51
+
52
+ const handleClick = (ev: MouseEvent) => {
53
+ const clickIsOutsideMenu = !menuRef?.current?.contains(ev.target as Node);
54
+ const clickIsOutsideOverflowTab = !overflowTabRef?.current?.contains(ev.target as Node);
55
+ if (isExpanded && clickIsOutsideMenu && clickIsOutsideOverflowTab) {
56
+ closeMenu();
57
+ }
58
+ };
59
+
60
+ React.useEffect(() => {
61
+ window.addEventListener('click', handleClick);
62
+ window.addEventListener('keydown', handleMenuKeys);
63
+
64
+ return () => {
65
+ window.removeEventListener('click', handleClick);
66
+ window.removeEventListener('keydown', handleMenuKeys);
67
+ };
68
+ }, [isExpanded, menuRef, overflowTabRef]);
69
+
70
+ const selectedTab = overflowingTabs.find(tab => tab.eventKey === localActiveKey);
71
+ const tabTitle = selectedTab?.title ? selectedTab.title : defaultTitleText;
72
+
73
+ const toggleMenu = () => {
74
+ setIsExpanded(prevIsExpanded => !prevIsExpanded);
75
+ setTimeout(() => {
76
+ if (menuRef?.current) {
77
+ const firstElement = menuRef.current.querySelector('li > button,input:not(:disabled)');
78
+ firstElement && (firstElement as HTMLElement).focus();
79
+ }
80
+ }, 0);
81
+ };
82
+
83
+ const overflowTab = (
84
+ <li
85
+ className={css(styles.tabsItem, 'pf-m-overflow', selectedTab && styles.modifiers.current, className)}
86
+ role="presentation"
87
+ ref={overflowLIRef}
88
+ {...props}
89
+ >
90
+ <button
91
+ className={css(styles.tabsLink, isExpanded && styles.modifiers.expanded)}
92
+ onClick={() => toggleMenu()}
93
+ aria-label={toggleAriaLabel}
94
+ aria-haspopup="menu"
95
+ aria-expanded={isExpanded}
96
+ role="tab"
97
+ ref={overflowTabRef}
98
+ >
99
+ <TabTitleText>
100
+ {tabTitle}
101
+ {showTabCount && tabTitle === defaultTitleText && ` (${overflowingTabs.length})`}
102
+ </TabTitleText>
103
+ <span className={styles.tabsLinkToggleIcon}>
104
+ <AngleRightIcon />
105
+ </span>
106
+ </button>
107
+ </li>
108
+ );
109
+
110
+ const tabs = overflowingTabs.map(tab => (
111
+ <MenuItem key={tab.eventKey} itemId={tab.eventKey} isSelected={localActiveKey === tab.eventKey}>
112
+ {tab.title}
113
+ </MenuItem>
114
+ ));
115
+
116
+ const onTabSelect = (event: React.MouseEvent<HTMLElement, MouseEvent>, key: number | string) => {
117
+ closeMenu();
118
+ const selectedTabRef = overflowingTabs.find(tab => tab.eventKey === key).tabContentRef;
119
+ handleTabClick(event, key, selectedTabRef);
120
+ };
121
+
122
+ const overflowMenu = (
123
+ <Menu ref={menuRef} onSelect={(ev, itemId) => onTabSelect(ev as React.MouseEvent<HTMLElement, MouseEvent>, itemId)}>
124
+ <MenuContent>
125
+ <MenuList>{tabs}</MenuList>
126
+ </MenuContent>
127
+ </Menu>
128
+ );
129
+
130
+ return (
131
+ <Popper
132
+ trigger={overflowTab}
133
+ popper={overflowMenu}
134
+ isVisible={isExpanded}
135
+ popperMatchesTriggerWidth={false}
136
+ appendTo={overflowLIRef.current}
137
+ />
138
+ );
139
+ };
140
+
141
+ OverflowTab.displayName = 'OverflowTab';
@@ -10,6 +10,7 @@ import { getUniqueId, isElementInView, formatBreakpointMods } from '../../helper
10
10
  import { TabContent } from './TabContent';
11
11
  import { TabProps } from './Tab';
12
12
  import { TabsContextProvider } from './TabsContext';
13
+ import { OverflowTab } from './OverflowTab';
13
14
  import { Button } from '../Button';
14
15
  import { getOUIAProps, OUIAProps, getDefaultOUIAId, canUseDOM } from '../../helpers';
15
16
  import { GenerateId } from '../../helpers/GenerateId/GenerateId';
@@ -19,6 +20,15 @@ export enum TabsComponent {
19
20
  nav = 'nav'
20
21
  }
21
22
 
23
+ export interface HorizontalOverflowObject {
24
+ /** Flag which shows the count of overflowing tabs when enabled */
25
+ showTabCount?: boolean;
26
+ /** The text which displays when an overflowing tab isn't selected */
27
+ defaultTitleText?: string;
28
+ /** The aria label applied to the button which toggles the tab overflow menu */
29
+ toggleAriaLabel?: string;
30
+ }
31
+
22
32
  export interface TabsProps extends Omit<React.HTMLProps<HTMLElement | HTMLDivElement>, 'onSelect'>, OUIAProps {
23
33
  /** Content rendered inside the tabs component. Must be React.ReactElement<TabProps>[] */
24
34
  children: React.ReactNode;
@@ -94,6 +104,8 @@ export interface TabsProps extends Omit<React.HTMLProps<HTMLElement | HTMLDivEle
94
104
  toggleAriaLabel?: string;
95
105
  /** Callback function to toggle the expandable tabs. */
96
106
  onToggle?: (isExpanded: boolean) => void;
107
+ /** @beta Flag which places overflowing tabs into a menu triggered by the last tab. Additionally an object can be passed with custom settings for the overflow tab. */
108
+ isOverflowHorizontal?: boolean | HorizontalOverflowObject;
97
109
  }
98
110
 
99
111
  const variantStyle = {
@@ -109,6 +121,7 @@ interface TabsState {
109
121
  uncontrolledActiveKey: number | string;
110
122
  uncontrolledIsExpandedLocal: boolean;
111
123
  ouiaStateId: string;
124
+ overflowingTabCount: number;
112
125
  }
113
126
 
114
127
  export class Tabs extends React.Component<TabsProps, TabsState> {
@@ -123,7 +136,8 @@ export class Tabs extends React.Component<TabsProps, TabsState> {
123
136
  shownKeys: this.props.defaultActiveKey !== undefined ? [this.props.defaultActiveKey] : [this.props.activeKey], // only for mountOnEnter case
124
137
  uncontrolledActiveKey: this.props.defaultActiveKey,
125
138
  uncontrolledIsExpandedLocal: this.props.defaultIsExpanded,
126
- ouiaStateId: getDefaultOUIAId(Tabs.displayName)
139
+ ouiaStateId: getDefaultOUIAId(Tabs.displayName),
140
+ overflowingTabCount: 0
127
141
  };
128
142
 
129
143
  if (this.props.isVertical && this.props.expandable !== undefined) {
@@ -192,7 +206,13 @@ export class Tabs extends React.Component<TabsProps, TabsState> {
192
206
  }
193
207
  }
194
208
 
209
+ countOverflowingElements = (container: HTMLUListElement) => {
210
+ const elements = Array.from(container.children);
211
+ return elements.filter(element => !isElementInView(container, element as HTMLElement, false)).length;
212
+ };
213
+
195
214
  handleScrollButtons = () => {
215
+ const { isOverflowHorizontal: isOverflowHorizontal } = this.props;
196
216
  // add debounce to the scroll event
197
217
  clearTimeout(this.scrollTimeout);
198
218
  this.scrollTimeout = setTimeout(() => {
@@ -200,8 +220,9 @@ export class Tabs extends React.Component<TabsProps, TabsState> {
200
220
  let disableLeftScrollButton = true;
201
221
  let disableRightScrollButton = true;
202
222
  let showScrollButtons = false;
223
+ let overflowingTabCount = 0;
203
224
 
204
- if (container && !this.props.isVertical) {
225
+ if (container && !this.props.isVertical && !isOverflowHorizontal) {
205
226
  // get first element and check if it is in view
206
227
  const overflowOnLeft = !isElementInView(container, container.firstChild as HTMLElement, false);
207
228
 
@@ -213,10 +234,16 @@ export class Tabs extends React.Component<TabsProps, TabsState> {
213
234
  disableLeftScrollButton = !overflowOnLeft;
214
235
  disableRightScrollButton = !overflowOnRight;
215
236
  }
237
+
238
+ if (isOverflowHorizontal) {
239
+ overflowingTabCount = this.countOverflowingElements(container);
240
+ }
241
+
216
242
  this.setState({
217
243
  showScrollButtons,
218
244
  disableLeftScrollButton,
219
- disableRightScrollButton
245
+ disableRightScrollButton,
246
+ overflowingTabCount
220
247
  });
221
248
  }, 100);
222
249
  };
@@ -280,8 +307,8 @@ export class Tabs extends React.Component<TabsProps, TabsState> {
280
307
  }
281
308
 
282
309
  componentDidUpdate(prevProps: TabsProps) {
283
- const { activeKey, mountOnEnter, children } = this.props;
284
- const { shownKeys } = this.state;
310
+ const { activeKey, mountOnEnter, isOverflowHorizontal, children } = this.props;
311
+ const { shownKeys, overflowingTabCount } = this.state;
285
312
  if (prevProps.activeKey !== activeKey && mountOnEnter && shownKeys.indexOf(activeKey) < 0) {
286
313
  this.setState({
287
314
  shownKeys: shownKeys.concat(activeKey)
@@ -295,6 +322,11 @@ export class Tabs extends React.Component<TabsProps, TabsState> {
295
322
  ) {
296
323
  this.handleScrollButtons();
297
324
  }
325
+
326
+ const currentOverflowingTabCount = this.countOverflowingElements(this.tabList.current);
327
+ if (isOverflowHorizontal && currentOverflowingTabCount) {
328
+ this.setState({ overflowingTabCount: currentOverflowingTabCount + overflowingTabCount });
329
+ }
298
330
  }
299
331
 
300
332
  render() {
@@ -330,6 +362,7 @@ export class Tabs extends React.Component<TabsProps, TabsState> {
330
362
  onToggle,
331
363
  onClose,
332
364
  onAdd,
365
+ isOverflowHorizontal: isOverflowHorizontal,
333
366
  ...props
334
367
  } = this.props;
335
368
  const {
@@ -338,12 +371,17 @@ export class Tabs extends React.Component<TabsProps, TabsState> {
338
371
  disableRightScrollButton,
339
372
  shownKeys,
340
373
  uncontrolledActiveKey,
341
- uncontrolledIsExpandedLocal
374
+ uncontrolledIsExpandedLocal,
375
+ overflowingTabCount
342
376
  } = this.state;
343
377
  const filteredChildren = (React.Children.toArray(children) as React.ReactElement<TabProps>[])
344
378
  .filter(Boolean)
345
379
  .filter(child => !child.props.isHidden);
346
380
 
381
+ const filteredChildrenWithoutOverflow = filteredChildren.slice(0, filteredChildren.length - overflowingTabCount);
382
+ const filteredChildrenOverflowing = filteredChildren.slice(filteredChildren.length - overflowingTabCount);
383
+ const overflowingTabProps = filteredChildrenOverflowing.map((child: React.ReactElement<TabProps>) => child.props);
384
+
347
385
  const uniqueId = id || getUniqueId();
348
386
  const Component: any = component === TabsComponent.nav ? 'nav' : 'div';
349
387
  const localActiveKey = defaultActiveKey !== undefined ? uncontrolledActiveKey : activeKey;
@@ -358,6 +396,9 @@ export class Tabs extends React.Component<TabsProps, TabsState> {
358
396
  }
359
397
  };
360
398
 
399
+ const hasOverflowTab = isOverflowHorizontal && overflowingTabCount > 0;
400
+ const overflowObjectProps = typeof isOverflowHorizontal === 'object' ? { ...isOverflowHorizontal } : {};
401
+
361
402
  return (
362
403
  <TabsContextProvider
363
404
  value={{
@@ -386,6 +427,7 @@ export class Tabs extends React.Component<TabsProps, TabsState> {
386
427
  hasSecondaryBorderBottom && styles.modifiers.borderBottom,
387
428
  formatBreakpointMods(inset, styles),
388
429
  variantStyle[variant],
430
+ hasOverflowTab && styles.modifiers.overflow,
389
431
  className
390
432
  )}
391
433
  {...getOUIAProps(Tabs.displayName, ouiaId !== undefined ? ouiaId : this.state.ouiaStateId, ouiaSafe)}
@@ -419,27 +461,32 @@ export class Tabs extends React.Component<TabsProps, TabsState> {
419
461
  )}
420
462
  </GenerateId>
421
463
  )}
422
- <button
423
- className={css(styles.tabsScrollButton, isSecondary && buttonStyles.modifiers.secondary)}
424
- aria-label={leftScrollAriaLabel}
425
- onClick={this.scrollLeft}
426
- disabled={disableLeftScrollButton}
427
- aria-hidden={disableLeftScrollButton}
428
- >
429
- <AngleLeftIcon />
430
- </button>
464
+ {!isOverflowHorizontal && (
465
+ <button
466
+ className={css(styles.tabsScrollButton, isSecondary && buttonStyles.modifiers.secondary)}
467
+ aria-label={leftScrollAriaLabel}
468
+ onClick={this.scrollLeft}
469
+ disabled={disableLeftScrollButton}
470
+ aria-hidden={disableLeftScrollButton}
471
+ >
472
+ <AngleLeftIcon />
473
+ </button>
474
+ )}
431
475
  <ul className={css(styles.tabsList)} ref={this.tabList} onScroll={this.handleScrollButtons} role="tablist">
432
- {filteredChildren}
476
+ {isOverflowHorizontal ? filteredChildrenWithoutOverflow : filteredChildren}
477
+ {hasOverflowTab && <OverflowTab overflowingTabs={overflowingTabProps} {...overflowObjectProps} />}
433
478
  </ul>
434
- <button
435
- className={css(styles.tabsScrollButton, isSecondary && buttonStyles.modifiers.secondary)}
436
- aria-label={rightScrollAriaLabel}
437
- onClick={this.scrollRight}
438
- disabled={disableRightScrollButton}
439
- aria-hidden={disableRightScrollButton}
440
- >
441
- <AngleRightIcon />
442
- </button>
479
+ {!isOverflowHorizontal && (
480
+ <button
481
+ className={css(styles.tabsScrollButton, isSecondary && buttonStyles.modifiers.secondary)}
482
+ aria-label={rightScrollAriaLabel}
483
+ onClick={this.scrollRight}
484
+ disabled={disableRightScrollButton}
485
+ aria-hidden={disableRightScrollButton}
486
+ >
487
+ <AngleRightIcon />
488
+ </button>
489
+ )}
443
490
  {onAdd !== undefined && (
444
491
  <span className={css(styles.tabsAdd)}>
445
492
  <Button variant="plain" aria-label={addButtonAriaLabel || 'Add tab'} onClick={onAdd}>
@@ -2,7 +2,7 @@
2
2
  id: Tabs
3
3
  section: components
4
4
  cssPrefix: pf-c-tabs
5
- propComponents: ['Tabs', 'Tab', 'TabContent', 'TabTitleText', 'TabTitleIcon']
5
+ propComponents: ['Tabs', 'Tab', 'TabContent', 'TabTitleText', 'TabTitleIcon', 'horizontalOverflowObject']
6
6
  ouia: true
7
7
  ---
8
8
 
@@ -410,6 +410,11 @@ class ScrollButtonsPrimaryTabs extends React.Component {
410
410
  }
411
411
  ```
412
412
 
413
+ ### Horizontal overflow
414
+
415
+ ```ts file="./TabsHorizontalOverflow.tsx" isBeta
416
+ ```
417
+
413
418
  ### Vertical
414
419
 
415
420
  ```js