@patternfly/react-core 4.227.7 → 4.228.0

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 (99) hide show
  1. package/CHANGELOG.md +27 -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 +2 -2
  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/Tabs/OverflowTab.d.ts +16 -0
  11. package/dist/esm/components/Tabs/OverflowTab.d.ts.map +1 -0
  12. package/dist/esm/components/Tabs/OverflowTab.js +74 -0
  13. package/dist/esm/components/Tabs/OverflowTab.js.map +1 -0
  14. package/dist/esm/components/Tabs/Tabs.d.ts +12 -0
  15. package/dist/esm/components/Tabs/Tabs.d.ts.map +1 -1
  16. package/dist/esm/components/Tabs/Tabs.js +36 -13
  17. package/dist/esm/components/Tabs/Tabs.js.map +1 -1
  18. package/dist/esm/components/TextInputGroup/TextInputGroup.d.ts +2 -0
  19. package/dist/esm/components/TextInputGroup/TextInputGroup.d.ts.map +1 -1
  20. package/dist/esm/components/TextInputGroup/TextInputGroup.js +2 -2
  21. package/dist/esm/components/TextInputGroup/TextInputGroup.js.map +1 -1
  22. package/dist/esm/components/TextInputGroup/TextInputGroupMain.d.ts +1 -1
  23. package/dist/esm/components/TextInputGroup/TextInputGroupMain.d.ts.map +1 -1
  24. package/dist/esm/components/TreeView/TreeView.d.ts +2 -0
  25. package/dist/esm/components/TreeView/TreeView.d.ts.map +1 -1
  26. package/dist/esm/components/TreeView/TreeView.js +3 -3
  27. package/dist/esm/components/TreeView/TreeView.js.map +1 -1
  28. package/dist/esm/components/TreeView/TreeViewListItem.d.ts +2 -0
  29. package/dist/esm/components/TreeView/TreeViewListItem.d.ts.map +1 -1
  30. package/dist/esm/components/TreeView/TreeViewListItem.js +23 -14
  31. package/dist/esm/components/TreeView/TreeViewListItem.js.map +1 -1
  32. package/dist/js/components/Menu/MenuItem.d.ts +3 -1
  33. package/dist/js/components/Menu/MenuItem.d.ts.map +1 -1
  34. package/dist/js/components/Menu/MenuItem.js +2 -2
  35. package/dist/js/components/Menu/MenuItem.js.map +1 -1
  36. package/dist/js/components/MenuToggle/MenuToggle.d.ts +6 -12
  37. package/dist/js/components/MenuToggle/MenuToggle.d.ts.map +1 -1
  38. package/dist/js/components/MenuToggle/MenuToggle.js +12 -8
  39. package/dist/js/components/MenuToggle/MenuToggle.js.map +1 -1
  40. package/dist/js/components/Tabs/OverflowTab.d.ts +16 -0
  41. package/dist/js/components/Tabs/OverflowTab.d.ts.map +1 -0
  42. package/dist/js/components/Tabs/OverflowTab.js +78 -0
  43. package/dist/js/components/Tabs/OverflowTab.js.map +1 -0
  44. package/dist/js/components/Tabs/Tabs.d.ts +12 -0
  45. package/dist/js/components/Tabs/Tabs.d.ts.map +1 -1
  46. package/dist/js/components/Tabs/Tabs.js +36 -13
  47. package/dist/js/components/Tabs/Tabs.js.map +1 -1
  48. package/dist/js/components/TextInputGroup/TextInputGroup.d.ts +2 -0
  49. package/dist/js/components/TextInputGroup/TextInputGroup.d.ts.map +1 -1
  50. package/dist/js/components/TextInputGroup/TextInputGroup.js +2 -2
  51. package/dist/js/components/TextInputGroup/TextInputGroup.js.map +1 -1
  52. package/dist/js/components/TextInputGroup/TextInputGroupMain.d.ts +1 -1
  53. package/dist/js/components/TextInputGroup/TextInputGroupMain.d.ts.map +1 -1
  54. package/dist/js/components/TreeView/TreeView.d.ts +2 -0
  55. package/dist/js/components/TreeView/TreeView.d.ts.map +1 -1
  56. package/dist/js/components/TreeView/TreeView.js +3 -3
  57. package/dist/js/components/TreeView/TreeView.js.map +1 -1
  58. package/dist/js/components/TreeView/TreeViewListItem.d.ts +2 -0
  59. package/dist/js/components/TreeView/TreeViewListItem.d.ts.map +1 -1
  60. package/dist/js/components/TreeView/TreeViewListItem.js +23 -14
  61. package/dist/js/components/TreeView/TreeViewListItem.js.map +1 -1
  62. package/dist/styles/base-no-reset.css +9 -0
  63. package/dist/styles/base.css +9 -0
  64. package/dist/umd/react-core.css +21 -31
  65. package/dist/umd/react-core.js +149 -48
  66. package/dist/umd/react-core.min.css +21 -31
  67. package/dist/umd/react-core.min.js +5 -5
  68. package/package.json +6 -6
  69. package/src/components/Menu/MenuItem.tsx +4 -0
  70. package/src/components/MenuToggle/MenuToggle.tsx +63 -25
  71. package/src/components/Page/examples/Page.md +10 -615
  72. package/src/components/Page/examples/PageCenteredSection.tsx +76 -0
  73. package/src/components/Page/examples/PageGroupSection.tsx +106 -0
  74. package/src/components/Page/examples/PageHorizontalNav.tsx +43 -0
  75. package/src/components/Page/examples/PageMainSectionPadding.tsx +72 -0
  76. package/src/components/Page/examples/PageTertiaryNav.tsx +42 -0
  77. package/src/components/Page/examples/PageUncontrolledNav.tsx +53 -0
  78. package/src/components/Page/examples/PageVerticalNav.tsx +65 -0
  79. package/src/components/Page/examples/PageVerticalNavUsingPageHeaderComponent.tsx +44 -0
  80. package/src/components/Page/examples/PageWithOrWithoutFill.tsx +67 -0
  81. package/src/components/Switch/examples/Switch.md +6 -153
  82. package/src/components/Switch/examples/SwitchBasic.tsx +20 -0
  83. package/src/components/Switch/examples/SwitchCheckedWithLabel.tsx +22 -0
  84. package/src/components/Switch/examples/SwitchDisabled.tsx +28 -0
  85. package/src/components/Switch/examples/SwitchReversed.tsx +21 -0
  86. package/src/components/Switch/examples/SwitchUncontrolled.tsx +14 -0
  87. package/src/components/Switch/examples/SwitchWithoutLabel.tsx +12 -0
  88. package/src/components/Tabs/OverflowTab.tsx +141 -0
  89. package/src/components/Tabs/Tabs.tsx +72 -25
  90. package/src/components/Tabs/examples/Tabs.md +6 -1
  91. package/src/components/Tabs/examples/TabsHorizontalOverflow.tsx +67 -0
  92. package/src/components/TextInputGroup/TextInputGroup.tsx +9 -1
  93. package/src/components/TextInputGroup/TextInputGroupMain.tsx +1 -1
  94. package/src/components/TreeView/TreeView.tsx +5 -0
  95. package/src/components/TreeView/TreeViewListItem.tsx +31 -17
  96. package/src/components/TreeView/__tests__/__snapshots__/TreeView.test.tsx.snap +299 -299
  97. package/src/components/TreeView/examples/TreeView.md +125 -7
  98. package/src/demos/ComposableMenu/ComposableMenu.md +8 -0
  99. package/src/demos/ComposableMenu/examples/ComposableTypeaheadSelect.tsx +196 -0
@@ -10,177 +10,30 @@ ouia: true
10
10
 
11
11
  ### Basic
12
12
 
13
- ```js
14
- import React from 'react';
15
- import { Switch } from '@patternfly/react-core';
16
-
17
- class SimpleSwitch extends React.Component {
18
- constructor(props) {
19
- super(props);
20
- this.state = {
21
- isChecked: true
22
- };
23
- this.handleChange = isChecked => {
24
- this.setState({ isChecked });
25
- };
26
- }
27
-
28
- render() {
29
- const { isChecked } = this.state;
30
- return (
31
- <Switch
32
- id="simple-switch"
33
- label="Message when on"
34
- labelOff="Message when off"
35
- isChecked={isChecked}
36
- onChange={this.handleChange}
37
- />
38
- );
39
- }
40
- }
13
+ ```ts file="./SwitchBasic.tsx"
41
14
  ```
42
15
 
43
16
  ### Reversed Layout
44
17
 
45
- ```js
46
- import React from 'react';
47
- import { Switch } from '@patternfly/react-core';
48
-
49
- class ReversedSwitch extends React.Component {
50
- constructor(props) {
51
- super(props);
52
- this.state = {
53
- isChecked: true
54
- };
55
- this.handleChange = isChecked => {
56
- this.setState({ isChecked });
57
- };
58
- }
59
-
60
- render() {
61
- const { isChecked } = this.state;
62
- return (
63
- <Switch
64
- id="reversed-switch"
65
- label="Message when on"
66
- labelOff="Message when off"
67
- isChecked={isChecked}
68
- onChange={this.handleChange}
69
- isReversed
70
- />
71
- );
72
- }
73
- }
18
+ ```ts file="./SwitchReversed.tsx"
74
19
  ```
75
20
 
76
21
  ### Without label
77
22
 
78
- ```js
79
- import React from 'react';
80
- import { Switch } from '@patternfly/react-core';
81
-
82
- class NoLabelSwitch extends React.Component {
83
- constructor(props) {
84
- super(props);
85
- this.state = {
86
- isChecked: true
87
- };
88
- this.handleChange = isChecked => {
89
- this.setState({ isChecked });
90
- };
91
- }
92
-
93
- render() {
94
- const { isChecked } = this.state;
95
- return (
96
- <Switch id="no-label-switch-on" aria-label="Message when on" isChecked={isChecked} onChange={this.handleChange} />
97
- );
98
- }
99
- }
23
+ ```ts file="./SwitchWithoutLabel.tsx"
100
24
  ```
101
25
 
102
26
  ### Checked with label
103
27
 
104
- ```js
105
- import React from 'react';
106
- import { Switch } from '@patternfly/react-core';
107
-
108
- class CheckedWithLabelSwitch extends React.Component {
109
- constructor(props) {
110
- super(props);
111
- this.state = {
112
- isChecked: true
113
- };
114
- this.handleChange = isChecked => {
115
- this.setState({ isChecked });
116
- };
117
- }
118
-
119
- render() {
120
- const { isChecked } = this.state;
121
- return (
122
- <Switch
123
- label="Message when on"
124
- labelOff="Message when off"
125
- id="checked-with-label-switch-on"
126
- aria-label="Message when on"
127
- isChecked={isChecked}
128
- hasCheckIcon
129
- onChange={this.handleChange}
130
- />
131
- );
132
- }
133
- }
28
+ ```ts file="./SwitchCheckedWithLabel.tsx"
134
29
  ```
135
30
 
136
31
  ### Disabled
137
32
 
138
- ```js
139
- import React from 'react';
140
- import { Switch } from '@patternfly/react-core';
141
-
142
- <React.Fragment>
143
- <Switch
144
- id="disabled-switch-on"
145
- aria-label="Message when on"
146
- label="Message when on"
147
- labelOff="Message when off"
148
- isChecked
149
- isDisabled
150
- />
151
- <br />
152
- <Switch
153
- id="disabled-switch-off"
154
- aria-label="Message when on"
155
- label="Message when on"
156
- labelOff="Message when off"
157
- isChecked={false}
158
- isDisabled
159
- />
160
- <br />
161
- <Switch id="disabled-no-label-switch-on" aria-label="Message when on" isChecked isDisabled />
162
- <br />
163
- <Switch id="disabled-no-label-switch-off" aria-label="Message when on" isChecked={false} isDisabled />
164
- </React.Fragment>;
33
+ ```ts file="./SwitchDisabled.tsx"
165
34
  ```
166
35
 
167
36
  ### Uncontrolled
168
37
 
169
- ```js
170
- import React from 'react';
171
- import { Switch } from '@patternfly/react-core';
172
-
173
- <React.Fragment>
174
- <Switch
175
- id="uncontrolled-switch-no-label"
176
- aria-label="An uncontrolled switch with no label"
177
- defaultChecked={false}
178
- />
179
- <br />
180
- <Switch
181
- id="uncontrolled-switch-with-label"
182
- label="An uncontrolled switch"
183
- defaultChecked={false}
184
- />
185
- </React.Fragment>;
38
+ ```ts file="./SwitchUncontrolled.tsx"
186
39
  ```
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { Switch } from '@patternfly/react-core';
3
+
4
+ export const SwitchBasic: React.FunctionComponent = () => {
5
+ const [isChecked, setIsChecked] = React.useState<boolean>(true);
6
+
7
+ const handleChange = (checked: boolean, _event: React.FormEvent<HTMLInputElement>) => {
8
+ setIsChecked(checked);
9
+ };
10
+
11
+ return (
12
+ <Switch
13
+ id="simple-switch"
14
+ label="Message when on"
15
+ labelOff="Message when off"
16
+ isChecked={isChecked}
17
+ onChange={handleChange}
18
+ />
19
+ );
20
+ };
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { Switch } from '@patternfly/react-core';
3
+
4
+ export const SwitchCheckedWithLabel: React.FunctionComponent = () => {
5
+ const [isChecked, setIsChecked] = React.useState<boolean>(true);
6
+
7
+ const handleChange = (checked: boolean, _event: React.FormEvent<HTMLInputElement>) => {
8
+ setIsChecked(checked);
9
+ };
10
+
11
+ return (
12
+ <Switch
13
+ label="Message when on"
14
+ labelOff="Message when off"
15
+ id="checked-with-label-switch-on"
16
+ aria-label="Message when on"
17
+ isChecked={isChecked}
18
+ hasCheckIcon
19
+ onChange={handleChange}
20
+ />
21
+ );
22
+ };
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { Switch } from '@patternfly/react-core';
3
+
4
+ export const SwitchDisabled: React.FunctionComponent = () => (
5
+ <React.Fragment>
6
+ <Switch
7
+ id="disabled-switch-on"
8
+ aria-label="Message when on"
9
+ label="Message when on"
10
+ labelOff="Message when off"
11
+ isChecked
12
+ isDisabled
13
+ />
14
+ <br />
15
+ <Switch
16
+ id="disabled-switch-off"
17
+ aria-label="Message when on"
18
+ label="Message when on"
19
+ labelOff="Message when off"
20
+ isChecked={false}
21
+ isDisabled
22
+ />
23
+ <br />
24
+ <Switch id="disabled-no-label-switch-on" aria-label="Message when on" isChecked isDisabled />
25
+ <br />
26
+ <Switch id="disabled-no-label-switch-off" aria-label="Message when on" isChecked={false} isDisabled />
27
+ </React.Fragment>
28
+ );
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { Switch } from '@patternfly/react-core';
3
+
4
+ export const SwitchReversed: React.FunctionComponent = () => {
5
+ const [isChecked, setIsChecked] = React.useState<boolean>(true);
6
+
7
+ const handleChange = (checked: boolean, _event: React.FormEvent<HTMLInputElement>) => {
8
+ setIsChecked(checked);
9
+ };
10
+
11
+ return (
12
+ <Switch
13
+ id="reversed-switch"
14
+ label="Message when on"
15
+ labelOff="Message when off"
16
+ isChecked={isChecked}
17
+ onChange={handleChange}
18
+ isReversed
19
+ />
20
+ );
21
+ };
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { Switch } from '@patternfly/react-core';
3
+
4
+ export const SwitchUncontrolled: React.FunctionComponent = () => (
5
+ <React.Fragment>
6
+ <Switch
7
+ id="uncontrolled-switch-no-label"
8
+ aria-label="An uncontrolled switch with no label"
9
+ defaultChecked={false}
10
+ />
11
+ <br />
12
+ <Switch id="uncontrolled-switch-with-label" label="An uncontrolled switch" defaultChecked={false} />
13
+ </React.Fragment>
14
+ );
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import { Switch } from '@patternfly/react-core';
3
+
4
+ export const SwitchWithoutLabel: React.FunctionComponent = () => {
5
+ const [isChecked, setIsChecked] = React.useState<boolean>(true);
6
+
7
+ const handleChange = (checked: boolean, _event: React.FormEvent<HTMLInputElement>) => {
8
+ setIsChecked(checked);
9
+ };
10
+
11
+ return <Switch id="no-label-switch-on" aria-label="Message when on" isChecked={isChecked} onChange={handleChange} />;
12
+ };
@@ -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