- Toggle tooltip
+exports[`Matches snapshot 1`] = `
+
`;
diff --git a/packages/react-core/src/components/Tooltip/__tests__/__snapshots__/TooltipArrow.test.tsx.snap b/packages/react-core/src/components/Tooltip/__tests__/__snapshots__/TooltipArrow.test.tsx.snap
new file mode 100644
index 00000000000..91ea39d2799
--- /dev/null
+++ b/packages/react-core/src/components/Tooltip/__tests__/__snapshots__/TooltipArrow.test.tsx.snap
@@ -0,0 +1,10 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Matches the snapshot 1`] = `
+
+
+
+`;
diff --git a/packages/react-core/src/components/Tooltip/__tests__/__snapshots__/TooltipContent.test.tsx.snap b/packages/react-core/src/components/Tooltip/__tests__/__snapshots__/TooltipContent.test.tsx.snap
new file mode 100644
index 00000000000..a2be96e7d46
--- /dev/null
+++ b/packages/react-core/src/components/Tooltip/__tests__/__snapshots__/TooltipContent.test.tsx.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Matches the snapshot 1`] = `
+
+
+ Test content
+
+
+`;
diff --git a/packages/react-core/src/components/TreeView/examples/TreeView.md b/packages/react-core/src/components/TreeView/examples/TreeView.md
index 710fb7e8366..9039d6c8857 100644
--- a/packages/react-core/src/components/TreeView/examples/TreeView.md
+++ b/packages/react-core/src/components/TreeView/examples/TreeView.md
@@ -11,1289 +11,76 @@ import { FolderIcon, FolderOpenIcon, EllipsisVIcon, ClipboardIcon, HamburgerIcon
### Default
-```js
-import React from 'react';
-import { TreeView, Button } from '@patternfly/react-core';
+```ts file='./TreeViewDefault.tsx'
-class DefaultTreeView extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = { activeItems: [], allExpanded: null };
-
- this.onSelect = (evt, treeViewItem) => {
- // Ignore folders for selection
- if (treeViewItem && !treeViewItem.children) {
- this.setState({
- activeItems: [treeViewItem]
- });
- }
- };
-
- this.onToggle = (evt) => {
- const { allExpanded } = this.state;
- this.setState({
- allExpanded: allExpanded !== undefined ? !allExpanded : true
- });
- };
- }
-
- render() {
- const { activeItems, allExpanded } = this.state;
-
- const options = [
- {
- name: 'Application launcher',
- id: 'example1-AppLaunch',
- children: [
- {
- name: 'Application 1',
- id: 'example1-App1',
- children: [
- { name: 'Settings', id: 'example1-App1Settings' },
- { name: 'Current', id: 'example1-App1Current' }
- ]
- },
- {
- name: 'Application 2',
- id: 'example1-App2',
- children: [
- { name: 'Settings', id: 'example1-App2Settings' },
- {
- name: 'Loader',
- id: 'example1-App2Loader',
- children: [
- { name: 'Loading App 1', id: 'example1-LoadApp1' },
- { name: 'Loading App 2', id: 'example1-LoadApp2' },
- { name: 'Loading App 3', id: 'example1-LoadApp3' }
- ]
- }
- ]
- }
- ],
- defaultExpanded: true
- },
- {
- name: 'Cost management',
- id: 'example1-Cost',
- children: [
- {
- name: 'Application 3',
- id: 'example1-App3',
- children: [
- { name: 'Settings', id: 'example1-App3Settings' },
- { name: 'Current', id: 'example1-App3Current' }
- ]
- }
- ]
- },
- {
- name: 'Sources',
- id: 'example1-Sources',
- children: [
- { name: 'Application 4', id: 'example1-App4', children: [{ name: 'Settings', id: 'example1-App4Settings' }] }
- ]
- },
- {
- name: 'Really really really long folder name that overflows the container it is in',
- id: 'example1-Long',
- children: [{ name: 'Application 5', id: 'example1-App5' }]
- }
- ];
- return (
-
-
- {allExpanded && 'Collapse all'}
- {!allExpanded && 'Expand all'}
-
-
-
- );
- }
-}
```
### With separate selection and expansion
-The `hasSelectableNodes` modifier will separate the expansion and selection behaviors, allowing a parent node to be selected or deselected with toggling its expansion.
-
-```js
-import React from 'react';
-import { TreeView, Button } from '@patternfly/react-core';
-
-class SelectableNodesTreeView extends React.Component {
- constructor(props) {
- super(props);
+The `hasSelectableNodes` modifier will separate the expansion and selection behaviors, allowing a parent node to be selected or deselected without toggling its expansion.
- this.state = { activeItems: {} };
+```ts file='./TreeViewSelectionExpansion.tsx'
- this.onSelect = (evt, treeViewItem) => {
- this.setState({
- activeItems: [treeViewItem]
- });
- };
- }
-
- render() {
- const { activeItems, allExpanded } = this.state;
-
- const options = [
- {
- name: 'Application launcher',
- id: 'SelNodesTreeView-AppLaunch',
- children: [
- {
- name: 'Application 1',
- id: 'SelNodesTreeView-App1',
- children: [
- { name: 'Settings', id: 'SelNodesTreeView-App1Settings' },
- { name: 'Current', id: 'SelNodesTreeView-App1Current' }
- ]
- },
- {
- name: 'Application 2',
- id: 'SelNodesTreeView-App2',
- children: [
- { name: 'Settings', id: 'SelNodesTreeView-App2Settings' },
- {
- name: 'Loader',
- id: 'SelNodesTreeView-App2Loader',
- children: [
- { name: 'Loading App 1', id: 'SelNodesTreeView-LoadApp1' },
- { name: 'Loading App 2', id: 'SelNodesTreeView-LoadApp2' },
- { name: 'Loading App 3', id: 'SelNodesTreeView-LoadApp3' }
- ]
- }
- ]
- }
- ],
- defaultExpanded: true
- },
- {
- name: 'Cost management',
- id: 'SelNodesTreeView-Cost',
- children: [
- {
- name: 'Application 3',
- id: 'SelNodesTreeView-App3',
- children: [
- { name: 'Settings', id: 'SelNodesTreeView-App3Settings' },
- { name: 'Current', id: 'SelNodesTreeView-App3Current' }
- ]
- }
- ]
- },
- {
- name: 'Sources',
- id: 'SelNodesTreeView-Sources',
- children: [
- {
- name: 'Application 4',
- id: 'SelNodesTreeView-App4',
- children: [{ name: 'Settings', id: 'SelNodesTreeView-App4Settings' }]
- }
- ]
- },
- {
- name: 'Really really really long folder name that overflows the container it is in',
- id: 'SelNodesTreeView-Long',
- children: [{ name: 'Application 5', id: 'SelNodesTreeView-App5' }]
- }
- ];
- return (
-
- );
- }
-}
```
### With search
-```js
-import React from 'react';
-import { Toolbar, ToolbarContent, ToolbarItem, TreeView, TreeViewSearch } from '@patternfly/react-core';
-
-class SearchTreeView extends React.Component {
- constructor(props) {
- super(props);
-
- this.options = [
- {
- name: 'Application launcher',
- id: 'example2-AppLaunch',
- children: [
- {
- name: 'Application 1',
- id: 'example2-App1',
- children: [
- { name: 'Settings', id: 'example2-App1Settings' },
- { name: 'Current', id: 'example2-App1Current' }
- ]
- },
- {
- name: 'Application 2',
- id: 'example2-App2',
- children: [
- { name: 'Settings', id: 'example2-App2Settings' },
- {
- name: 'Loader',
- id: 'example2-App2Loader',
- children: [
- { name: 'Loading App 1', id: 'example2-LoadApp1' },
- { name: 'Loading App 2', id: 'example2-LoadApp2' },
- { name: 'Loading App 3', id: 'example2-LoadApp3' }
- ]
- }
- ]
- }
- ],
- defaultExpanded: true
- },
- {
- name: 'Cost management',
- id: 'example2-Cost',
- children: [
- {
- name: 'Application 3',
- id: 'example2-App3',
- children: [
- { name: 'Settings', id: 'example2-App3Settings' },
- { name: 'Current', id: 'example2-App3Current' }
- ]
- }
- ]
- },
- {
- name: 'Sources',
- id: 'example2-Sources',
- children: [
- {
- name: 'Application 4',
- id: 'example2-App4',
- children: [{ name: 'Settingexample2-s', id: 'example2-App4Settings' }]
- }
- ]
- },
- {
- name: 'Really really really long folder name that overflows the container it is in',
- id: 'example2-Long',
- children: [{ name: 'Application 5', id: 'example2-App5' }]
- }
- ];
-
- this.state = { activeItems: {}, filteredItems: this.options, isFiltered: null };
-
- this.onSelect = (evt, treeViewItem) => {
- // Ignore folders for selection
- if (treeViewItem && !treeViewItem.children) {
- this.setState({
- activeItems: [treeViewItem]
- });
- }
- };
-
- this.onSearch = (evt) => {
- const input = evt.target.value;
- if (input === '') {
- this.setState({ filteredItems: this.options, isFiltered: false });
- } else {
- const filtered = this.options
- .map((opt) => Object.assign({}, opt))
- .filter((item) => this.filterItems(item, input));
- this.setState({ filteredItems: filtered, isFiltered: true });
- }
- };
+```ts file='./TreeViewWithSearch.tsx'
- this.filterItems = (item, input) => {
- if (item.name.toLowerCase().includes(input.toLowerCase())) {
- return true;
- }
-
- if (item.children) {
- return (
- (item.children = item.children
- .map((opt) => Object.assign({}, opt))
- .filter((child) => this.filterItems(child, input))).length > 0
- );
- }
- };
- }
-
- render() {
- const { activeItems, filteredItems, isFiltered } = this.state;
-
- const toolbar = (
-
-
-
-
-
-
-
- );
-
- return (
-
- );
- }
-}
```
### With checkboxes
-```js
-import React from 'react';
-import { TreeView } from '@patternfly/react-core';
-
-class CheckboxTreeView extends React.Component {
- constructor(props) {
- super(props);
- this.options = [
- {
- name: 'Application launcher',
- id: 'example3-AppLaunch',
- checkProps: { 'aria-label': 'app-launcher-check', checked: false },
- children: [
- {
- name: 'Application 1',
- id: 'example3-App1',
- checkProps: { checked: false },
- children: [
- {
- name: 'Settings',
- id: 'example3-App1Settings',
- checkProps: { checked: false }
- },
- {
- name: 'Current',
- id: 'example3-App1Current',
- checkProps: { checked: false }
- }
- ]
- },
- {
- name: 'Application 2',
- id: 'example3-App2',
- checkProps: { checked: false },
- children: [
- {
- name: 'Settings',
- id: 'example3-App2Settings',
- checkProps: { checked: false }
- },
- {
- name: 'Loader',
- id: 'example3-App2Loader',
- checkProps: { checked: false },
- children: [
- {
- name: 'Loading App 1',
- id: 'example3-LoadApp1',
- checkProps: { checked: false }
- },
- {
- name: 'Loading App 2',
- id: 'example3-LoadApp2',
- checkProps: { checked: false }
- },
- {
- name: 'Loading App 3',
- id: 'example3-LoadApp3',
- checkProps: { checked: false }
- }
- ]
- }
- ]
- }
- ],
- defaultExpanded: true
- },
- {
- name: 'Cost management',
- id: 'example3-Cost',
- checkProps: { 'aria-label': 'cost-check', checked: false },
- children: [
- {
- name: 'Application 3',
- id: 'example3-App3',
- checkProps: { 'aria-label': 'app-3-check', checked: false },
- children: [
- {
- name: 'Settings',
- id: 'example3-App3Settings',
- checkProps: { 'aria-label': 'app-3-settings-check', checked: false }
- },
- {
- name: 'Current',
- id: 'example3-App3Current',
- checkProps: { 'aria-label': 'app-3-current-check', checked: false }
- }
- ]
- }
- ]
- },
- {
- name: 'Sources',
- id: 'example3-Sources',
- checkProps: { 'aria-label': 'sources-check', checked: false },
- children: [
- {
- name: 'Application 4',
- id: 'example3-App4',
- checkProps: { 'aria-label': 'app-4-check', checked: false },
- children: [
- {
- name: 'Settings',
- id: 'example3-App4Settings',
- checkProps: { 'aria-label': 'app-4-settings-check', checked: false }
- }
- ]
- }
- ]
- },
- {
- name: 'Really really really long folder name that overflows the container it is in',
- id: 'example3-Long',
- checkProps: { 'aria-label': 'long-check', checked: false },
- children: [
- { name: 'Application 5', id: 'example3-App5', checkProps: { 'aria-label': 'app-5-check', checked: false } }
- ]
- }
- ];
-
- this.state = { checkedItems: [] };
-
- this.onCheck = (evt, treeViewItem) => {
- const checked = evt.target.checked;
- console.log(checked);
-
- const checkedItemTree = this.options
- .map((opt) => Object.assign({}, opt))
- .filter((item) => this.filterItems(item, treeViewItem));
- const flatCheckedItems = this.flattenTree(checkedItemTree);
- console.log('flat', flatCheckedItems);
-
- this.setState(
- (prevState) => ({
- checkedItems: checked
- ? prevState.checkedItems.concat(
- flatCheckedItems.filter((item) => !prevState.checkedItems.some((i) => i.id === item.id))
- )
- : prevState.checkedItems.filter((item) => !flatCheckedItems.some((i) => i.id === item.id))
- }),
- () => {
- console.log('Checked items: ', this.state.checkedItems);
- }
- );
- };
-
- // Helper functions
- const isChecked = (dataItem) => this.state.checkedItems.some((item) => item.id === dataItem.id);
- const areAllDescendantsChecked = (dataItem) =>
- dataItem.children ? dataItem.children.every((child) => areAllDescendantsChecked(child)) : isChecked(dataItem);
- const areSomeDescendantsChecked = (dataItem) =>
- dataItem.children ? dataItem.children.some((child) => areSomeDescendantsChecked(child)) : isChecked(dataItem);
-
- this.flattenTree = (tree) => {
- var result = [];
- tree.forEach((item) => {
- result.push(item);
- if (item.children) {
- result = result.concat(this.flattenTree(item.children));
- }
- });
- return result;
- };
-
- this.mapTree = (item) => {
- const hasCheck = areAllDescendantsChecked(item);
- // Reset checked properties to be updated
- item.checkProps.checked = false;
-
- if (hasCheck) {
- item.checkProps.checked = true;
- } else {
- const hasPartialCheck = areSomeDescendantsChecked(item);
- if (hasPartialCheck) {
- item.checkProps.checked = null;
- }
- }
+```ts file='./TreeViewWithCheckboxes.tsx'
- if (item.children) {
- return {
- ...item,
- children: item.children.map((child) => this.mapTree(child))
- };
- }
- return item;
- };
-
- this.filterItems = (item, checkedItem) => {
- if (item.id === checkedItem.id) {
- return true;
- }
-
- if (item.children) {
- return (
- (item.children = item.children
- .map((opt) => Object.assign({}, opt))
- .filter((child) => this.filterItems(child, checkedItem))).length > 0
- );
- }
- };
- }
-
- render() {
- const mapped = this.options.map((item) => this.mapTree(item));
- return
;
- }
-}
```
### With icons
-```js
-import React from 'react';
-import { TreeView } from '@patternfly/react-core';
-import FolderIcon from '@patternfly/react-icons/dist/esm/icons/folder-icon';
-import FolderOpenIcon from '@patternfly/react-icons/dist/esm/icons/folder-open-icon';
-
-class IconTreeView extends React.Component {
- constructor(props) {
- super(props);
+```ts file='./TreeViewWithIcons.tsx'
- this.state = { activeItems: {} };
-
- this.onSelect = (evt, treeViewItem) => {
- // Ignore folders for selection
- if (treeViewItem && !treeViewItem.children) {
- this.setState({
- activeItems: [treeViewItem]
- });
- }
- };
- }
-
- render() {
- const { activeItems } = this.state;
- const options = [
- {
- name: 'Application launcher',
- id: 'example4-AppLaunch',
- children: [
- {
- name: 'Application 1',
- id: 'example4-App1',
- children: [
- { name: 'Settings', id: 'example4-App1Settings' },
- { name: 'Current', id: 'example4-App1Current' }
- ]
- },
- {
- name: 'Application 2',
- id: 'example4-App2',
- children: [
- { name: 'Settings', id: 'example4-App2Settings' },
- {
- name: 'Loader',
- id: 'example4-App2Loader',
- children: [
- { name: 'Loading App 1', id: 'example4-LoadApp1' },
- { name: 'Loading App 2', id: 'example4-LoadApp2' },
- { name: 'Loading App 3', id: 'example4-LoadApp3' }
- ]
- }
- ]
- }
- ],
- defaultExpanded: true
- },
- {
- name: 'Cost management',
- id: 'example4-Cost',
- children: [
- {
- name: 'Application 3',
- id: 'example4-App3',
- children: [
- { name: 'Settings', id: 'example4-App3Settings' },
- { name: 'Current', id: 'example4-App3Current' }
- ]
- }
- ]
- },
- {
- name: 'Sources',
- id: 'example4-Sources',
- children: [
- { name: 'Application 4', id: 'example4-App4', children: [{ name: 'Settings', id: 'example4-App4Settings' }] }
- ]
- },
- {
- name: 'Really really really long folder name that overflows the container it is in',
- id: 'example4-Long',
- children: [{ name: 'Application 5', id: 'example4-App5' }]
- }
- ];
- return (
-
}
- expandedIcon={
}
- />
- );
- }
-}
```
### With badges
-```js
-import React from 'react';
-import { TreeView } from '@patternfly/react-core';
-
-class BadgesTreeView extends React.Component {
- constructor(props) {
- super(props);
+```ts file='./TreeViewWithBadges.tsx'
- this.state = { activeItems: {} };
-
- this.onSelect = (evt, treeViewItem) => {
- // Ignore folders for selection
- if (treeViewItem && !treeViewItem.children) {
- this.setState({
- activeItems: [treeViewItem]
- });
- }
- };
- }
-
- render() {
- const { activeItems } = this.state;
- const options = [
- {
- name: 'Application launcher',
- id: 'example5-AppLaunch',
- children: [
- {
- name: 'Application 1',
- id: 'example5-App1',
- children: [
- { name: 'Settings', id: 'example5-App1Settings' },
- { name: 'Current', id: 'example5-App1Current' }
- ]
- },
- {
- name: 'Application 2',
- id: 'example5-App2',
- children: [
- { name: 'Settings', id: 'example5-App2Settings' },
- {
- name: 'Loader',
- id: 'example5-App2Loader',
- children: [
- { name: 'Loading App 1', id: 'example5-LoadApp1' },
- { name: 'Loading App 2', id: 'example5-LoadApp2' },
- { name: 'Loading App 3', id: 'example5-LoadApp3' }
- ]
- }
- ]
- }
- ],
- defaultExpanded: true
- },
- {
- name: 'Cost management',
- id: 'example5-Cost',
- children: [
- {
- name: 'Application 3',
- id: 'example5-App3',
- children: [
- { name: 'Settings', id: 'example5-App3Settings' },
- { name: 'Current', id: 'example5-App3Current' }
- ]
- }
- ]
- },
- {
- name: 'Sources',
- id: 'example5-Sources',
- children: [
- { name: 'Application 4', id: 'example5-App4', children: [{ name: 'Settings', id: 'example5-App4Settings' }] }
- ]
- },
- {
- name: 'Really really really long folder name that overflows the container it is in',
- id: 'example5-Long',
- children: [{ name: 'Application 5', id: 'example5-App5' }]
- }
- ];
- return
;
- }
-}
```
### With custom badges
-```js
-import React from 'react';
-import { TreeView } from '@patternfly/react-core';
-
-class CustomBadgesTreeView extends React.Component {
- constructor(props) {
- super(props);
+```ts file='./TreeViewWithCustomBadges.tsx'
- this.state = { activeItems: {} };
-
- this.onSelect = (evt, treeViewItem) => {
- // Ignore folders for selection
- if (treeViewItem && !treeViewItem.children) {
- this.setState({
- activeItems: [treeViewItem]
- });
- }
- };
- }
-
- render() {
- const { activeItems } = this.state;
- const options = [
- {
- name: 'Application launcher',
- id: 'example6-AppLaunch',
- customBadgeContent: '2 applications',
- children: [
- {
- name: 'Application 1',
- id: 'example6-App1',
- customBadgeContent: '2 children',
- children: [
- { name: 'Settings', id: 'example6-App1Settings' },
- { name: 'Current', id: 'example6-App1Current' }
- ]
- },
- {
- name: 'Application 2',
- id: 'example6-App2',
- customBadgeContent: '2 children',
- children: [
- { name: 'Settings', id: 'example6-App2Settings' },
- {
- name: 'Loader',
- id: 'example6-App2Loader',
- customBadgeContent: '3 loading apps',
- children: [
- { name: 'Loading app 1', id: 'example6-LoadApp1' },
- { name: 'Loading app 2', id: 'example6-LoadApp2' },
- { name: 'Loading app 3', id: 'example6-LoadApp3' }
- ]
- }
- ]
- }
- ],
- defaultExpanded: true
- },
- {
- name: 'Cost management',
- id: 'example6-Cost',
- customBadgeContent: '1 applications',
- children: [
- {
- name: 'Application 3',
- id: 'example6-App3',
- customBadgeContent: '2 children',
- children: [
- { name: 'Settings', id: 'example6-App3Settings' },
- { name: 'Current', id: 'example6-App3Current' }
- ]
- }
- ]
- },
- {
- name: 'Sources',
- id: 'example6-Sources',
- customBadgeContent: '1 source',
- children: [
- {
- name: 'Application 4',
- id: 'example6-App4',
- customBadgeContent: '1 child',
- children: [{ name: 'Settings', id: 'example6-App4Settings' }]
- }
- ]
- },
- {
- name: 'Really really really long folder name that overflows the container it is in',
- id: 'example6-Long',
- customBadgeContent: '1 application',
- children: [{ name: 'Application 5', id: 'example6-App5' }]
- }
- ];
- return
;
- }
-}
```
### With action items
-```js
-import React from 'react';
-import {
- TreeView,
- Button,
- Dropdown,
- DropdownList,
- DropdownItem,
- MenuToggle,
- MenuToggleElement
-} from '@patternfly/react-core';
-import ClipboardIcon from '@patternfly/react-icons/dist/esm/icons/clipboard-icon';
-import HamburgerIcon from '@patternfly/react-icons/dist/esm/icons/hamburger-icon';
-import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
-
-class IconTreeView extends React.Component {
- constructor(props) {
- super(props);
+```ts file='./TreeViewWithActionItems.tsx'
- this.state = { activeItems: {}, isOpen: false };
-
- this.onSelect = (evt, treeViewItem) => {
- // Ignore folders for selection
- if (treeViewItem && !treeViewItem.children) {
- this.setState({
- activeItems: [treeViewItem]
- });
- }
- };
-
- this.onToggle = () => {
- this.setState({
- isOpen: !this.state.isOpen
- });
- };
-
- this.onAppLaunchSelect = () => {
- this.setState({
- isOpen: !this.state.isOpen
- });
- };
- }
-
- render() {
- const { activeItems, isOpen } = this.state;
-
- const options = [
- {
- name: 'Application launcher',
- id: 'example7-AppLaunch',
- action: (
-
this.setState({ isOpen })}
- toggle={(toggleRef) => (
-
-
-
- )}
- >
-
- Action
- ev.preventDefault()}
- >
- Link
-
- Disabled Action
-
- Disabled Link
-
-
-
- ),
- children: [
- {
- name: 'Application 1',
- id: 'example7-App1',
- action: (
-
-
-
- ),
- actionProps: {
- 'aria-label': 'Launch app 1'
- },
- children: [
- { name: 'Settings', id: 'example7-App1Settings' },
- { name: 'Current', id: 'example7-App1Current' }
- ]
- },
- {
- name: 'Application 2',
- id: 'example7-App2',
- action: (
-
-
-
- ),
- children: [
- { name: 'Settings', id: 'example7-App2Settings' },
- {
- name: 'Loader',
- id: 'example7-App2Loader',
- children: [
- { name: 'Loading App 1', id: 'example7-LoadApp1' },
- { name: 'Loading App 2', id: 'example7-LoadApp2' },
- { name: 'Loading App 3', id: 'example7-LoadApp3' }
- ]
- }
- ]
- }
- ],
- defaultExpanded: true
- },
- {
- name: 'Cost management',
- id: 'example7-Cost',
- children: [
- {
- name: 'Application 3',
- id: 'example7-App3',
- children: [
- { name: 'Settings', id: 'example7-App3Settings' },
- { name: 'Current', id: 'example7-App3Current' }
- ]
- }
- ]
- },
- {
- name: 'Sources',
- id: 'example7-Sources',
- children: [
- { name: 'Application 4', id: 'example7-App4', children: [{ name: 'Settings', id: 'example7-App4Settings' }] }
- ]
- },
- {
- name: 'Really really really long folder name that overflows the container it is in',
- id: 'example7-Long',
- children: [{ name: 'Application 5', id: 'example7-App5' }]
- }
- ];
- return
;
- }
-}
```
### Guides
-```ts
-import React from 'react';
-import { TreeView, TreeViewDataItem } from '@patternfly/react-core';
+```ts file='./TreeViewGuides.tsx'
-const GuidesTreeView: React.FunctionComponent = () => {
- const options: TreeViewDataItem[] = [
- {
- name: 'Application launcher',
- id: 'example8-AppLaunch',
- children: [
- {
- name: 'Application 1',
- id: 'example8-App1',
- children: [
- { name: 'Settings', id: 'example8-App1Settings' },
- { name: 'Current', id: 'example8-App1Current' }
- ]
- },
- {
- name: 'Application 2',
- id: 'example8-App2',
- children: [
- { name: 'Settings', id: 'example8-App2Settings' },
- {
- name: 'Loader',
- id: 'example8-App2Loader',
- children: [
- { name: 'Loading App 1', id: 'example8-LoadApp1' },
- { name: 'Loading App 2', id: 'example8-LoadApp2' },
- { name: 'Loading App 3', id: 'example8-LoadApp3' }
- ]
- }
- ]
- }
- ],
- defaultExpanded: true
- },
- {
- name: 'Cost management',
- id: 'example8-Cost',
- children: [
- {
- name: 'Application 3',
- id: 'example8-App3',
- children: [
- { name: 'Settings', id: 'example8-App3Settings' },
- { name: 'Current', id: 'example8-App3Current' }
- ]
- }
- ]
- },
- {
- name: 'Sources',
- id: 'example8-Sources',
- children: [
- { name: 'Application 4', id: 'example8-App4', children: [{ name: 'Settings', id: 'example8-App4Settings' }] }
- ]
- },
- {
- name: 'Really really really long folder name that overflows the container it is in',
- id: 'example8-Long',
- children: [{ name: 'Application 5', id: 'example8-App5' }]
- }
- ];
- return
;
-};
```
### Compact
-```ts
-import React from 'react';
-import { TreeView, TreeViewDataItem } from '@patternfly/react-core';
+```ts file='./TreeViewCompact.tsx'
-const CompactTreeView: React.FunctionComponent = () => {
- const options: TreeViewDataItem[] = [
- {
- name: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value and may reject unrecognized values.',
- title: 'apiVersion',
- id: 'example9-apiVersion'
- },
- {
- name: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated is CamelCase. More info:',
- title: 'kind',
- id: 'example9-kind'
- },
- {
- name: 'Standard metadata object',
- title: 'metadata',
- id: 'example9-metadata'
- },
- {
- name: 'Standard metadata object',
- title: 'spec',
- id: 'example9-spec',
- children: [
- {
- name: 'Minimum number of seconds for which a newly created pod should be ready without any of its container crashing, for it to be considered available. Default to 0 (pod will be considered available as soon as it is ready).',
- title: 'minReadySeconds',
- id: 'example9-minReadySeconds'
- },
- {
- name: 'Indicates that the deployment is paused',
- title: 'paused',
- id: 'example9-paused'
- },
- {
- name: 'The maximum time in seconds for a deployment to make progress before it is considered to be failed. The deployment controller will continue to process failed deployments and a condition with a ProgressDeadlineExceeded reason will be surfaced in the deployment status. Note that the progress will not de estimated during the time a deployment is paused. Defaults to 600s.',
- title: 'progressDeadlineSeconds',
- id: 'example9-progressDeadlineSeconds',
- children: [
- {
- name: 'The number of old ReplicaSets to retain to allow rollback. This is a pointer to distinguish between explicit zero and not specified. Defaults to 10.',
- title: 'revisionHistoryLimit',
- id: 'example9-revisionHistoryLimit',
- children: [
- {
- name: 'Map of {key.value} pairs. A single {key.value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In" and the values array contains only "value". The requirements are ANDed.',
- title: 'matchLabels',
- id: 'example9-matchLabels'
- }
- ]
- }
- ]
- }
- ]
- }
- ];
- return
;
-};
```
### Compact, no background
-```ts
-import React from 'react';
-import { TreeView, TreeViewDataItem } from '@patternfly/react-core';
+```ts file='./TreeViewCompactNoBackground.tsx'
-const CompactNoBackgroundTreeView: React.FunctionComponent = () => {
- const options: TreeViewDataItem[] = [
- {
- name: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value and may reject unrecognized values.',
- title: 'apiVersion',
- id: 'example10-apiVersion'
- },
- {
- name: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated is CamelCase. More info:',
- title: 'kind',
- id: 'example10-kind'
- },
- {
- name: 'Standard metadata object',
- title: 'metadata',
- id: 'example10-metadata'
- },
- {
- name: 'Standard metadata object',
- title: 'spec',
- id: 'example10-spec',
- children: [
- {
- name: 'Minimum number of seconds for which a newly created pod should be ready without any of its container crashing, for it to be considered available. Default to 0 (pod will be considered available as soon as it is ready).',
- title: 'minReadySeconds',
- id: 'example10-minReadySeconds'
- },
- {
- name: 'Indicates that the deployment is paused',
- title: 'paused',
- id: 'example10-paused'
- },
- {
- name: 'The maximum time in seconds for a deployment to make progress before it is considered to be failed. The deployment controller will continue to process failed deployments and a condition with a ProgressDeadlineExceeded reason will be surfaced in the deployment status. Note that the progress will not de estimated during the time a deployment is paused. Defaults to 600s.',
- title: 'progressDeadlineSeconds',
- id: 'example10-progressDeadlineSeconds',
- children: [
- {
- name: 'The number of old ReplicaSets to retain to allow rollback. This is a pointer to distinguish between explicit zero and not specified. Defaults to 10.',
- title: 'revisionHistoryLimit',
- id: 'example10-revisionHistoryLimit',
- children: [
- {
- name: 'Map of {key.value} pairs. A single {key.value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In" and the values array contains only "value". The requirements are ANDed.',
- title: 'matchLabels',
- id: 'example10-matchLabels'
- }
- ]
- }
- ]
- }
- ]
- }
- ];
- return
;
-};
```
### With memoization
Turning on memoization with the `useMemo` property helps prevent unnecessary re-renders for large data sets. With this flag active, `activeItems` must pass in an array of nodes along the selected item's path to update properly.
-```js
-import React from 'react';
-import { TreeView, Button } from '@patternfly/react-core';
-
-class MemoTreeView extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = { activeItems: {}, allExpanded: false };
-
- this.onSelect = (evt, treeViewItem) => {
- let filtered = [];
- this.options.forEach((item) => this.filterItems(item, treeViewItem.id, filtered));
- this.setState({
- activeItems: filtered
- });
- };
-
- this.onToggle = (evt) => {
- const { allExpanded } = this.state;
- this.setState({
- allExpanded: allExpanded !== undefined ? !allExpanded : true
- });
- };
-
- this.filterItems = (item, input, list) => {
- if (item.children) {
- let childContained = false;
- item.children.forEach((child) => {
- if (childContained) {
- this.filterItems(child, input, list);
- } else {
- childContained = this.filterItems(child, input, list);
- }
- });
- if (childContained) {
- list.push(item);
- }
- }
-
- if (item.id === input) {
- list.push(item);
- return true;
- } else {
- return false;
- }
- };
-
- this.options = [];
- for (let i = 1; i <= 20; i++) {
- const childNum = 5;
- let childOptions = [];
- for (let j = 1; j <= childNum; j++) {
- childOptions.push({ name: 'Child ' + j, id: `Option${i} - Child${j}` });
- }
- this.options.push({ name: 'Option ' + i, id: i, children: childOptions });
- }
- }
-
- render() {
- const { activeItems, allExpanded } = this.state;
- const tree = (
-
- );
+```ts file='./TreeViewWithMemoization.tsx'
- return (
-
-
- {allExpanded && 'Collapse all'}
- {!allExpanded && 'Expand all'}
-
- {tree}
-
- );
- }
-}
```
diff --git a/packages/react-core/src/components/TreeView/examples/TreeViewCompact.tsx b/packages/react-core/src/components/TreeView/examples/TreeViewCompact.tsx
new file mode 100644
index 00000000000..68fd651d60f
--- /dev/null
+++ b/packages/react-core/src/components/TreeView/examples/TreeViewCompact.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { TreeView, TreeViewDataItem } from '@patternfly/react-core';
+
+export const TreeViewCompact: React.FunctionComponent = () => {
+ const options: TreeViewDataItem[] = [
+ {
+ name: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value and may reject unrecognized values.',
+ title: 'apiVersion',
+ id: 'example9-apiVersion'
+ },
+ {
+ name: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated is CamelCase. More info:',
+ title: 'kind',
+ id: 'example9-kind'
+ },
+ {
+ name: 'Standard metadata object',
+ title: 'metadata',
+ id: 'example9-metadata'
+ },
+ {
+ name: 'Standard metadata object',
+ title: 'spec',
+ id: 'example9-spec',
+ children: [
+ {
+ name: 'Minimum number of seconds for which a newly created pod should be ready without any of its container crashing, for it to be considered available. Default to 0 (pod will be considered available as soon as it is ready).',
+ title: 'minReadySeconds',
+ id: 'example9-minReadySeconds'
+ },
+ {
+ name: 'Indicates that the deployment is paused',
+ title: 'paused',
+ id: 'example9-paused'
+ },
+ {
+ name: 'The maximum time in seconds for a deployment to make progress before it is considered to be failed. The deployment controller will continue to process failed deployments and a condition with a ProgressDeadlineExceeded reason will be surfaced in the deployment status. Note that the progress will not de estimated during the time a deployment is paused. Defaults to 600s.',
+ title: 'progressDeadlineSeconds',
+ id: 'example9-progressDeadlineSeconds',
+ children: [
+ {
+ name: 'The number of old ReplicaSets to retain to allow rollback. This is a pointer to distinguish between explicit zero and not specified. Defaults to 10.',
+ title: 'revisionHistoryLimit',
+ id: 'example9-revisionHistoryLimit',
+ children: [
+ {
+ name: 'Map of {key.value} pairs. A single {key.value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In" and the values array contains only "value". The requirements are ANDed.',
+ title: 'matchLabels',
+ id: 'example9-matchLabels'
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ];
+ return
;
+};
diff --git a/packages/react-core/src/components/TreeView/examples/TreeViewCompactNoBackground.tsx b/packages/react-core/src/components/TreeView/examples/TreeViewCompactNoBackground.tsx
new file mode 100644
index 00000000000..bc4030518c0
--- /dev/null
+++ b/packages/react-core/src/components/TreeView/examples/TreeViewCompactNoBackground.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { TreeView, TreeViewDataItem } from '@patternfly/react-core';
+
+export const TreeViewCompactNoBackground: React.FunctionComponent = () => {
+ const options: TreeViewDataItem[] = [
+ {
+ name: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value and may reject unrecognized values.',
+ title: 'apiVersion',
+ id: 'example10-apiVersion'
+ },
+ {
+ name: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated is CamelCase. More info:',
+ title: 'kind',
+ id: 'example10-kind'
+ },
+ {
+ name: 'Standard metadata object',
+ title: 'metadata',
+ id: 'example10-metadata'
+ },
+ {
+ name: 'Standard metadata object',
+ title: 'spec',
+ id: 'example10-spec',
+ children: [
+ {
+ name: 'Minimum number of seconds for which a newly created pod should be ready without any of its container crashing, for it to be considered available. Default to 0 (pod will be considered available as soon as it is ready).',
+ title: 'minReadySeconds',
+ id: 'example10-minReadySeconds'
+ },
+ {
+ name: 'Indicates that the deployment is paused',
+ title: 'paused',
+ id: 'example10-paused'
+ },
+ {
+ name: 'The maximum time in seconds for a deployment to make progress before it is considered to be failed. The deployment controller will continue to process failed deployments and a condition with a ProgressDeadlineExceeded reason will be surfaced in the deployment status. Note that the progress will not de estimated during the time a deployment is paused. Defaults to 600s.',
+ title: 'progressDeadlineSeconds',
+ id: 'example10-progressDeadlineSeconds',
+ children: [
+ {
+ name: 'The number of old ReplicaSets to retain to allow rollback. This is a pointer to distinguish between explicit zero and not specified. Defaults to 10.',
+ title: 'revisionHistoryLimit',
+ id: 'example10-revisionHistoryLimit',
+ children: [
+ {
+ name: 'Map of {key.value} pairs. A single {key.value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In" and the values array contains only "value". The requirements are ANDed.',
+ title: 'matchLabels',
+ id: 'example10-matchLabels'
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ];
+ return
;
+};
diff --git a/packages/react-core/src/components/TreeView/examples/TreeViewDefault.tsx b/packages/react-core/src/components/TreeView/examples/TreeViewDefault.tsx
new file mode 100644
index 00000000000..db14d756cf2
--- /dev/null
+++ b/packages/react-core/src/components/TreeView/examples/TreeViewDefault.tsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import { TreeView, Button, TreeViewDataItem } from '@patternfly/react-core';
+
+export const TreeViewDefault: React.FunctionComponent = () => {
+ const [activeItems, setActiveItems] = React.useState
();
+ const [allExpanded, setAllExpanded] = React.useState();
+
+ const onSelect = (_event: React.MouseEvent, treeViewItem: TreeViewDataItem) => {
+ // Ignore folders for selection
+ if (treeViewItem && !treeViewItem.children) {
+ setActiveItems([treeViewItem]);
+ }
+ };
+
+ const onToggle = (_event: React.MouseEvent) => {
+ setAllExpanded((prevAllExpanded) => !prevAllExpanded);
+ };
+
+ const options = [
+ {
+ name: 'Application launcher',
+ id: 'example1-AppLaunch',
+ children: [
+ {
+ name: 'Application 1',
+ id: 'example1-App1',
+ children: [
+ { name: 'Settings', id: 'example1-App1Settings' },
+ { name: 'Current', id: 'example1-App1Current' }
+ ]
+ },
+ {
+ name: 'Application 2',
+ id: 'example1-App2',
+ children: [
+ { name: 'Settings', id: 'example1-App2Settings' },
+ {
+ name: 'Loader',
+ id: 'example1-App2Loader',
+ children: [
+ { name: 'Loading App 1', id: 'example1-LoadApp1' },
+ { name: 'Loading App 2', id: 'example1-LoadApp2' },
+ { name: 'Loading App 3', id: 'example1-LoadApp3' }
+ ]
+ }
+ ]
+ }
+ ],
+ defaultExpanded: true
+ },
+ {
+ name: 'Cost management',
+ id: 'example1-Cost',
+ children: [
+ {
+ name: 'Application 3',
+ id: 'example1-App3',
+ children: [
+ { name: 'Settings', id: 'example1-App3Settings' },
+ { name: 'Current', id: 'example1-App3Current' }
+ ]
+ }
+ ]
+ },
+ {
+ name: 'Sources',
+ id: 'example1-Sources',
+ children: [
+ { name: 'Application 4', id: 'example1-App4', children: [{ name: 'Settings', id: 'example1-App4Settings' }] }
+ ]
+ },
+ {
+ name: 'Really really really long folder name that overflows the container it is in',
+ id: 'example1-Long',
+ children: [{ name: 'Application 5', id: 'example1-App5' }]
+ }
+ ];
+ return (
+
+
+ {allExpanded && 'Collapse all'}
+ {!allExpanded && 'Expand all'}
+
+
+
+ );
+};
diff --git a/packages/react-core/src/components/TreeView/examples/TreeViewGuides.tsx b/packages/react-core/src/components/TreeView/examples/TreeViewGuides.tsx
new file mode 100644
index 00000000000..4fa9cd2a353
--- /dev/null
+++ b/packages/react-core/src/components/TreeView/examples/TreeViewGuides.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { TreeView, TreeViewDataItem } from '@patternfly/react-core';
+
+export const GuidesTreeView: React.FunctionComponent = () => {
+ const options: TreeViewDataItem[] = [
+ {
+ name: 'Application launcher',
+ id: 'example8-AppLaunch',
+ children: [
+ {
+ name: 'Application 1',
+ id: 'example8-App1',
+ children: [
+ { name: 'Settings', id: 'example8-App1Settings' },
+ { name: 'Current', id: 'example8-App1Current' }
+ ]
+ },
+ {
+ name: 'Application 2',
+ id: 'example8-App2',
+ children: [
+ { name: 'Settings', id: 'example8-App2Settings' },
+ {
+ name: 'Loader',
+ id: 'example8-App2Loader',
+ children: [
+ { name: 'Loading App 1', id: 'example8-LoadApp1' },
+ { name: 'Loading App 2', id: 'example8-LoadApp2' },
+ { name: 'Loading App 3', id: 'example8-LoadApp3' }
+ ]
+ }
+ ]
+ }
+ ],
+ defaultExpanded: true
+ },
+ {
+ name: 'Cost management',
+ id: 'example8-Cost',
+ children: [
+ {
+ name: 'Application 3',
+ id: 'example8-App3',
+ children: [
+ { name: 'Settings', id: 'example8-App3Settings' },
+ { name: 'Current', id: 'example8-App3Current' }
+ ]
+ }
+ ]
+ },
+ {
+ name: 'Sources',
+ id: 'example8-Sources',
+ children: [
+ { name: 'Application 4', id: 'example8-App4', children: [{ name: 'Settings', id: 'example8-App4Settings' }] }
+ ]
+ },
+ {
+ name: 'Really really really long folder name that overflows the container it is in',
+ id: 'example8-Long',
+ children: [{ name: 'Application 5', id: 'example8-App5' }]
+ }
+ ];
+ return ;
+};
diff --git a/packages/react-core/src/components/TreeView/examples/TreeViewSelectionExpansion.tsx b/packages/react-core/src/components/TreeView/examples/TreeViewSelectionExpansion.tsx
new file mode 100644
index 00000000000..d1a2396d040
--- /dev/null
+++ b/packages/react-core/src/components/TreeView/examples/TreeViewSelectionExpansion.tsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import { TreeView, TreeViewDataItem } from '@patternfly/react-core';
+
+export const TreeViewSelectableNodes: React.FunctionComponent = () => {
+ const [activeItems, setActiveItems] = React.useState();
+
+ const onSelect = (_event: React.MouseEvent, treeViewItem: TreeViewDataItem) => {
+ setActiveItems([treeViewItem]);
+ };
+
+ const options = [
+ {
+ name: 'Application launcher',
+ id: 'SelNodesTreeView-AppLaunch',
+ children: [
+ {
+ name: 'Application 1',
+ id: 'SelNodesTreeView-App1',
+ children: [
+ { name: 'Settings', id: 'SelNodesTreeView-App1Settings' },
+ { name: 'Current', id: 'SelNodesTreeView-App1Current' }
+ ]
+ },
+ {
+ name: 'Application 2',
+ id: 'SelNodesTreeView-App2',
+ children: [
+ { name: 'Settings', id: 'SelNodesTreeView-App2Settings' },
+ {
+ name: 'Loader',
+ id: 'SelNodesTreeView-App2Loader',
+ children: [
+ { name: 'Loading App 1', id: 'SelNodesTreeView-LoadApp1' },
+ { name: 'Loading App 2', id: 'SelNodesTreeView-LoadApp2' },
+ { name: 'Loading App 3', id: 'SelNodesTreeView-LoadApp3' }
+ ]
+ }
+ ]
+ }
+ ],
+ defaultExpanded: true
+ },
+ {
+ name: 'Cost management',
+ id: 'SelNodesTreeView-Cost',
+ children: [
+ {
+ name: 'Application 3',
+ id: 'SelNodesTreeView-App3',
+ children: [
+ { name: 'Settings', id: 'SelNodesTreeView-App3Settings' },
+ { name: 'Current', id: 'SelNodesTreeView-App3Current' }
+ ]
+ }
+ ]
+ },
+ {
+ name: 'Sources',
+ id: 'SelNodesTreeView-Sources',
+ children: [
+ {
+ name: 'Application 4',
+ id: 'SelNodesTreeView-App4',
+ children: [{ name: 'Settings', id: 'SelNodesTreeView-App4Settings' }]
+ }
+ ]
+ },
+ {
+ name: 'Really really really long folder name that overflows the container it is in',
+ id: 'SelNodesTreeView-Long',
+ children: [{ name: 'Application 5', id: 'SelNodesTreeView-App5' }]
+ }
+ ];
+ return ;
+};
diff --git a/packages/react-core/src/components/TreeView/examples/TreeViewWithActionItems.tsx b/packages/react-core/src/components/TreeView/examples/TreeViewWithActionItems.tsx
new file mode 100644
index 00000000000..0fe77a8ad97
--- /dev/null
+++ b/packages/react-core/src/components/TreeView/examples/TreeViewWithActionItems.tsx
@@ -0,0 +1,140 @@
+import React from 'react';
+import {
+ TreeView,
+ Button,
+ Dropdown,
+ DropdownList,
+ DropdownItem,
+ MenuToggle,
+ TreeViewDataItem
+} from '@patternfly/react-core';
+import ClipboardIcon from '@patternfly/react-icons/dist/esm/icons/clipboard-icon';
+import HamburgerIcon from '@patternfly/react-icons/dist/esm/icons/hamburger-icon';
+import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
+
+export const TreeViewWithActionItems: React.FunctionComponent = () => {
+ const [activeItems, setActiveItems] = React.useState();
+ const [isOpen, setIsOpen] = React.useState();
+
+ const onSelect = (_event: React.MouseEvent, treeViewItem: TreeViewDataItem) => {
+ // Ignore folders for selection
+ if (treeViewItem && !treeViewItem.children) {
+ setActiveItems([treeViewItem]);
+ }
+ };
+
+ const onToggle = () => {
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ };
+
+ const onAppLaunchSelect = () => {
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ };
+
+ const options = [
+ {
+ name: 'Application launcher',
+ id: 'example7-AppLaunch',
+ action: (
+ setIsOpen(isOpen)}
+ toggle={(toggleRef) => (
+
+
+
+ )}
+ >
+
+ Action
+ ev.preventDefault()}
+ >
+ Link
+
+ Disabled Action
+
+ Disabled Link
+
+
+
+ ),
+ children: [
+ {
+ name: 'Application 1',
+ id: 'example7-App1',
+ action: (
+
+
+
+ ),
+ actionProps: {
+ 'aria-label': 'Launch app 1'
+ },
+ children: [
+ { name: 'Settings', id: 'example7-App1Settings' },
+ { name: 'Current', id: 'example7-App1Current' }
+ ]
+ },
+ {
+ name: 'Application 2',
+ id: 'example7-App2',
+ action: (
+
+
+
+ ),
+ children: [
+ { name: 'Settings', id: 'example7-App2Settings' },
+ {
+ name: 'Loader',
+ id: 'example7-App2Loader',
+ children: [
+ { name: 'Loading App 1', id: 'example7-LoadApp1' },
+ { name: 'Loading App 2', id: 'example7-LoadApp2' },
+ { name: 'Loading App 3', id: 'example7-LoadApp3' }
+ ]
+ }
+ ]
+ }
+ ],
+ defaultExpanded: true
+ },
+ {
+ name: 'Cost management',
+ id: 'example7-Cost',
+ children: [
+ {
+ name: 'Application 3',
+ id: 'example7-App3',
+ children: [
+ { name: 'Settings', id: 'example7-App3Settings' },
+ { name: 'Current', id: 'example7-App3Current' }
+ ]
+ }
+ ]
+ },
+ {
+ name: 'Sources',
+ id: 'example7-Sources',
+ children: [
+ { name: 'Application 4', id: 'example7-App4', children: [{ name: 'Settings', id: 'example7-App4Settings' }] }
+ ]
+ },
+ {
+ name: 'Really really really long folder name that overflows the container it is in',
+ id: 'example7-Long',
+ children: [{ name: 'Application 5', id: 'example7-App5' }]
+ }
+ ];
+ return ;
+};
diff --git a/packages/react-core/src/components/TreeView/examples/TreeViewWithBadges.tsx b/packages/react-core/src/components/TreeView/examples/TreeViewWithBadges.tsx
new file mode 100644
index 00000000000..9bc603bb9bd
--- /dev/null
+++ b/packages/react-core/src/components/TreeView/examples/TreeViewWithBadges.tsx
@@ -0,0 +1,79 @@
+import React from 'react';
+import { TreeView, TreeViewDataItem } from '@patternfly/react-core';
+
+export const TreeViewBadges: React.FunctionComponent = () => {
+ const [activeItems, setActiveItems] = React.useState();
+
+ const onSelect = (_event: React.MouseEvent, treeViewItem: TreeViewDataItem) => {
+ // Ignore folders for selection
+ if (treeViewItem && !treeViewItem.children) {
+ setActiveItems([treeViewItem]);
+ }
+ };
+
+ const options = [
+ {
+ name: 'Application launcher',
+ id: 'example5-AppLaunch',
+ children: [
+ {
+ name: 'Application 1',
+ id: 'example5-App1',
+ children: [
+ { name: 'Settings', id: 'example5-App1Settings' },
+ { name: 'Current', id: 'example5-App1Current' }
+ ]
+ },
+ {
+ name: 'Application 2',
+ id: 'example5-App2',
+ children: [
+ { name: 'Settings', id: 'example5-App2Settings' },
+ {
+ name: 'Loader',
+ id: 'example5-App2Loader',
+ children: [
+ { name: 'Loading App 1', id: 'example5-LoadApp1' },
+ { name: 'Loading App 2', id: 'example5-LoadApp2' },
+ { name: 'Loading App 3', id: 'example5-LoadApp3' }
+ ]
+ }
+ ]
+ }
+ ],
+ defaultExpanded: true
+ },
+ {
+ name: 'Cost management',
+ id: 'example5-Cost',
+ children: [
+ {
+ name: 'Application 3',
+ id: 'example5-App3',
+ children: [
+ { name: 'Settings', id: 'example5-App3Settings' },
+ { name: 'Current', id: 'example5-App3Current' }
+ ]
+ }
+ ]
+ },
+ {
+ name: 'Sources',
+ id: 'example5-Sources',
+ children: [
+ {
+ name: 'Application 4',
+ id: 'example5-App4',
+ children: [{ name: 'Settings', id: 'example5-App4Settings' }]
+ }
+ ]
+ },
+ {
+ name: 'Really really really long folder name that overflows the container it is in',
+ id: 'example5-Long',
+ children: [{ name: 'Application 5', id: 'example5-App5' }]
+ }
+ ];
+
+ return ;
+};
diff --git a/packages/react-core/src/components/TreeView/examples/TreeViewWithCheckboxes.tsx b/packages/react-core/src/components/TreeView/examples/TreeViewWithCheckboxes.tsx
new file mode 100644
index 00000000000..3e660959a7e
--- /dev/null
+++ b/packages/react-core/src/components/TreeView/examples/TreeViewWithCheckboxes.tsx
@@ -0,0 +1,198 @@
+import React from 'react';
+import { TreeView, TreeViewDataItem } from '@patternfly/react-core';
+
+export const TreeViewWithCheckboxes: React.FunctionComponent = () => {
+ const [checkedItems, setCheckedItems] = React.useState([]);
+
+ React.useEffect(() => {
+ // eslint-disable-next-line no-console
+ console.log('Checked items: ', checkedItems);
+ }, [checkedItems]);
+
+ const options = [
+ {
+ name: 'Application launcher',
+ id: 'example3-AppLaunch',
+ checkProps: { 'aria-label': 'app-launcher-check', checked: false },
+ children: [
+ {
+ name: 'Application 1',
+ id: 'example3-App1',
+ checkProps: { checked: false },
+ children: [
+ {
+ name: 'Settings',
+ id: 'example3-App1Settings',
+ checkProps: { checked: false }
+ },
+ {
+ name: 'Current',
+ id: 'example3-App1Current',
+ checkProps: { checked: false }
+ }
+ ]
+ },
+ {
+ name: 'Application 2',
+ id: 'example3-App2',
+ checkProps: { checked: false },
+ children: [
+ {
+ name: 'Settings',
+ id: 'example3-App2Settings',
+ checkProps: { checked: false }
+ },
+ {
+ name: 'Loader',
+ id: 'example3-App2Loader',
+ checkProps: { checked: false },
+ children: [
+ {
+ name: 'Loading App 1',
+ id: 'example3-LoadApp1',
+ checkProps: { checked: false }
+ },
+ {
+ name: 'Loading App 2',
+ id: 'example3-LoadApp2',
+ checkProps: { checked: false }
+ },
+ {
+ name: 'Loading App 3',
+ id: 'example3-LoadApp3',
+ checkProps: { checked: false }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ defaultExpanded: true
+ },
+ {
+ name: 'Cost management',
+ id: 'example3-Cost',
+ checkProps: { 'aria-label': 'cost-check', checked: false },
+ children: [
+ {
+ name: 'Application 3',
+ id: 'example3-App3',
+ checkProps: { 'aria-label': 'app-3-check', checked: false },
+ children: [
+ {
+ name: 'Settings',
+ id: 'example3-App3Settings',
+ checkProps: { 'aria-label': 'app-3-settings-check', checked: false }
+ },
+ {
+ name: 'Current',
+ id: 'example3-App3Current',
+ checkProps: { 'aria-label': 'app-3-current-check', checked: false }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ name: 'Sources',
+ id: 'example3-Sources',
+ checkProps: { 'aria-label': 'sources-check', checked: false },
+ children: [
+ {
+ name: 'Application 4',
+ id: 'example3-App4',
+ checkProps: { 'aria-label': 'app-4-check', checked: false },
+ children: [
+ {
+ name: 'Settings',
+ id: 'example3-App4Settings',
+ checkProps: { 'aria-label': 'app-4-settings-check', checked: false }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ name: 'Really really really long folder name that overflows the container it is in',
+ id: 'example3-Long',
+ checkProps: { 'aria-label': 'long-check', checked: false },
+ children: [
+ { name: 'Application 5', id: 'example3-App5', checkProps: { 'aria-label': 'app-5-check', checked: false } }
+ ]
+ }
+ ];
+
+ const onCheck = (event: React.ChangeEvent, treeViewItem: TreeViewDataItem) => {
+ const checked = (event.target as HTMLInputElement).checked;
+
+ const checkedItemTree = options
+ .map((opt) => Object.assign({}, opt))
+ .filter((item) => filterItems(item, treeViewItem));
+ const flatCheckedItems = flattenTree(checkedItemTree);
+
+ setCheckedItems((prevCheckedItems) =>
+ checked
+ ? prevCheckedItems.concat(flatCheckedItems.filter((item) => !checkedItems.some((i) => i.id === item.id)))
+ : prevCheckedItems.filter((item) => !flatCheckedItems.some((i) => i.id === item.id))
+ );
+ };
+
+ // Helper functions
+ const isChecked = (dataItem: TreeViewDataItem) => checkedItems.some((item) => item.id === dataItem.id);
+ const areAllDescendantsChecked = (dataItem: TreeViewDataItem) =>
+ dataItem.children ? dataItem.children.every((child) => areAllDescendantsChecked(child)) : isChecked(dataItem);
+ const areSomeDescendantsChecked = (dataItem: TreeViewDataItem) =>
+ dataItem.children ? dataItem.children.some((child) => areSomeDescendantsChecked(child)) : isChecked(dataItem);
+
+ const flattenTree = (tree: TreeViewDataItem[]) => {
+ let result: TreeViewDataItem[] = [];
+ tree.forEach((item) => {
+ result.push(item);
+ if (item.children) {
+ result = result.concat(flattenTree(item.children));
+ }
+ });
+ return result;
+ };
+
+ const mapTree = (item: TreeViewDataItem) => {
+ const hasCheck = areAllDescendantsChecked(item);
+ // Reset checked properties to be updated
+ if (item.checkProps) {
+ item.checkProps.checked = false;
+
+ if (hasCheck) {
+ item.checkProps.checked = true;
+ } else {
+ const hasPartialCheck = areSomeDescendantsChecked(item);
+ if (hasPartialCheck) {
+ item.checkProps.checked = null;
+ }
+ }
+
+ if (item.children) {
+ return {
+ ...item,
+ children: item.children.map((child) => mapTree(child))
+ };
+ }
+ }
+ return item;
+ };
+
+ const filterItems = (item: TreeViewDataItem, checkedItem: TreeViewDataItem) => {
+ if (item.id === checkedItem.id) {
+ return true;
+ }
+
+ if (item.children) {
+ return (
+ (item.children = item.children
+ .map((opt) => Object.assign({}, opt))
+ .filter((child) => filterItems(child, checkedItem))).length > 0
+ );
+ }
+ };
+ const mapped = options.map((item) => mapTree(item));
+ return ;
+};
diff --git a/packages/react-core/src/components/TreeView/examples/TreeViewWithCustomBadges.tsx b/packages/react-core/src/components/TreeView/examples/TreeViewWithCustomBadges.tsx
new file mode 100644
index 00000000000..c520dabaec4
--- /dev/null
+++ b/packages/react-core/src/components/TreeView/examples/TreeViewWithCustomBadges.tsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import { TreeView, TreeViewDataItem } from '@patternfly/react-core';
+
+export const TreeViewCustomBadges: React.FunctionComponent = () => {
+ const [activeItems, setActiveItems] = React.useState();
+
+ const onSelect = (_event: React.MouseEvent, treeViewItem: TreeViewDataItem) => {
+ // Ignore folders for selection
+ if (treeViewItem && !treeViewItem.children) {
+ setActiveItems([treeViewItem]);
+ }
+ };
+ const options = [
+ {
+ name: 'Application launcher',
+ id: 'example6-AppLaunch',
+ customBadgeContent: '2 applications',
+ children: [
+ {
+ name: 'Application 1',
+ id: 'example6-App1',
+ customBadgeContent: '2 children',
+ children: [
+ { name: 'Settings', id: 'example6-App1Settings' },
+ { name: 'Current', id: 'example6-App1Current' }
+ ]
+ },
+ {
+ name: 'Application 2',
+ id: 'example6-App2',
+ customBadgeContent: '2 children',
+ children: [
+ { name: 'Settings', id: 'example6-App2Settings' },
+ {
+ name: 'Loader',
+ id: 'example6-App2Loader',
+ customBadgeContent: '3 loading apps',
+ children: [
+ { name: 'Loading app 1', id: 'example6-LoadApp1' },
+ { name: 'Loading app 2', id: 'example6-LoadApp2' },
+ { name: 'Loading app 3', id: 'example6-LoadApp3' }
+ ]
+ }
+ ]
+ }
+ ],
+ defaultExpanded: true
+ },
+ {
+ name: 'Cost management',
+ id: 'example6-Cost',
+ customBadgeContent: '1 applications',
+ children: [
+ {
+ name: 'Application 3',
+ id: 'example6-App3',
+ customBadgeContent: '2 children',
+ children: [
+ { name: 'Settings', id: 'example6-App3Settings' },
+ { name: 'Current', id: 'example6-App3Current' }
+ ]
+ }
+ ]
+ },
+ {
+ name: 'Sources',
+ id: 'example6-Sources',
+ customBadgeContent: '1 source',
+ children: [
+ {
+ name: 'Application 4',
+ id: 'example6-App4',
+ customBadgeContent: '1 child',
+ children: [{ name: 'Settings', id: 'example6-App4Settings' }]
+ }
+ ]
+ },
+ {
+ name: 'Really really really long folder name that overflows the container it is in',
+ id: 'example6-Long',
+ customBadgeContent: '1 application',
+ children: [{ name: 'Application 5', id: 'example6-App5' }]
+ }
+ ];
+ return ;
+};
diff --git a/packages/react-core/src/components/TreeView/examples/TreeViewWithIcons.tsx b/packages/react-core/src/components/TreeView/examples/TreeViewWithIcons.tsx
new file mode 100644
index 00000000000..4e3027fa388
--- /dev/null
+++ b/packages/react-core/src/components/TreeView/examples/TreeViewWithIcons.tsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import { TreeView, TreeViewDataItem } from '@patternfly/react-core';
+import FolderIcon from '@patternfly/react-icons/dist/esm/icons/folder-icon';
+import FolderOpenIcon from '@patternfly/react-icons/dist/esm/icons/folder-open-icon';
+
+export const TreeViewWithIcons: React.FunctionComponent = () => {
+ const [activeItems, setActiveItems] = React.useState();
+
+ const onSelect = (_event: React.MouseEvent, treeViewItem: TreeViewDataItem) => {
+ // Ignore folders for selection
+ if (treeViewItem && !treeViewItem.children) {
+ setActiveItems([treeViewItem]);
+ }
+ };
+ const options = [
+ {
+ name: 'Application launcher',
+ id: 'example4-AppLaunch',
+ children: [
+ {
+ name: 'Application 1',
+ id: 'example4-App1',
+ children: [
+ { name: 'Settings', id: 'example4-App1Settings' },
+ { name: 'Current', id: 'example4-App1Current' }
+ ]
+ },
+ {
+ name: 'Application 2',
+ id: 'example4-App2',
+ children: [
+ { name: 'Settings', id: 'example4-App2Settings' },
+ {
+ name: 'Loader',
+ id: 'example4-App2Loader',
+ children: [
+ { name: 'Loading App 1', id: 'example4-LoadApp1' },
+ { name: 'Loading App 2', id: 'example4-LoadApp2' },
+ { name: 'Loading App 3', id: 'example4-LoadApp3' }
+ ]
+ }
+ ]
+ }
+ ],
+ defaultExpanded: true
+ },
+ {
+ name: 'Cost management',
+ id: 'example4-Cost',
+ children: [
+ {
+ name: 'Application 3',
+ id: 'example4-App3',
+ children: [
+ { name: 'Settings', id: 'example4-App3Settings' },
+ { name: 'Current', id: 'example4-App3Current' }
+ ]
+ }
+ ]
+ },
+ {
+ name: 'Sources',
+ id: 'example4-Sources',
+ children: [
+ { name: 'Application 4', id: 'example4-App4', children: [{ name: 'Settings', id: 'example4-App4Settings' }] }
+ ]
+ },
+ {
+ name: 'Really really really long folder name that overflows the container it is in',
+ id: 'example4-Long',
+ children: [{ name: 'Application 5', id: 'example4-App5' }]
+ }
+ ];
+ return (
+ }
+ expandedIcon={ }
+ />
+ );
+};
diff --git a/packages/react-core/src/components/TreeView/examples/TreeViewWithMemoization.tsx b/packages/react-core/src/components/TreeView/examples/TreeViewWithMemoization.tsx
new file mode 100644
index 00000000000..1006f4b3ad2
--- /dev/null
+++ b/packages/react-core/src/components/TreeView/examples/TreeViewWithMemoization.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { TreeView, Button, TreeViewDataItem } from '@patternfly/react-core';
+
+export const TreeViewWithMemoization: React.FunctionComponent = () => {
+ const [activeItems, setActiveItems] = React.useState();
+ const [allExpanded, setAllExpanded] = React.useState(false);
+
+ const onSelect = (_event: React.MouseEvent, treeViewItem: TreeViewDataItem) => {
+ const filtered: TreeViewDataItem[] = [];
+ options.forEach((item) => filterItems(item, treeViewItem.id, filtered));
+ if (treeViewItem && !treeViewItem.children) {
+ setActiveItems(filtered);
+ }
+ };
+
+ const onToggle = (_event: React.MouseEvent) => {
+ setAllExpanded((prevAllExpanded) => !prevAllExpanded);
+ };
+
+ const filterItems = (item: TreeViewDataItem, input: string | undefined, list: TreeViewDataItem[]) => {
+ if (item.children) {
+ let childContained = false;
+ item.children.forEach((child) => {
+ if (childContained) {
+ filterItems(child, input, list);
+ } else {
+ childContained = filterItems(child, input, list);
+ }
+ });
+ if (childContained) {
+ list.push(item);
+ }
+ }
+
+ if (item.id === input) {
+ list.push(item);
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ const options: TreeViewDataItem[] = [];
+ for (let i = 1; i <= 20; i++) {
+ const childNum = 5;
+ const childOptions: TreeViewDataItem[] = [];
+ for (let j = 1; j <= childNum; j++) {
+ childOptions.push({ name: 'Child ' + j, id: `Option${i} - Child${j}` });
+ }
+ options.push({ name: 'Option ' + i, id: i.toString(), children: childOptions });
+ }
+ const tree = (
+
+ );
+
+ return (
+
+
+ {allExpanded && 'Collapse all'}
+ {!allExpanded && 'Expand all'}
+
+ {tree}
+
+ );
+};
diff --git a/packages/react-core/src/components/TreeView/examples/TreeViewWithSearch.tsx b/packages/react-core/src/components/TreeView/examples/TreeViewWithSearch.tsx
new file mode 100644
index 00000000000..ea1c3dd58cf
--- /dev/null
+++ b/packages/react-core/src/components/TreeView/examples/TreeViewWithSearch.tsx
@@ -0,0 +1,128 @@
+import React from 'react';
+import {
+ Toolbar,
+ ToolbarContent,
+ ToolbarItem,
+ TreeView,
+ TreeViewDataItem,
+ TreeViewSearch
+} from '@patternfly/react-core';
+export const TreeViewWithSearch: React.FunctionComponent = () => {
+ const options = [
+ {
+ name: 'Application launcher',
+ id: 'example2-AppLaunch',
+ children: [
+ {
+ name: 'Application 1',
+ id: 'example2-App1',
+ children: [
+ { name: 'Settings', id: 'example2-App1Settings' },
+ { name: 'Current', id: 'example2-App1Current' }
+ ]
+ },
+ {
+ name: 'Application 2',
+ id: 'example2-App2',
+ children: [
+ { name: 'Settings', id: 'example2-App2Settings' },
+ {
+ name: 'Loader',
+ id: 'example2-App2Loader',
+ children: [
+ { name: 'Loading App 1', id: 'example2-LoadApp1' },
+ { name: 'Loading App 2', id: 'example2-LoadApp2' },
+ { name: 'Loading App 3', id: 'example2-LoadApp3' }
+ ]
+ }
+ ]
+ }
+ ],
+ defaultExpanded: true
+ },
+ {
+ name: 'Cost management',
+ id: 'example2-Cost',
+ children: [
+ {
+ name: 'Application 3',
+ id: 'example2-App3',
+ children: [
+ { name: 'Settings', id: 'example2-App3Settings' },
+ { name: 'Current', id: 'example2-App3Current' }
+ ]
+ }
+ ]
+ },
+ {
+ name: 'Sources',
+ id: 'example2-Sources',
+ children: [
+ {
+ name: 'Application 4',
+ id: 'example2-App4',
+ children: [{ name: 'Settingexample2-s', id: 'example2-App4Settings' }]
+ }
+ ]
+ },
+ {
+ name: 'Really really really long folder name that overflows the container it is in',
+ id: 'example2-Long',
+ children: [{ name: 'Application 5', id: 'example2-App5' }]
+ }
+ ];
+
+ const [activeItems, setActiveItems] = React.useState();
+ const [filteredItems, setFilteredItems] = React.useState(options);
+ const [isFiltered, setIsFiltered] = React.useState(false);
+
+ const onSelect = (_event: React.MouseEvent, treeViewItem: TreeViewDataItem) => {
+ // Ignore folders for selection
+ if (treeViewItem && !treeViewItem.children) {
+ setActiveItems([treeViewItem]);
+ }
+ };
+
+ const onSearch = (event: React.ChangeEvent) => {
+ const input = event.target.value;
+ if (input === '') {
+ setFilteredItems(options);
+ setIsFiltered(false);
+ } else {
+ const filtered = options.map((opt) => Object.assign({}, opt)).filter((item) => filterItems(item, input));
+ setFilteredItems(filtered);
+ setIsFiltered(true);
+ }
+ };
+ const filterItems = (item, input) => {
+ if (item.name.toLowerCase().includes(input.toLowerCase())) {
+ return true;
+ }
+ if (item.children) {
+ return (
+ (item.children = item.children
+ .map((opt) => Object.assign({}, opt))
+ .filter((child) => filterItems(child, input))).length > 0
+ );
+ }
+ };
+ const toolbar = (
+
+
+
+
+
+
+
+ );
+
+ return (
+
+ );
+};
diff --git a/packages/react-core/src/demos/Toolbar.md b/packages/react-core/src/demos/Toolbar.md
index 9ad7feeaf9f..6e9e67ae874 100644
--- a/packages/react-core/src/demos/Toolbar.md
+++ b/packages/react-core/src/demos/Toolbar.md
@@ -236,7 +236,7 @@ class ConsoleLogViewerToolbar extends React.Component {
});
};
- this.onPageResize = ({ windowSize }) => {
+ this.onPageResize = (_event, { windowSize }) => {
if (windowSize >= 1450) {
this.setState({
mobileView: false
diff --git a/packages/react-core/src/demos/examples/DashboardWrapper.js b/packages/react-core/src/demos/examples/DashboardWrapper.js
index 1f48967ffa2..817d719ef3c 100644
--- a/packages/react-core/src/demos/examples/DashboardWrapper.js
+++ b/packages/react-core/src/demos/examples/DashboardWrapper.js
@@ -114,7 +114,9 @@ export default class DashboardWrapper extends React.Component {
mainContainerId={mainContainerId ? mainContainerId : 'main-content-page-layout-default-nav'}
notificationDrawer={notificationDrawer}
isNotificationDrawerExpanded={isNotificationDrawerExpanded}
- {...(typeof onPageResize === 'function' && { onPageResize: (event) => onPageResize(event) })}
+ {...(typeof onPageResize === 'function' && {
+ onPageResize: (event, resizeObject) => onPageResize(event, resizeObject)
+ })}
{...pageProps}
>
{hasPageTemplateTitle && PageTemplateTitle}
diff --git a/packages/react-core/src/deprecated/components/Select/SelectToggle.tsx b/packages/react-core/src/deprecated/components/Select/SelectToggle.tsx
index 2fc07511e15..1a1267cbd99 100644
--- a/packages/react-core/src/deprecated/components/Select/SelectToggle.tsx
+++ b/packages/react-core/src/deprecated/components/Select/SelectToggle.tsx
@@ -104,7 +104,7 @@ class SelectToggleBase extends React.Component {
}
componentWillUnmount() {
- document.removeEventListener('click', this.onDocClick);
+ document.removeEventListener('click', this.onDocClick, { capture: true });
document.removeEventListener('touchstart', this.onDocClick);
document.removeEventListener('keydown', this.handleGlobalKeys);
}
@@ -124,17 +124,8 @@ class SelectToggleBase extends React.Component {
};
handleGlobalKeys = (event: KeyboardEvent) => {
- const {
- parentRef,
- menuRef,
- hasFooter,
- footerRef,
- isOpen,
- variant,
- onToggle,
- onClose,
- moveFocusToLastMenuItem
- } = this.props;
+ const { parentRef, menuRef, hasFooter, footerRef, isOpen, variant, onToggle, onClose, moveFocusToLastMenuItem } =
+ this.props;
const escFromToggle = parentRef && parentRef.current && parentRef.current.contains(event.target as Node);
const escFromWithinMenu =
menuRef && menuRef.current && menuRef.current.contains && menuRef.current.contains(event.target as Node);
@@ -307,7 +298,7 @@ class SelectToggleBase extends React.Component {
aria-label={ariaLabel}
onBlur={onBlur}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- onClick={event => {
+ onClick={(event) => {
onToggle(event, !isOpen);
if (isOpen) {
onClose();
@@ -334,7 +325,7 @@ class SelectToggleBase extends React.Component {
)}
onBlur={onBlur}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- onClick={event => {
+ onClick={(event) => {
if (!isDisabled) {
onToggle(event, !isOpen);
if (isOpen) {
@@ -350,7 +341,7 @@ class SelectToggleBase extends React.Component {
type={type}
className={css(buttonStyles.button, styles.selectToggleButton, styles.modifiers.plain)}
aria-label={ariaLabel}
- onClick={event => {
+ onClick={(event) => {
onToggle(event, !isOpen);
if (isOpen) {
onClose();
diff --git a/packages/react-core/src/helpers/Popper/__mocks__/Popper.tsx b/packages/react-core/src/helpers/Popper/__mocks__/Popper.tsx
index 76befc60fdf..7449f0c2660 100644
--- a/packages/react-core/src/helpers/Popper/__mocks__/Popper.tsx
+++ b/packages/react-core/src/helpers/Popper/__mocks__/Popper.tsx
@@ -1,11 +1,30 @@
import React from 'react';
import { PopperProps } from '../Popper';
-export const Popper = ({ popper, zIndex, isVisible, trigger }: PopperProps) => (
- <>
- {popper}
+export const Popper = ({
+ popper,
+ zIndex,
+ placement,
+ trigger,
+ enableFlip,
+ appendTo,
+ distance,
+ flipBehavior,
+ isVisible,
+ minWidth
+}: PopperProps) => (
+
+
{isVisible && popper}
{`zIndex: ${zIndex}`}
-
{`isOpen: ${isVisible}`}
-
{trigger}
- >
+
{`isVisible: ${isVisible}`}
+
{`enableFlip: ${enableFlip}`}
+
{`placement: ${placement}`}
+
{`appendTo: ${appendTo}`}
+
{`distance: ${distance}`}
+
{`flipBehavior: ${flipBehavior}`}
+
{`minWidth: ${minWidth}`}
+ {trigger}
+
);
+
+export const getOpacityTransition = () => {};
diff --git a/packages/react-integration/cypress/integration/card.spec.ts b/packages/react-integration/cypress/integration/card.spec.ts
index b2fc5b76d8c..3d7701566c7 100644
--- a/packages/react-integration/cypress/integration/card.spec.ts
+++ b/packages/react-integration/cypress/integration/card.spec.ts
@@ -3,8 +3,18 @@ describe('Card Demo Test', () => {
cy.visit('http://localhost:3000/card-demo-nav-link');
});
- it('Verify that selectable card can be selected and unselected with keyboard input', () => {
- cy.get('#selectableCard').focus();
+ it('Verify card with actions', () => {
+ cy.get('#cardWithActions .pf-v5-c-menu-toggle').then(($menuToggle) => {
+ cy.wrap($menuToggle).should('not.have.class', 'pf-m-expanded');
+ cy.wrap($menuToggle).click();
+ cy.wrap($menuToggle).should('have.class', 'pf-m-expanded');
+ });
+ cy.get('#cardWithActions .pf-v5-c-menu .pf-v5-c-menu__item').first().click();
+ cy.get('#cardWithActions .pf-v5-c-menu-toggle').should('not.have.class', 'pf-m-expanded');
+ });
+
+ it('Verify that deprecated selectable card can be selected and unselected with keyboard input', () => {
+ cy.get('#selectableCardDeprecated').focus();
cy.focused().should('have.class', 'pf-m-selectable');
cy.focused().should('not.have.class', 'pf-m-selected');
cy.focused().type('{enter}');
@@ -29,4 +39,66 @@ describe('Card Demo Test', () => {
cy.get('.pf-v5-c-card__header-toggle .pf-v5-c-button').click();
cy.get('#expand-card').should('have.class', 'pf-m-expanded');
});
+
+ it('Verify new selectable card can be selected', () => {
+ cy.get('#selectable-card-example-1 #selectable-card-input-1').should('not.be.checked');
+ cy.get('#selectable-card-example-2 #selectable-card-input-2').should('not.be.checked');
+ cy.get('#selectable-card-example-1').then(($card) => {
+ cy.wrap($card).click();
+ cy.wrap($card).get('#selectable-card-input-1').should('be.checked');
+ cy.get('#selectable-card-example-2 #selectable-card-input-2').should('not.be.checked');
+ });
+ cy.get('#selectable-card-example-2').then(($card) => {
+ cy.wrap($card).click();
+ cy.wrap($card).get('#selectable-card-input-2').should('be.checked');
+ cy.get('#selectable-card-example-1 #selectable-card-input-1').should('be.checked');
+ });
+ });
+
+ it('Verify new single selectable card can be selected', () => {
+ cy.get('#single-selectable-card-example-1 #single-selectable-card-input-1').should('not.be.checked');
+ cy.get('#single-selectable-card-example-2 #single-selectable-card-input-2').should('not.be.checked');
+ cy.get('#single-selectable-card-example-1').then(($card) => {
+ cy.wrap($card).click();
+ cy.wrap($card).get('#single-selectable-card-input-1').should('be.checked');
+ cy.get('#single-selectable-card-example-2 #single-selectable-card-input-2').should('not.be.checked');
+ });
+ cy.get('#single-selectable-card-example-2').then(($card) => {
+ cy.wrap($card).click();
+ cy.wrap($card).get('#single-selectable-card-input-2').should('be.checked');
+ cy.get('#single-selectable-card-example-1 #single-selectable-card-input-1').should('not.be.checked');
+ });
+ });
+
+ it('Verify clickable only card action is triggered', () => {
+ cy.get('#clickable-card-drawer').should('not.have.class', 'pf-m-expanded');
+ cy.get('#clickable-card-example-1 #clickable-card-input-1').should('not.be.checked');
+ cy.get('#clickable-card-example-1').click();
+ cy.get('#clickable-card-drawer').should('have.class', 'pf-m-expanded');
+ cy.get('#clickable-card-example-1 #clickable-card-input-1').should('be.checked');
+ });
+
+ it('Verify clickable only card link is navigated to', () => {
+ cy.location('pathname').should('eq', '/card-demo-nav-link');
+ cy.get('#clickable-card-example-2').click();
+ cy.location('pathname').should('eq', '/button-demo-nav-link');
+ cy.go('back');
+ });
+
+ it('Verify clickable and selectable card', () => {
+ cy.get('#clickable-selectable-card-drawer').should('not.have.class', 'pf-m-expanded');
+ cy.get('#clickable-selectable-card-example-1 #clickable-selectable-card-input-1').should('not.be.checked');
+ // Clicking outside clickable areas should not change input or trigger action
+ cy.get('#clickable-selectable-card-example-1').click();
+ cy.get('#clickable-selectable-card-drawer').should('not.have.class', 'pf-m-expanded');
+ cy.get('#clickable-selectable-card-example-1 #clickable-selectable-card-input-1').should('not.be.checked');
+
+ // Ciicking input should not trigger action
+ cy.get('#clickable-selectable-card-example-1 #clickable-selectable-card-input-1').click();
+ cy.get('#clickable-selectable-card-example-1 #clickable-selectable-card-input-1').should('be.checked');
+ cy.get('#clickable-selectable-card-drawer').should('not.have.class', 'pf-m-expanded');
+
+ cy.get('#clickable-selectable-card-example-1 .pf-v5-c-button').click();
+ cy.get('#clickable-selectable-card-drawer').should('have.class', 'pf-m-expanded');
+ });
});
diff --git a/packages/react-integration/cypress/integration/tooltip.spec.ts b/packages/react-integration/cypress/integration/tooltip.spec.ts
index c483d0adbcb..cf2a8a7c289 100644
--- a/packages/react-integration/cypress/integration/tooltip.spec.ts
+++ b/packages/react-integration/cypress/integration/tooltip.spec.ts
@@ -3,13 +3,87 @@ describe('Tooltip Demo Test', () => {
cy.visit('http://localhost:3000/tooltip-demo-nav-link');
});
- it('Display Tooltip', () => {
- ['tooltipTarget', 'tooltip-selector', 'tooltip-ref'].forEach((id) => {
- cy.get(`[id="${id}"]`).then((tooltipLink: JQuery) => {
- cy.get('.pf-v5-c-tooltip').should('not.exist');
- cy.wrap(tooltipLink).trigger('mouseenter').get('.pf-v5-c-tooltip').should('exist');
+ const defaultEntryDelay = 300;
+ const defaultExitDelay = 300;
+
+ it('Renders Tooltip on hover by default', () => {
+ ['tooltip-children', 'tooltip-selector', 'tooltip-ref'].forEach((id) => {
+ cy.get(`[id="${id}-trigger"]`).then((tooltipTrigger: JQuery) => {
+ cy.get(`#${id}-content.pf-v5-c-tooltip`).should('not.exist');
+ cy.wrap(tooltipTrigger).trigger('mouseenter');
+ cy.wait(defaultEntryDelay);
+ cy.get(`#${id}-content.pf-v5-c-tooltip`).should('exist');
+ cy.wrap(tooltipTrigger).trigger('mouseleave');
+ cy.wait(defaultExitDelay);
+ cy.get(`#${id}-content.pf-v5-c-tooltip`).should('not.exist');
+ });
+ });
+ });
+
+ it('Renders Tooltip on focus by default', () => {
+ ['tooltip-children', 'tooltip-selector', 'tooltip-ref'].forEach((id) => {
+ cy.get(`[id="${id}-trigger"]`).then((tooltipTrigger: JQuery) => {
+ cy.get(`#${id}-content.pf-v5-c-tooltip`).should('not.exist');
+ cy.wrap(tooltipTrigger).trigger('focus');
+ cy.wait(defaultEntryDelay);
+ cy.get(`#${id}-content.pf-v5-c-tooltip`).should('exist');
+ cy.wrap(tooltipTrigger).trigger('blur');
+ cy.wait(defaultExitDelay);
+ cy.get(`#${id}-content.pf-v5-c-tooltip`).should('not.exist');
+ });
+ });
+ });
+
+ it('Remains rendered when Tooltip is hovered', () => {
+ ['tooltip-children', 'tooltip-selector', 'tooltip-ref'].forEach((id) => {
+ cy.get(`[id="${id}-trigger"]`).then((tooltipTrigger: JQuery) => {
+ cy.get(`#${id}-content.pf-v5-c-tooltip`).should('not.exist');
+ cy.wrap(tooltipTrigger).trigger('mouseenter');
+ cy.wait(defaultEntryDelay);
+ cy.get(`#${id}-content.pf-v5-c-tooltip`).should('exist');
});
- cy.get(`[id="${id}"]`).trigger('mouseleave');
+ cy.get(`#${id}-content.pf-v5-c-tooltip`).trigger('mouseenter');
+ cy.wait(defaultExitDelay + 100);
+ cy.get(`#${id}-content.pf-v5-c-tooltip`).should('exist');
+ cy.get(`#${id}-content.pf-v5-c-tooltip`).trigger('mouseleave').should('not.exist');
+ });
+ });
+
+ it('Renders tooltip on click', () => {
+ cy.get('#tooltip-click-trigger').trigger('mouseenter');
+ cy.get('#tooltip-click-content.pf-v5-c-tooltip').should('not.exist');
+ cy.get('#tooltip-click-trigger').trigger('focus');
+ cy.get('#tooltip-click-content.pf-v5-c-tooltip').should('not.exist');
+ cy.get('#tooltip-click-trigger').click();
+ cy.wait(defaultEntryDelay);
+ cy.get('#tooltip-click-content.pf-v5-c-tooltip').should('exist');
+ cy.get('#tooltip-click-content.pf-v5-c-tooltip').click();
+ cy.wait(defaultExitDelay);
+ cy.get('#tooltip-click-content.pf-v5-c-tooltip').should('not.exist');
+ });
+
+ it('Renders with passed in entryDelay and exitDelay', () => {
+ cy.get('#tooltip-delay-trigger').trigger('mouseenter');
+ cy.wait(defaultEntryDelay);
+ cy.get('#tooltip-delay-content.pf-v5-c-tooltip').should('not.exist');
+ cy.wait(200);
+ cy.get('#tooltip-delay-content.pf-v5-c-tooltip').should('exist');
+ cy.get('#tooltip-delay-trigger').trigger('mouseleave');
+ cy.wait(defaultExitDelay);
+ cy.get('#tooltip-delay-content.pf-v5-c-tooltip').should('exist');
+ cy.wait(200);
+ cy.get('#tooltip-delay-content.pf-v5-c-tooltip').should('not.exist');
+ });
+
+ it('Renders with passed in animationDuration styling', () => {
+ cy.get('#tooltip-animationDuration-trigger').trigger('mouseenter');
+ cy.wait(defaultEntryDelay);
+ cy.get('#tooltip-animationDuration-content.pf-v5-c-tooltip').then((tooltipContent) => {
+ expect(tooltipContent)
+ .to.have.attr('style')
+ .match(/transition: opacity 500ms/);
});
+ cy.get('#tooltip-animationDuration-trigger').trigger('mouseleave');
+ cy.wait(defaultExitDelay);
});
});
diff --git a/packages/react-integration/demo-app-ts/src/components/demos/CardDemo/CardDemo.tsx b/packages/react-integration/demo-app-ts/src/components/demos/CardDemo/CardDemo.tsx
index ef3898d3f24..962f50e2f88 100644
--- a/packages/react-integration/demo-app-ts/src/components/demos/CardDemo/CardDemo.tsx
+++ b/packages/react-integration/demo-app-ts/src/components/demos/CardDemo/CardDemo.tsx
@@ -1,13 +1,17 @@
import React from 'react';
import {
+ Brand,
+ Button,
Card,
CardTitle,
CardHeader,
CardBody,
CardFooter,
CardExpandableContent,
- Checkbox,
- Brand,
+ Drawer,
+ DrawerContent,
+ DrawerContentBody,
+ DrawerPanelContent,
Dropdown,
DropdownItem,
DropdownList,
@@ -20,7 +24,12 @@ interface CardDemoState {
selected: string;
isExpanded: boolean;
isOpen: boolean;
- check1: boolean;
+ selectableChecked1: boolean;
+ selectableChecked2: boolean;
+ drawerIsExpanded: boolean;
+ selectableClickableChecked: boolean;
+ selectableClickableSelected: boolean;
+ selectaleClickableDrawerIsExpanded: boolean;
}
export class CardDemo extends React.Component {
@@ -30,7 +39,12 @@ export class CardDemo extends React.Component {
selected: null,
isExpanded: false,
isOpen: false,
- check1: false
+ selectableChecked1: false,
+ selectableChecked2: false,
+ drawerIsExpanded: false,
+ selectableClickableChecked: false,
+ selectableClickableSelected: false,
+ selectaleClickableDrawerIsExpanded: false
};
onKeyDown = (event: any) => {
@@ -67,14 +81,39 @@ export class CardDemo extends React.Component {
});
};
- onClick = (event: any, _checked: boolean) => {
- const target = event.target;
- const value = target.type === 'checkbox' ? target.checked : target.value;
- const name = target.name;
- this.setState({ [name]: value });
+ onSelectableChange = (event: React.FormEvent, checked: boolean) => {
+ const name = event.currentTarget.name;
+
+ switch (name) {
+ case 'selectable-card-input-1':
+ this.setState({ selectableChecked1: checked });
+ break;
+ case 'selectable-card-input-2':
+ this.setState({ selectableChecked2: checked });
+ break;
+ }
+ };
+
+ onSelectableClickableChange = (_event: React.FormEvent, checked: boolean) => {
+ this.setState({ selectableClickableChecked: checked });
+ };
+
+ onSelectableClickableClick = () => {
+ this.setState({
+ selectableClickableSelected: !this.state.selectableClickableSelected,
+ selectaleClickableDrawerIsExpanded: !this.state.selectaleClickableDrawerIsExpanded
+ });
};
render() {
+ const {
+ selectableChecked1,
+ selectableChecked2,
+ drawerIsExpanded,
+ selectableClickableChecked,
+ selectableClickableSelected,
+ selectaleClickableDrawerIsExpanded
+ } = this.state;
const dropdownItems = [
Link ,
Action ,
@@ -103,74 +142,24 @@ export class CardDemo extends React.Component {
>
{dropdownItems}
-
>
);
return (
-
- Header
- Body
- Footer
-
-
-
- Header
- Body
- Footer
-
-
-
+
+
+
+
Header
Body
Footer
-
-
- Header
-
- Body
- Footer
-
-
-
- Header
- Body
- Footer
-
-
- Header
- Body
- Footer
-
-
- Header
- Body
- Footer
-
-
- Header
- Body
- Footer
-
-
- Header
- Body
- Footer
-
-
@@ -202,14 +191,143 @@ export class CardDemo extends React.Component {
)}
-
-
-
+
+
+ First selectable card
- Header
- Body
- Footer
+ This card is selectable.
+
+
+
+ Second selectable card
+
+ This card is selectable.
+
+
+
+
+ First single selectable card
+
+ This card is single selectable.
+
+
+ Second single selectable card
+
+ This card is single selectable.
+
+
+
+
+
+ Clickable card drawer panel
+
+ }
+ >
+
+
+ {
+ this.setState({ drawerIsExpanded: !drawerIsExpanded });
+ },
+ selectableActionId: 'clickable-card-input-1',
+ selectableActionAriaLabelledby: 'clickable-card-example-1',
+ name: 'clickable-card-example-1'
+ }}
+ >
+ Clickable card with action
+
+ This card performs an action on click.
+
+
+
+
+
+
+
+
+ Clickable card with link
+
+ This card can navigate to a link on click.
+
+
+
+
+
+ Clickable and selectable card drawer panel
+
+ }
+ >
+
+
+
+
+
+ Clickable and selectable card
+
+
+
+ This card performs an action upon clicking the card title and is selectable.
+
+
+
+
+
);
}
diff --git a/packages/react-integration/demo-app-ts/src/components/demos/TooltipDemo/TooltipDemo.tsx b/packages/react-integration/demo-app-ts/src/components/demos/TooltipDemo/TooltipDemo.tsx
index d934a44fe4e..c2c52b74827 100644
--- a/packages/react-integration/demo-app-ts/src/components/demos/TooltipDemo/TooltipDemo.tsx
+++ b/packages/react-integration/demo-app-ts/src/components/demos/TooltipDemo/TooltipDemo.tsx
@@ -20,20 +20,53 @@ export class TooltipDemo extends Component {
render() {
return (
-
{this.myTooltipProps.children}
-
- Tooltip attached via react ref
+ Tooltip content attached via children
}>
+
Tooltip trigger attached via children
+
+
+
+
+ Tooltip trigger attached via react ref
- Tooltip attached via react ref
} triggerRef={this.tooltipRef} />
+ Tooltip content attached via react ref }
+ triggerRef={this.tooltipRef}
+ />