@patternfly/react-core 4.212.0 → 4.213.2

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 (81) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/esm/components/ApplicationLauncher/ApplicationLauncher.d.ts +2 -9
  3. package/dist/esm/components/ApplicationLauncher/ApplicationLauncher.d.ts.map +1 -1
  4. package/dist/esm/components/ApplicationLauncher/ApplicationLauncher.js.map +1 -1
  5. package/dist/esm/components/ContextSelector/ContextSelector.d.ts +2 -9
  6. package/dist/esm/components/ContextSelector/ContextSelector.d.ts.map +1 -1
  7. package/dist/esm/components/ContextSelector/ContextSelector.js.map +1 -1
  8. package/dist/esm/components/Dropdown/Dropdown.d.ts +2 -9
  9. package/dist/esm/components/Dropdown/Dropdown.d.ts.map +1 -1
  10. package/dist/esm/components/Dropdown/Dropdown.js +2 -2
  11. package/dist/esm/components/Dropdown/Dropdown.js.map +1 -1
  12. package/dist/esm/components/OptionsMenu/OptionsMenu.d.ts +2 -9
  13. package/dist/esm/components/OptionsMenu/OptionsMenu.d.ts.map +1 -1
  14. package/dist/esm/components/OptionsMenu/OptionsMenu.js.map +1 -1
  15. package/dist/esm/components/Select/Select.d.ts +2 -9
  16. package/dist/esm/components/Select/Select.d.ts.map +1 -1
  17. package/dist/esm/components/Select/Select.js.map +1 -1
  18. package/dist/esm/components/Tabs/Tab.d.ts +5 -1
  19. package/dist/esm/components/Tabs/Tab.d.ts.map +1 -1
  20. package/dist/esm/components/Tabs/Tab.js +10 -3
  21. package/dist/esm/components/Tabs/Tab.js.map +1 -1
  22. package/dist/esm/components/Tabs/Tabs.d.ts +6 -0
  23. package/dist/esm/components/Tabs/Tabs.d.ts.map +1 -1
  24. package/dist/esm/components/Tabs/Tabs.js +14 -4
  25. package/dist/esm/components/Tabs/Tabs.js.map +1 -1
  26. package/dist/esm/components/Tabs/TabsContext.d.ts +1 -0
  27. package/dist/esm/components/Tabs/TabsContext.d.ts.map +1 -1
  28. package/dist/esm/components/Tabs/TabsContext.js +2 -1
  29. package/dist/esm/components/Tabs/TabsContext.js.map +1 -1
  30. package/dist/esm/helpers/Popper/Popper.d.ts +10 -0
  31. package/dist/esm/helpers/Popper/Popper.d.ts.map +1 -1
  32. package/dist/esm/helpers/Popper/Popper.js.map +1 -1
  33. package/dist/js/components/ApplicationLauncher/ApplicationLauncher.d.ts +2 -9
  34. package/dist/js/components/ApplicationLauncher/ApplicationLauncher.d.ts.map +1 -1
  35. package/dist/js/components/ApplicationLauncher/ApplicationLauncher.js.map +1 -1
  36. package/dist/js/components/ContextSelector/ContextSelector.d.ts +2 -9
  37. package/dist/js/components/ContextSelector/ContextSelector.d.ts.map +1 -1
  38. package/dist/js/components/ContextSelector/ContextSelector.js.map +1 -1
  39. package/dist/js/components/Dropdown/Dropdown.d.ts +2 -9
  40. package/dist/js/components/Dropdown/Dropdown.d.ts.map +1 -1
  41. package/dist/js/components/Dropdown/Dropdown.js +2 -2
  42. package/dist/js/components/Dropdown/Dropdown.js.map +1 -1
  43. package/dist/js/components/OptionsMenu/OptionsMenu.d.ts +2 -9
  44. package/dist/js/components/OptionsMenu/OptionsMenu.d.ts.map +1 -1
  45. package/dist/js/components/OptionsMenu/OptionsMenu.js.map +1 -1
  46. package/dist/js/components/Select/Select.d.ts +2 -9
  47. package/dist/js/components/Select/Select.d.ts.map +1 -1
  48. package/dist/js/components/Select/Select.js.map +1 -1
  49. package/dist/js/components/Tabs/Tab.d.ts +5 -1
  50. package/dist/js/components/Tabs/Tab.d.ts.map +1 -1
  51. package/dist/js/components/Tabs/Tab.js +10 -3
  52. package/dist/js/components/Tabs/Tab.js.map +1 -1
  53. package/dist/js/components/Tabs/Tabs.d.ts +6 -0
  54. package/dist/js/components/Tabs/Tabs.d.ts.map +1 -1
  55. package/dist/js/components/Tabs/Tabs.js +14 -4
  56. package/dist/js/components/Tabs/Tabs.js.map +1 -1
  57. package/dist/js/components/Tabs/TabsContext.d.ts +1 -0
  58. package/dist/js/components/Tabs/TabsContext.d.ts.map +1 -1
  59. package/dist/js/components/Tabs/TabsContext.js +2 -1
  60. package/dist/js/components/Tabs/TabsContext.js.map +1 -1
  61. package/dist/js/helpers/Popper/Popper.d.ts +10 -0
  62. package/dist/js/helpers/Popper/Popper.d.ts.map +1 -1
  63. package/dist/js/helpers/Popper/Popper.js.map +1 -1
  64. package/dist/umd/react-core.css +511 -511
  65. package/dist/umd/react-core.js +38 -23
  66. package/dist/umd/react-core.min.css +107 -107
  67. package/dist/umd/react-core.min.js +2 -2
  68. package/package.json +6 -6
  69. package/src/components/ApplicationLauncher/ApplicationLauncher.tsx +2 -9
  70. package/src/components/ContextSelector/ContextSelector.tsx +2 -9
  71. package/src/components/Dropdown/Dropdown.tsx +3 -11
  72. package/src/components/OptionsMenu/OptionsMenu.tsx +2 -9
  73. package/src/components/Select/Select.tsx +3 -10
  74. package/src/components/Tabs/Tab.tsx +32 -2
  75. package/src/components/Tabs/Tabs.tsx +28 -2
  76. package/src/components/Tabs/TabsContext.ts +7 -1
  77. package/src/components/Tabs/__tests__/Tabs.test.tsx +22 -0
  78. package/src/components/Tabs/__tests__/__snapshots__/Tabs.test.tsx.snap +13 -13
  79. package/src/components/Tabs/examples/Tabs.md +7 -0
  80. package/src/components/Tabs/examples/TabsDynamic.tsx +64 -0
  81. package/src/helpers/Popper/Popper.tsx +11 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patternfly/react-core",
3
- "version": "4.212.0",
3
+ "version": "4.213.2",
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.63.0",
38
- "@patternfly/react-styles": "^4.62.0",
39
- "@patternfly/react-tokens": "^4.64.0",
37
+ "@patternfly/react-icons": "^4.64.2",
38
+ "@patternfly/react-styles": "^4.63.2",
39
+ "@patternfly/react-tokens": "^4.65.2",
40
40
  "focus-trap": "6.2.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.194.1",
46
+ "@patternfly/patternfly": "4.194.2",
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": "5c55ff97106790c5511f3b6177e0952bce0ba4c3"
63
+ "gitHead": "42579815df2b03fd9efa0082f41dd349fa04bfe4"
64
64
  }
@@ -9,9 +9,10 @@ import { ApplicationLauncherGroup } from './ApplicationLauncherGroup';
9
9
  import { ApplicationLauncherSeparator } from './ApplicationLauncherSeparator';
10
10
  import { ApplicationLauncherItem } from './ApplicationLauncherItem';
11
11
  import { ApplicationLauncherContext } from './ApplicationLauncherContext';
12
+ import { ToggleMenuBaseProps } from '../../helpers/Popper/Popper';
12
13
  import { createRenderableFavorites, extendItemsWithFavorite } from '../../helpers/favorites';
13
14
 
14
- export interface ApplicationLauncherProps extends React.HTMLProps<HTMLDivElement> {
15
+ export interface ApplicationLauncherProps extends ToggleMenuBaseProps, React.HTMLProps<HTMLDivElement> {
15
16
  /** Additional element css classes */
16
17
  className?: string;
17
18
  /** Display menu above or below dropdown toggle */
@@ -34,14 +35,6 @@ export interface ApplicationLauncherProps extends React.HTMLProps<HTMLDivElement
34
35
  isGrouped?: boolean;
35
36
  /** Toggle Icon, optional to override the icon used for the toggle */
36
37
  toggleIcon?: React.ReactNode;
37
- /** The container to append the menu to. Defaults to 'inline'.
38
- * If your menu is being cut off you can append it to an element higher up the DOM tree.
39
- * Some examples:
40
- * menuAppendTo="parent"
41
- * menuAppendTo={() => document.body}
42
- * menuAppendTo={document.getElementById('target')}
43
- */
44
- menuAppendTo?: HTMLElement | (() => HTMLElement) | 'inline' | 'parent';
45
38
  /** ID list of favorited ApplicationLauncherItems */
46
39
  favorites?: string[];
47
40
  /** Enables favorites. Callback called when an ApplicationLauncherItem's favorite button is clicked */
@@ -10,6 +10,7 @@ import { TextInput } from '../TextInput';
10
10
  import { InputGroup } from '../InputGroup';
11
11
  import { KEY_CODES } from '../../helpers/constants';
12
12
  import { FocusTrap } from '../../helpers';
13
+ import { ToggleMenuBaseProps } from '../../helpers/Popper/Popper';
13
14
  import { Popper } from '../../helpers/Popper/Popper';
14
15
  import { getOUIAProps, OUIAProps, getDefaultOUIAId } from '../../helpers';
15
16
 
@@ -17,7 +18,7 @@ import { getOUIAProps, OUIAProps, getDefaultOUIAId } from '../../helpers';
17
18
  let currentId = 0;
18
19
  const newId = currentId++;
19
20
 
20
- export interface ContextSelectorProps extends OUIAProps {
21
+ export interface ContextSelectorProps extends ToggleMenuBaseProps, OUIAProps {
21
22
  /** content rendered inside the Context Selector */
22
23
  children?: React.ReactNode;
23
24
  /** Classes applied to root element of Context Selector */
@@ -28,14 +29,6 @@ export interface ContextSelectorProps extends OUIAProps {
28
29
  onToggle?: (event: any, value: boolean) => void;
29
30
  /** Function callback called when user selects item */
30
31
  onSelect?: (event: any, value: React.ReactNode) => void;
31
- /** The container to append the menu to. Defaults to 'inline'.
32
- * If your menu is being cut off you can append it to an element higher up the DOM tree.
33
- * Some examples:
34
- * menuAppendTo="parent"
35
- * menuAppendTo={() => document.body}
36
- * menuAppendTo={document.getElementById('target')}
37
- */
38
- menuAppendTo?: HTMLElement | (() => HTMLElement) | 'inline' | 'parent';
39
32
  /** Flag indicating that the context selector should expand to full height */
40
33
  isFullHeight?: boolean;
41
34
  /** Labels the Context Selector for Screen Readers */
@@ -2,9 +2,10 @@ import * as React from 'react';
2
2
  import styles from '@patternfly/react-styles/css/components/Dropdown/dropdown';
3
3
  import { DropdownPosition, DropdownDirection, DropdownContext } from './dropdownConstants';
4
4
  import { DropdownWithContext } from './DropdownWithContext';
5
+ import { ToggleMenuBaseProps } from '../../helpers/Popper/Popper';
5
6
  import { OUIAProps, useOUIAId } from '../../helpers';
6
7
 
7
- export interface DropdownProps extends React.HTMLProps<HTMLDivElement>, OUIAProps {
8
+ export interface DropdownProps extends ToggleMenuBaseProps, React.HTMLProps<HTMLDivElement>, OUIAProps {
8
9
  /** Anything which can be rendered in a dropdown */
9
10
  children?: React.ReactNode;
10
11
  /** Classes applied to root element of dropdown */
@@ -31,14 +32,6 @@ export interface DropdownProps extends React.HTMLProps<HTMLDivElement>, OUIAProp
31
32
  };
32
33
  /** Display menu above or below dropdown toggle */
33
34
  direction?: DropdownDirection | 'up' | 'down';
34
- /** The container to append the menu to. Defaults to 'inline'.
35
- * If your menu is being cut off you can append it to an element higher up the DOM tree.
36
- * Some examples:
37
- * menuAppendTo="parent"
38
- * menuAppendTo={() => document.body}
39
- * menuAppendTo={document.getElementById('target')}
40
- */
41
- menuAppendTo?: HTMLElement | (() => HTMLElement) | 'inline' | 'parent';
42
35
  /** Flag to indicate if dropdown has groups */
43
36
  isGrouped?: boolean;
44
37
  /** Toggle for the dropdown, examples: <DropdownToggle> or <DropdownToggleCheckbox> */
@@ -61,7 +54,6 @@ export const Dropdown: React.FunctionComponent<DropdownProps> = ({
61
54
  ouiaSafe,
62
55
  alignments,
63
56
  contextProps,
64
- menuAppendTo = 'inline',
65
57
  ...props
66
58
  }: DropdownProps) => (
67
59
  <DropdownContext.Provider
@@ -87,7 +79,7 @@ export const Dropdown: React.FunctionComponent<DropdownProps> = ({
87
79
  ...contextProps
88
80
  }}
89
81
  >
90
- <DropdownWithContext menuAppendTo={menuAppendTo} {...props} />
82
+ <DropdownWithContext {...props} />
91
83
  </DropdownContext.Provider>
92
84
  );
93
85
  Dropdown.displayName = 'Dropdown';
@@ -3,6 +3,7 @@ import styles from '@patternfly/react-styles/css/components/OptionsMenu/options-
3
3
  import { DropdownContext } from '../Dropdown';
4
4
  import { DropdownWithContext } from '../Dropdown/DropdownWithContext';
5
5
  import { OUIAProps, useOUIAId } from '../../helpers';
6
+ import { ToggleMenuBaseProps } from '../../helpers/Popper/Popper';
6
7
 
7
8
  export enum OptionsMenuPosition {
8
9
  right = 'right',
@@ -14,7 +15,7 @@ export enum OptionsMenuDirection {
14
15
  down = 'down'
15
16
  }
16
17
 
17
- export interface OptionsMenuProps extends React.HTMLProps<HTMLDivElement>, OUIAProps {
18
+ export interface OptionsMenuProps extends ToggleMenuBaseProps, React.HTMLProps<HTMLDivElement>, OUIAProps {
18
19
  /** Classes applied to root element of the options menu */
19
20
  className?: string;
20
21
  /** Id of the root element of the options menu */
@@ -35,14 +36,6 @@ export interface OptionsMenuProps extends React.HTMLProps<HTMLDivElement>, OUIAP
35
36
  position?: 'right' | 'left';
36
37
  /** Menu will open up or open down from the options menu toggle */
37
38
  direction?: 'up' | 'down';
38
- /** The container to append the menu to. Defaults to 'inline'.
39
- * If your menu is being cut off you can append it to an element higher up the DOM tree.
40
- * Some examples:
41
- * menuAppendTo="parent"
42
- * menuAppendTo={() => document.body}
43
- * menuAppendTo={document.getElementById('target')}
44
- */
45
- menuAppendTo?: HTMLElement | (() => HTMLElement) | 'inline' | 'parent';
46
39
  }
47
40
 
48
41
  export const OptionsMenu: React.FunctionComponent<OptionsMenuProps> = ({
@@ -33,7 +33,7 @@ import {
33
33
  } from '../../helpers';
34
34
  import { KeyTypes } from '../../helpers/constants';
35
35
  import { Divider } from '../Divider';
36
- import { Popper } from '../../helpers/Popper/Popper';
36
+ import { ToggleMenuBaseProps, Popper } from '../../helpers/Popper/Popper';
37
37
  import { createRenderableFavorites, extendItemsWithFavorite } from '../../helpers/favorites';
38
38
  import { ValidatedOptions } from '../../helpers/constants';
39
39
  import { findTabbableElements } from '../../helpers/util';
@@ -48,7 +48,8 @@ export interface SelectViewMoreObject {
48
48
  onClick: (event: React.MouseEvent | React.ChangeEvent) => void;
49
49
  }
50
50
  export interface SelectProps
51
- extends Omit<React.HTMLProps<HTMLDivElement>, 'onSelect' | 'ref' | 'checked' | 'selected'>,
51
+ extends ToggleMenuBaseProps,
52
+ Omit<React.HTMLProps<HTMLDivElement>, 'onSelect' | 'ref' | 'checked' | 'selected'>,
52
53
  OUIAProps {
53
54
  /** Content rendered inside the Select. Must be React.ReactElement<SelectGroupProps>[] */
54
55
  children?: React.ReactElement[];
@@ -164,14 +165,6 @@ export interface SelectProps
164
165
  shouldResetOnSelect?: boolean;
165
166
  /** Content rendered in the footer of the select menu */
166
167
  footer?: React.ReactNode;
167
- /** The container to append the menu to. Defaults to 'inline'.
168
- * If your menu is being cut off you can append it to an element higher up the DOM tree.
169
- * Some examples:
170
- * menuAppendTo="parent"
171
- * menuAppendTo={() => document.body}
172
- * menuAppendTo={document.getElementById('target')}
173
- */
174
- menuAppendTo?: HTMLElement | (() => HTMLElement) | 'inline' | 'parent';
175
168
  }
176
169
 
177
170
  export interface SelectState {
@@ -5,6 +5,8 @@ import { TabButton } from './TabButton';
5
5
  import { TabsContext } from './TabsContext';
6
6
  import { css } from '@patternfly/react-styles';
7
7
  import { Tooltip } from '../Tooltip';
8
+ import { Button } from '../Button';
9
+ import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
8
10
 
9
11
  export interface TabProps extends Omit<React.HTMLProps<HTMLAnchorElement | HTMLButtonElement>, 'title'>, OUIAProps {
10
12
  /** content rendered inside the Tab content area. */
@@ -33,6 +35,10 @@ export interface TabProps extends Omit<React.HTMLProps<HTMLAnchorElement | HTMLB
33
35
  innerRef?: React.Ref<any>;
34
36
  /** Optional Tooltip rendered to a Tab. Should be <Tooltip> with appropriate props for proper rendering. */
35
37
  tooltip?: React.ReactElement<any>;
38
+ /** @beta Aria-label for the close button added by passing the onClose property to Tabs. */
39
+ closeButtonAriaLabel?: string;
40
+ /** @beta Flag indicating the close button should be disabled */
41
+ isCloseDisabled?: boolean;
36
42
  }
37
43
 
38
44
  const TabBase: React.FunctionComponent<TabProps> = ({
@@ -49,6 +55,8 @@ const TabBase: React.FunctionComponent<TabProps> = ({
49
55
  href,
50
56
  innerRef,
51
57
  tooltip,
58
+ closeButtonAriaLabel,
59
+ isCloseDisabled = false,
52
60
  ...props
53
61
  }: TabProps) => {
54
62
  const preventedEvents = inoperableEvents.reduce(
@@ -60,7 +68,9 @@ const TabBase: React.FunctionComponent<TabProps> = ({
60
68
  }),
61
69
  {}
62
70
  );
63
- const { mountOnEnter, localActiveKey, unmountOnExit, uniqueId, handleTabClick } = React.useContext(TabsContext);
71
+ const { mountOnEnter, localActiveKey, unmountOnExit, uniqueId, handleTabClick, handleTabClose } = React.useContext(
72
+ TabsContext
73
+ );
64
74
  let ariaControls = tabContentId ? `${tabContentId}` : `pf-tab-section-${eventKey}-${childId || uniqueId}`;
65
75
  if ((mountOnEnter || unmountOnExit) && eventKey !== localActiveKey) {
66
76
  ariaControls = undefined;
@@ -102,10 +112,30 @@ const TabBase: React.FunctionComponent<TabProps> = ({
102
112
 
103
113
  return (
104
114
  <li
105
- className={css(styles.tabsItem, eventKey === localActiveKey && styles.modifiers.current, childClassName)}
115
+ className={css(
116
+ styles.tabsItem,
117
+ eventKey === localActiveKey && styles.modifiers.current,
118
+ handleTabClose && styles.modifiers.action,
119
+ handleTabClose && (isDisabled || isAriaDisabled) && styles.modifiers.disabled,
120
+ childClassName
121
+ )}
106
122
  role="presentation"
107
123
  >
108
124
  {tooltip ? <Tooltip {...tooltip.props}>{tabButton}</Tooltip> : tabButton}
125
+ {handleTabClose !== undefined && (
126
+ <span className={css(styles.tabsItemClose)}>
127
+ <Button
128
+ variant="plain"
129
+ aria-label={closeButtonAriaLabel || 'Close tab'}
130
+ onClick={(event: any) => handleTabClose(event, eventKey, tabContentRef)}
131
+ isDisabled={isCloseDisabled}
132
+ >
133
+ <span className={css(styles.tabsItemCloseIcon)}>
134
+ <TimesIcon />
135
+ </span>
136
+ </Button>
137
+ </span>
138
+ )}
109
139
  </li>
110
140
  );
111
141
  };
@@ -5,6 +5,7 @@ import { css } from '@patternfly/react-styles';
5
5
  import { PickOptional } from '../../helpers/typeUtils';
6
6
  import AngleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-left-icon';
7
7
  import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon';
8
+ import PlusIcon from '@patternfly/react-icons/dist/esm/icons/plus-icon';
8
9
  import { getUniqueId, isElementInView, formatBreakpointMods } from '../../helpers/util';
9
10
  import { TabContent } from './TabContent';
10
11
  import { TabProps } from './Tab';
@@ -31,6 +32,12 @@ export interface TabsProps extends Omit<React.HTMLProps<HTMLElement | HTMLDivEle
31
32
  defaultActiveKey?: number | string;
32
33
  /** Callback to handle tab selection */
33
34
  onSelect?: (event: React.MouseEvent<HTMLElement, MouseEvent>, eventKey: number | string) => void;
35
+ /** @beta Callback to handle tab closing */
36
+ onClose?: (event: React.MouseEvent<HTMLElement, MouseEvent>, eventKey: number | string) => void;
37
+ /** @beta Callback for the add button. Passing this property inserts the add button */
38
+ onAdd?: () => void;
39
+ /** @beta Aria-label for the add button */
40
+ addButtonAriaLabel?: string;
34
41
  /** Uniquely identifies the tabs */
35
42
  id?: string;
36
43
  /** Enables the filled tab list layout */
@@ -273,13 +280,21 @@ export class Tabs extends React.Component<TabsProps, TabsState> {
273
280
  }
274
281
 
275
282
  componentDidUpdate(prevProps: TabsProps) {
276
- const { activeKey, mountOnEnter } = this.props;
283
+ const { activeKey, mountOnEnter, children } = this.props;
277
284
  const { shownKeys } = this.state;
278
285
  if (prevProps.activeKey !== activeKey && mountOnEnter && shownKeys.indexOf(activeKey) < 0) {
279
286
  this.setState({
280
287
  shownKeys: shownKeys.concat(activeKey)
281
288
  });
282
289
  }
290
+
291
+ if (
292
+ prevProps.children &&
293
+ children &&
294
+ React.Children.toArray(prevProps.children).length !== React.Children.toArray(children).length
295
+ ) {
296
+ this.handleScrollButtons();
297
+ }
283
298
  }
284
299
 
285
300
  render() {
@@ -311,7 +326,10 @@ export class Tabs extends React.Component<TabsProps, TabsState> {
311
326
  defaultIsExpanded,
312
327
  toggleText,
313
328
  toggleAriaLabel,
329
+ addButtonAriaLabel,
314
330
  onToggle,
331
+ onClose,
332
+ onAdd,
315
333
  ...props
316
334
  } = this.props;
317
335
  const {
@@ -348,7 +366,8 @@ export class Tabs extends React.Component<TabsProps, TabsState> {
348
366
  unmountOnExit,
349
367
  localActiveKey,
350
368
  uniqueId,
351
- handleTabClick: (...args) => this.handleTabClick(...args)
369
+ handleTabClick: (...args) => this.handleTabClick(...args),
370
+ handleTabClose: onClose
352
371
  }}
353
372
  >
354
373
  <Component
@@ -421,6 +440,13 @@ export class Tabs extends React.Component<TabsProps, TabsState> {
421
440
  >
422
441
  <AngleRightIcon />
423
442
  </button>
443
+ {onAdd !== undefined && (
444
+ <span className={css(styles.tabsAdd)}>
445
+ <Button variant="plain" aria-label={addButtonAriaLabel || 'Add tab'} onClick={onAdd}>
446
+ <PlusIcon />
447
+ </Button>
448
+ </span>
449
+ )}
424
450
  </Component>
425
451
  {filteredChildren
426
452
  .filter(
@@ -11,6 +11,11 @@ export interface TabsContextProps {
11
11
  eventKey: number | string,
12
12
  tabContentRef: React.RefObject<any>
13
13
  ) => void;
14
+ handleTabClose: (
15
+ event: React.MouseEvent<HTMLElement, MouseEvent>,
16
+ eventKey: number | string,
17
+ tabContentRef?: React.RefObject<any>
18
+ ) => void;
14
19
  }
15
20
 
16
21
  export const TabsContext = React.createContext<TabsContextProps>({
@@ -19,7 +24,8 @@ export const TabsContext = React.createContext<TabsContextProps>({
19
24
  unmountOnExit: false,
20
25
  localActiveKey: '',
21
26
  uniqueId: '',
22
- handleTabClick: () => null
27
+ handleTabClick: () => null,
28
+ handleTabClose: undefined
23
29
  });
24
30
 
25
31
  export const TabsContextProvider = TabsContext.Provider;
@@ -38,6 +38,28 @@ test('should render simple tabs', () => {
38
38
  expect(asFragment()).toMatchSnapshot();
39
39
  });
40
40
 
41
+ test('should render closeable tabs', () => {
42
+ const view = render(
43
+ <Tabs onClose={jest.fn()}>
44
+ <Tab eventKey={0} title={<TabTitleText>"Tab item 1"</TabTitleText>} closeButtonAriaLabel="close-label">
45
+ Tab 1 section
46
+ </Tab>
47
+ </Tabs>
48
+ );
49
+ expect(screen.getByLabelText('close-label')).toBeTruthy();
50
+ });
51
+
52
+ test('should render add button', () => {
53
+ const view = render(
54
+ <Tabs onAdd={jest.fn()} addButtonAriaLabel="add-label">
55
+ <Tab eventKey={0} title={<TabTitleText>"Tab item 1"</TabTitleText>} closeButtonAriaLabel="close-label">
56
+ Tab 1 section
57
+ </Tab>
58
+ </Tabs>
59
+ );
60
+ expect(screen.getByLabelText('add-label')).toBeTruthy();
61
+ });
62
+
41
63
  test('should render uncontrolled tabs', () => {
42
64
  const { asFragment } = render(
43
65
  <Tabs defaultActiveKey={0}>
@@ -5,7 +5,7 @@ exports[`should render accessible tabs 1`] = `
5
5
  <nav
6
6
  aria-label="accessible Tabs example"
7
7
  class="pf-c-tabs"
8
- data-ouia-component-id="OUIA-Generated-Tabs-7"
8
+ data-ouia-component-id="OUIA-Generated-Tabs-9"
9
9
  data-ouia-component-type="PF4/Tabs"
10
10
  data-ouia-safe="true"
11
11
  id="accessibleTabs"
@@ -161,7 +161,7 @@ exports[`should render box tabs 1`] = `
161
161
  <DocumentFragment>
162
162
  <div
163
163
  class="pf-c-tabs pf-m-box"
164
- data-ouia-component-id="OUIA-Generated-Tabs-6"
164
+ data-ouia-component-id="OUIA-Generated-Tabs-8"
165
165
  data-ouia-component-type="PF4/Tabs"
166
166
  data-ouia-safe="true"
167
167
  id="boxTabs"
@@ -355,7 +355,7 @@ exports[`should render box tabs of light variant 1`] = `
355
355
  <DocumentFragment>
356
356
  <div
357
357
  class="pf-c-tabs pf-m-box pf-m-color-scheme--light-300"
358
- data-ouia-component-id="OUIA-Generated-Tabs-13"
358
+ data-ouia-component-id="OUIA-Generated-Tabs-15"
359
359
  data-ouia-component-type="PF4/Tabs"
360
360
  data-ouia-safe="true"
361
361
  id="boxLightVariantTabs"
@@ -508,7 +508,7 @@ exports[`should render expandable vertical tabs 1`] = `
508
508
  <DocumentFragment>
509
509
  <div
510
510
  class="pf-c-tabs pf-m-vertical pf-m-expandable"
511
- data-ouia-component-id="OUIA-Generated-Tabs-4"
511
+ data-ouia-component-id="OUIA-Generated-Tabs-6"
512
512
  data-ouia-component-type="PF4/Tabs"
513
513
  data-ouia-safe="true"
514
514
  id="verticalTabs"
@@ -523,7 +523,7 @@ exports[`should render expandable vertical tabs 1`] = `
523
523
  aria-disabled="false"
524
524
  aria-labelledby="pf-random-id-0-text pf-random-id-0-button"
525
525
  class="pf-c-button pf-m-plain"
526
- data-ouia-component-id="OUIA-Generated-Button-plain-1"
526
+ data-ouia-component-id="OUIA-Generated-Button-plain-3"
527
527
  data-ouia-component-type="PF4/Button"
528
528
  data-ouia-safe="true"
529
529
  id="pf-random-id-0-button"
@@ -745,7 +745,7 @@ exports[`should render filled tabs 1`] = `
745
745
  <DocumentFragment>
746
746
  <div
747
747
  class="pf-c-tabs pf-m-fill"
748
- data-ouia-component-id="OUIA-Generated-Tabs-8"
748
+ data-ouia-component-id="OUIA-Generated-Tabs-10"
749
749
  data-ouia-component-type="PF4/Tabs"
750
750
  data-ouia-safe="true"
751
751
  id="filledTabs"
@@ -898,7 +898,7 @@ exports[`should render secondary tabs 1`] = `
898
898
  <DocumentFragment>
899
899
  <div
900
900
  class="pf-c-tabs"
901
- data-ouia-component-id="OUIA-Generated-Tabs-9"
901
+ data-ouia-component-id="OUIA-Generated-Tabs-11"
902
902
  data-ouia-component-type="PF4/Tabs"
903
903
  data-ouia-safe="true"
904
904
  id="primarieTabs"
@@ -1020,7 +1020,7 @@ exports[`should render secondary tabs 1`] = `
1020
1020
  >
1021
1021
  <div
1022
1022
  class="pf-c-tabs pf-m-secondary"
1023
- data-ouia-component-id="OUIA-Generated-Tabs-10"
1023
+ data-ouia-component-id="OUIA-Generated-Tabs-12"
1024
1024
  data-ouia-component-type="PF4/Tabs"
1025
1025
  data-ouia-safe="true"
1026
1026
  id="secondaryTabs"
@@ -1393,7 +1393,7 @@ exports[`should render tabs with eventKey Strings 1`] = `
1393
1393
  <DocumentFragment>
1394
1394
  <div
1395
1395
  class="pf-c-tabs"
1396
- data-ouia-component-id="OUIA-Generated-Tabs-11"
1396
+ data-ouia-component-id="OUIA-Generated-Tabs-13"
1397
1397
  data-ouia-component-type="PF4/Tabs"
1398
1398
  data-ouia-safe="true"
1399
1399
  id="eventKeyTabs"
@@ -1547,7 +1547,7 @@ exports[`should render tabs with no bottom border 1`] = `
1547
1547
  <DocumentFragment>
1548
1548
  <div
1549
1549
  class="pf-c-tabs pf-m-no-border-bottom"
1550
- data-ouia-component-id="OUIA-Generated-Tabs-14"
1550
+ data-ouia-component-id="OUIA-Generated-Tabs-16"
1551
1551
  data-ouia-component-type="PF4/Tabs"
1552
1552
  data-ouia-safe="true"
1553
1553
  id="noBottomBorderTabs"
@@ -1700,7 +1700,7 @@ exports[`should render tabs with separate content 1`] = `
1700
1700
  <DocumentFragment>
1701
1701
  <div
1702
1702
  class="pf-c-tabs"
1703
- data-ouia-component-id="OUIA-Generated-Tabs-12"
1703
+ data-ouia-component-id="OUIA-Generated-Tabs-14"
1704
1704
  data-ouia-component-type="PF4/Tabs"
1705
1705
  data-ouia-safe="true"
1706
1706
  id="separateTabs"
@@ -1863,7 +1863,7 @@ exports[`should render uncontrolled tabs 1`] = `
1863
1863
  <DocumentFragment>
1864
1864
  <div
1865
1865
  class="pf-c-tabs"
1866
- data-ouia-component-id="OUIA-Generated-Tabs-2"
1866
+ data-ouia-component-id="OUIA-Generated-Tabs-4"
1867
1867
  data-ouia-component-type="PF4/Tabs"
1868
1868
  data-ouia-safe="true"
1869
1869
  >
@@ -2056,7 +2056,7 @@ exports[`should render vertical tabs 1`] = `
2056
2056
  <DocumentFragment>
2057
2057
  <div
2058
2058
  class="pf-c-tabs pf-m-vertical"
2059
- data-ouia-component-id="OUIA-Generated-Tabs-3"
2059
+ data-ouia-component-id="OUIA-Generated-Tabs-5"
2060
2060
  data-ouia-component-type="PF4/Tabs"
2061
2061
  data-ouia-safe="true"
2062
2062
  id="verticalTabs"
@@ -1617,3 +1617,10 @@ class ToggledSeparateContent extends React.Component {
1617
1617
  }
1618
1618
  }
1619
1619
  ```
1620
+
1621
+ ### Dynamic
1622
+
1623
+ To enable closeable tabs, pass the `onClose` property to `Tabs`, and to enable the add button, pass the `onAdd` property to `Tabs`. Aria labels may be controlled manually by passing `closeButtonAriaLabel` to `Tab` and `addButtonAriaLabel` to `Tabs`.
1624
+
1625
+ ```ts file="./TabsDynamic.tsx" isBeta
1626
+ ```
@@ -0,0 +1,64 @@
1
+ import React from 'react';
2
+ import { Tabs, Tab, TabTitleText } from '@patternfly/react-core';
3
+
4
+ export const TabsDynamic: React.FunctionComponent = () => {
5
+ const [activeTabKey, setActiveTabKey] = React.useState<number>(0);
6
+ const [tabs, setTabs] = React.useState<string[]>(['Terminal 1', 'Terminal 2', 'Terminal 3']);
7
+ const [newTabNumber, setNewTabNumber] = React.useState<number>(4);
8
+ const tabComponentRef = React.useRef<any>();
9
+ const firstMount = React.useRef(true);
10
+
11
+ const onClose = (event: any, tabIndex: string | number) => {
12
+ const tabIndexNum = tabIndex as number;
13
+ let nextTabIndex = activeTabKey;
14
+ if (tabIndexNum < activeTabKey) {
15
+ // if a preceding tab is closing, keep focus on the new index of the current tab
16
+ nextTabIndex = activeTabKey - 1 > 0 ? activeTabKey - 1 : 0;
17
+ } else if (activeTabKey === tabs.length - 1) {
18
+ // if the closing tab is the last tab, focus the preceding tab
19
+ nextTabIndex = tabs.length - 2 > 0 ? tabs.length - 2 : 0;
20
+ }
21
+ setActiveTabKey(nextTabIndex);
22
+ setTabs(tabs.filter((tab, index) => index !== tabIndex));
23
+ };
24
+
25
+ const onAdd = () => {
26
+ setTabs([...tabs, `Terminal ${newTabNumber}`]);
27
+ setActiveTabKey(tabs.length);
28
+ setNewTabNumber(newTabNumber + 1);
29
+ };
30
+
31
+ React.useEffect(() => {
32
+ if (firstMount.current) {
33
+ firstMount.current = false;
34
+ return;
35
+ } else {
36
+ const first = tabComponentRef.current.tabList.current.childNodes[activeTabKey];
37
+ first && first.firstChild.focus();
38
+ }
39
+ }, [tabs]);
40
+
41
+ return (
42
+ <Tabs
43
+ activeKey={activeTabKey}
44
+ onSelect={(event: any, tabIndex: string | number) => setActiveTabKey(tabIndex as number)}
45
+ onClose={onClose}
46
+ onAdd={onAdd}
47
+ aria-label="Tabs in the addable/closeable example"
48
+ addButtonAriaLabel="Add new tab"
49
+ ref={tabComponentRef}
50
+ >
51
+ {tabs.map((tab, index) => (
52
+ <Tab
53
+ key={index}
54
+ eventKey={index}
55
+ title={<TabTitleText>{tab}</TabTitleText>}
56
+ closeButtonAriaLabel={`Close ${tab}`}
57
+ isCloseDisabled={tabs.length === 1}
58
+ >
59
+ {tab}
60
+ </Tab>
61
+ ))}
62
+ </Tabs>
63
+ );
64
+ };
@@ -14,6 +14,17 @@ const getOppositePlacement = (placement: Placement): any =>
14
14
  (matched: string) => hash[matched as 'left' | 'right' | 'bottom' | 'top'] as BasePlacement
15
15
  );
16
16
 
17
+ export interface ToggleMenuBaseProps {
18
+ /** The container to append the menu to. Defaults to 'inline'
19
+ * If your menu is being cut off you can append it to an element higher up the DOM tree.
20
+ * Some examples:
21
+ * menuAppendTo="parent"
22
+ * menuAppendTo={() => document.body}
23
+ * menuAppendTo={document.getElementById('target')}
24
+ */
25
+ menuAppendTo?: HTMLElement | (() => HTMLElement) | 'parent' | 'inline';
26
+ }
27
+
17
28
  export const getOpacityTransition = (animationDuration: number) =>
18
29
  `opacity ${animationDuration}ms cubic-bezier(.54, 1.5, .38, 1.11)`;
19
30