@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.
- package/CHANGELOG.md +30 -0
- package/dist/esm/components/Menu/MenuItem.d.ts +3 -1
- package/dist/esm/components/Menu/MenuItem.d.ts.map +1 -1
- package/dist/esm/components/Menu/MenuItem.js +7 -5
- package/dist/esm/components/Menu/MenuItem.js.map +1 -1
- package/dist/esm/components/MenuToggle/MenuToggle.d.ts +6 -12
- package/dist/esm/components/MenuToggle/MenuToggle.d.ts.map +1 -1
- package/dist/esm/components/MenuToggle/MenuToggle.js +12 -8
- package/dist/esm/components/MenuToggle/MenuToggle.js.map +1 -1
- package/dist/esm/components/Nav/NavItem.d.ts.map +1 -1
- package/dist/esm/components/Nav/NavItem.js +5 -1
- package/dist/esm/components/Nav/NavItem.js.map +1 -1
- package/dist/esm/components/Tabs/OverflowTab.d.ts +16 -0
- package/dist/esm/components/Tabs/OverflowTab.d.ts.map +1 -0
- package/dist/esm/components/Tabs/OverflowTab.js +74 -0
- package/dist/esm/components/Tabs/OverflowTab.js.map +1 -0
- package/dist/esm/components/Tabs/Tabs.d.ts +12 -0
- package/dist/esm/components/Tabs/Tabs.d.ts.map +1 -1
- package/dist/esm/components/Tabs/Tabs.js +36 -13
- package/dist/esm/components/Tabs/Tabs.js.map +1 -1
- package/dist/esm/components/TextInputGroup/TextInputGroup.d.ts +2 -0
- package/dist/esm/components/TextInputGroup/TextInputGroup.d.ts.map +1 -1
- package/dist/esm/components/TextInputGroup/TextInputGroup.js +2 -2
- package/dist/esm/components/TextInputGroup/TextInputGroup.js.map +1 -1
- package/dist/esm/components/TextInputGroup/TextInputGroupMain.d.ts +1 -1
- package/dist/esm/components/TextInputGroup/TextInputGroupMain.d.ts.map +1 -1
- package/dist/esm/components/TreeView/TreeView.d.ts +2 -0
- package/dist/esm/components/TreeView/TreeView.d.ts.map +1 -1
- package/dist/esm/components/TreeView/TreeView.js +3 -3
- package/dist/esm/components/TreeView/TreeView.js.map +1 -1
- package/dist/esm/components/TreeView/TreeViewListItem.d.ts +2 -0
- package/dist/esm/components/TreeView/TreeViewListItem.d.ts.map +1 -1
- package/dist/esm/components/TreeView/TreeViewListItem.js +23 -14
- package/dist/esm/components/TreeView/TreeViewListItem.js.map +1 -1
- package/dist/js/components/Menu/MenuItem.d.ts +3 -1
- package/dist/js/components/Menu/MenuItem.d.ts.map +1 -1
- package/dist/js/components/Menu/MenuItem.js +7 -5
- package/dist/js/components/Menu/MenuItem.js.map +1 -1
- package/dist/js/components/MenuToggle/MenuToggle.d.ts +6 -12
- package/dist/js/components/MenuToggle/MenuToggle.d.ts.map +1 -1
- package/dist/js/components/MenuToggle/MenuToggle.js +12 -8
- package/dist/js/components/MenuToggle/MenuToggle.js.map +1 -1
- package/dist/js/components/Nav/NavItem.d.ts.map +1 -1
- package/dist/js/components/Nav/NavItem.js +5 -1
- package/dist/js/components/Nav/NavItem.js.map +1 -1
- package/dist/js/components/Tabs/OverflowTab.d.ts +16 -0
- package/dist/js/components/Tabs/OverflowTab.d.ts.map +1 -0
- package/dist/js/components/Tabs/OverflowTab.js +78 -0
- package/dist/js/components/Tabs/OverflowTab.js.map +1 -0
- package/dist/js/components/Tabs/Tabs.d.ts +12 -0
- package/dist/js/components/Tabs/Tabs.d.ts.map +1 -1
- package/dist/js/components/Tabs/Tabs.js +36 -13
- package/dist/js/components/Tabs/Tabs.js.map +1 -1
- package/dist/js/components/TextInputGroup/TextInputGroup.d.ts +2 -0
- package/dist/js/components/TextInputGroup/TextInputGroup.d.ts.map +1 -1
- package/dist/js/components/TextInputGroup/TextInputGroup.js +2 -2
- package/dist/js/components/TextInputGroup/TextInputGroup.js.map +1 -1
- package/dist/js/components/TextInputGroup/TextInputGroupMain.d.ts +1 -1
- package/dist/js/components/TextInputGroup/TextInputGroupMain.d.ts.map +1 -1
- package/dist/js/components/TreeView/TreeView.d.ts +2 -0
- package/dist/js/components/TreeView/TreeView.d.ts.map +1 -1
- package/dist/js/components/TreeView/TreeView.js +3 -3
- package/dist/js/components/TreeView/TreeView.js.map +1 -1
- package/dist/js/components/TreeView/TreeViewListItem.d.ts +2 -0
- package/dist/js/components/TreeView/TreeViewListItem.d.ts.map +1 -1
- package/dist/js/components/TreeView/TreeViewListItem.js +23 -14
- package/dist/js/components/TreeView/TreeViewListItem.js.map +1 -1
- package/dist/styles/base-no-reset.css +9 -0
- package/dist/styles/base.css +9 -0
- package/dist/umd/react-core.css +767 -777
- package/dist/umd/react-core.js +159 -52
- package/dist/umd/react-core.min.css +507 -517
- package/dist/umd/react-core.min.js +5 -5
- package/package.json +6 -6
- package/src/components/Menu/MenuItem.tsx +11 -5
- package/src/components/MenuToggle/MenuToggle.tsx +63 -25
- package/src/components/Nav/NavItem.tsx +6 -0
- package/src/components/Tabs/OverflowTab.tsx +141 -0
- package/src/components/Tabs/Tabs.tsx +72 -25
- package/src/components/Tabs/examples/Tabs.md +6 -1
- package/src/components/Tabs/examples/TabsHorizontalOverflow.tsx +67 -0
- package/src/components/TextInputGroup/TextInputGroup.tsx +9 -1
- package/src/components/TextInputGroup/TextInputGroupMain.tsx +1 -1
- package/src/components/TreeView/TreeView.tsx +5 -0
- package/src/components/TreeView/TreeViewListItem.tsx +31 -17
- package/src/components/TreeView/__tests__/__snapshots__/TreeView.test.tsx.snap +299 -299
- package/src/components/TreeView/examples/TreeView.md +125 -7
- package/src/demos/ComposableMenu/ComposableMenu.md +8 -0
- 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.
|
|
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.
|
|
38
|
-
"@patternfly/react-styles": "^4.
|
|
39
|
-
"@patternfly/react-tokens": "^4.
|
|
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.
|
|
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": "
|
|
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'] =
|
|
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.
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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-
|
|
87
|
-
|
|
88
|
-
{
|
|
89
|
-
{...
|
|
90
|
-
|
|
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<
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|