import * as React from 'react'; import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/TreeView/tree-view'; import { canUseDOM } from '../../helpers/util'; import { handleArrows } from '../../helpers'; import { KeyTypes } from '../../helpers/constants'; export interface TreeViewRootProps { /** Child nodes of the tree view */ children: React.ReactNode; /** Flag indicating if the tree view has checkboxes. */ hasCheckboxes?: boolean; /** Flag indicating if tree view has guide lines. */ hasGuides?: boolean; /** Variant presentation styles for the tree view. */ variant?: 'default' | 'compact' | 'compactNoBackground'; /** Class to add to add if not passed a parentItem */ className?: string; /** Flag indicating that tree nodes should be independently selectable, even when having children */ hasSelectableNodes?: boolean; } class TreeViewRoot extends React.Component { displayName = 'TreeViewRoot'; private treeRef = React.createRef(); componentDidMount() { if (canUseDOM) { window.addEventListener( 'keydown', this.props.hasCheckboxes || this.props.hasSelectableNodes ? this.handleKeysCheckbox : this.handleKeys ); } if (this.props.hasCheckboxes || this.props.hasSelectableNodes) { const firstToggle = this.treeRef.current.getElementsByClassName(styles.treeViewNodeToggle)[0] as HTMLElement; if (firstToggle) { firstToggle.tabIndex = 0; } if (this.props.hasCheckboxes) { const firstInput = this.treeRef.current.getElementsByTagName('INPUT')[0] as HTMLElement; if (firstInput) { firstInput.tabIndex = 0; } } if (this.props.hasSelectableNodes) { const firstTextButton = this.treeRef.current.getElementsByClassName(styles.treeViewNodeText)[0] as HTMLElement; if (firstTextButton) { firstTextButton.tabIndex = 0; } } } else { (this.treeRef.current?.getElementsByClassName(styles.treeViewNode)[0] as HTMLElement).tabIndex = 0; } } componentWillUnmount() { if (canUseDOM) { window.removeEventListener( 'keydown', this.props.hasCheckboxes || this.props.hasSelectableNodes ? this.handleKeysCheckbox : this.handleKeys ); } } handleKeys = (event: KeyboardEvent) => { if ( !this.treeRef.current.contains(event.target as HTMLElement) || !(event.target as HTMLElement).classList.contains(styles.treeViewNode) ) { return; } const activeElement = document.activeElement; const key = event.key; const treeItems = Array.from(this.treeRef.current?.getElementsByClassName(styles.treeViewNode)).filter( (el) => !el.classList.contains('pf-m-disabled') ); if (key === KeyTypes.Space) { (activeElement as HTMLElement).click(); event.preventDefault(); } handleArrows( event, treeItems, (element: Element) => activeElement === element, undefined, [], undefined, true, true ); if (['ArrowLeft', 'ArrowRight'].includes(key)) { const isExpandable = activeElement?.firstElementChild?.firstElementChild?.classList.contains( styles.treeViewNodeToggle ); const isExpanded = activeElement?.closest('li')?.classList.contains('pf-m-expanded'); if (key === 'ArrowLeft') { if (isExpandable && isExpanded) { (activeElement as HTMLElement).click(); } else { const parentList = activeElement?.closest('ul')?.parentElement; if (parentList?.tagName !== 'DIV') { const parentButton = parentList?.querySelector('button'); (activeElement as HTMLElement).tabIndex = -1; if (parentButton) { parentButton.tabIndex = 0; parentButton.focus(); } } } } else { if (isExpandable && !isExpanded) { (activeElement as HTMLElement).tabIndex = -1; (activeElement as HTMLElement).click(); const childElement = activeElement?.closest('li')?.querySelector('ul > li')?.querySelector('button'); if (childElement) { childElement.tabIndex = 0; childElement.focus(); } } } event.preventDefault(); } }; handleKeysCheckbox = (event: KeyboardEvent) => { if (!this.treeRef.current.contains(event.target as HTMLElement)) { return; } const activeElement = document.activeElement; const key = event.key; if (key === KeyTypes.Space) { (activeElement as HTMLElement).click(); event.preventDefault(); } const treeNodes = Array.from(this.treeRef.current?.getElementsByClassName(styles.treeViewNode)); handleArrows( event, treeNodes as HTMLElement[], (element: Element) => element.contains(activeElement), (element: Element) => element.querySelector('button,input'), [], undefined, true, true ); if (['ArrowLeft', 'ArrowRight'].includes(key)) { if (key === 'ArrowLeft') { if (activeElement?.tagName === 'INPUT') { activeElement?.parentElement?.previousSibling && (activeElement.parentElement.previousSibling as HTMLElement).focus(); } else if (activeElement?.previousSibling) { if (activeElement.previousElementSibling?.tagName === 'SPAN') { (activeElement.previousSibling.firstChild as HTMLElement).focus(); } else { (activeElement.previousSibling as HTMLElement).focus(); } } } else { if (activeElement?.tagName === 'INPUT') { activeElement.parentElement?.nextSibling && (activeElement.parentElement.nextSibling as HTMLElement).focus(); } else if (activeElement?.nextSibling) { if (activeElement.nextElementSibling?.tagName === 'SPAN') { (activeElement.nextSibling.firstChild as HTMLElement).focus(); } else { (activeElement.nextSibling as HTMLElement).focus(); } } } event.preventDefault(); } }; variantStyleModifiers: { [key in TreeViewRootProps['variant']]: string | string[] } = { default: '', compact: styles.modifiers.compact, compactNoBackground: [styles.modifiers.compact, styles.modifiers.noBackground] }; render() { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { children, hasCheckboxes, hasGuides, variant, className, hasSelectableNodes, ...props } = this.props; return (
{children}
); } } export { TreeViewRoot };