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 ( - - - - - ); - } -} ``` ### 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 ( - - - {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 ( + + + + + ); +}; 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 ( + + + {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 ( + + ); +};