import * as React from 'react'; import { css } from '@patternfly/react-styles'; import { DropdownContext } from './dropdownConstants'; import { KEYHANDLER_DIRECTION } from '../../../helpers/constants'; import { preventedEvents } from '../../../helpers/util'; import { Tooltip } from '../../../components/Tooltip'; import styles from '@patternfly/react-styles/css/components/Dropdown/dropdown'; export interface InternalDropdownItemProps extends React.HTMLProps { /** Anything which can be rendered as dropdown item */ children?: React.ReactNode; /** Whether to set className on component when React.isValidElement(component) */ styleChildren?: boolean; /** Classes applied to root element of dropdown item */ className?: string; /** Class applied to list element */ listItemClassName?: string; /** Indicates which component will be used as dropdown item. Will have className injected if React.isValidElement(component) */ component?: React.ReactNode; /** Role for the item */ role?: string; /** Render dropdown item as disabled option */ isDisabled?: boolean; /** Render dropdown item as aria disabled option */ isAriaDisabled?: boolean; /** Render dropdown item as a non-interactive item */ isPlainText?: boolean; /** Default hyperlink location */ href?: string; /** Tooltip to display when hovered over the item */ tooltip?: React.ReactNode; /** Additional tooltip props forwarded to the Tooltip component */ tooltipProps?: any; index?: number; context?: { keyHandler?: (index: number, innerIndex: number, direction: string) => void; sendRef?: (index: number, ref: any, isDisabled: boolean, isSeparator: boolean) => void; }; /** Callback for click event */ onClick?: (event: React.MouseEvent | React.KeyboardEvent | MouseEvent) => void; /** ID for the list element */ id?: string; /** ID for the component element */ componentID?: string; /** Additional content to include alongside item within the
- */ additionalChild?: React.ReactNode; /** Custom item rendering that receives the DropdownContext */ customChild?: React.ReactNode; /** Flag indicating if hitting enter on an item also triggers an arrow down key press */ enterTriggersArrowDown?: boolean; /** An image to display within the InternalDropdownItem, appearing before any component children */ icon?: React.ReactNode; /** Initial focus on the item when the menu is opened (Note: Only applicable to one of the items) */ autoFocus?: boolean; /** A short description of the dropdown item, displayed under the dropdown item content */ description?: React.ReactNode; /** Events to prevent when the item is disabled */ inoperableEvents?: string[]; } class InternalDropdownItem extends React.Component { static displayName = 'InternalDropdownItem'; ref = React.createRef(); additionalRef = React.createRef(); static defaultProps: InternalDropdownItemProps = { className: '', component: 'a', role: 'none', isDisabled: false, isPlainText: false, tooltipProps: {}, // eslint-disable-next-line @typescript-eslint/no-unused-vars onClick: (event: React.MouseEvent | React.KeyboardEvent | MouseEvent) => undefined as any, index: -1, context: { keyHandler: () => {}, sendRef: () => {} }, enterTriggersArrowDown: false, icon: null, styleChildren: true, description: null, inoperableEvents: ['onClick', 'onKeyPress'] }; componentDidMount() { const { context, index, isDisabled, role, customChild, autoFocus } = this.props; const customRef = customChild ? this.getInnerNode(this.ref.current) : this.ref.current; context?.sendRef?.( index, [customRef, customChild ? customRef : this.additionalRef.current], isDisabled, role === 'separator' ); autoFocus && setTimeout(() => customRef.focus()); } componentDidUpdate() { const { context, index, isDisabled, role, customChild } = this.props; const customRef = customChild ? this.getInnerNode(this.ref.current) : this.ref.current; context?.sendRef?.( index, [customRef, customChild ? customRef : this.additionalRef.current], isDisabled, role === 'separator' ); } getInnerNode = (node: any) => (node && node.childNodes && node.childNodes.length ? node.childNodes[0] : node); onKeyDown = (event: any) => { // Detected key press on this item, notify the menu parent so that the appropriate item can be focused const innerIndex = event.target === this.ref.current ? 0 : 1; if (!this.props.customChild) { event.preventDefault(); } if (event.key === 'ArrowUp') { this.props.context?.keyHandler?.(this.props.index, innerIndex, KEYHANDLER_DIRECTION.UP); event.stopPropagation(); } else if (event.key === 'ArrowDown') { this.props.context?.keyHandler?.(this.props.index, innerIndex, KEYHANDLER_DIRECTION.DOWN); event.stopPropagation(); } else if (event.key === 'ArrowRight') { this.props.context?.keyHandler?.(this.props.index, innerIndex, KEYHANDLER_DIRECTION.RIGHT); event.stopPropagation(); } else if (event.key === 'ArrowLeft') { this.props.context?.keyHandler?.(this.props.index, innerIndex, KEYHANDLER_DIRECTION.LEFT); event.stopPropagation(); } else if (event.key === 'Enter' || event.key === ' ') { event.target.click(); this.props.enterTriggersArrowDown && this.props.context?.keyHandler?.(this.props.index, innerIndex, KEYHANDLER_DIRECTION.DOWN); } }; extendAdditionalChildRef() { const { additionalChild } = this.props; return React.cloneElement(additionalChild as React.ReactElement, { ref: this.additionalRef }); } componentRef = (element: HTMLLIElement) => { (this.ref as React.MutableRefObject).current = element; const { component } = this.props; const ref = (component as any).ref; if (ref) { if (typeof ref === 'function') { ref(element); } else { (ref as React.MutableRefObject).current = element; } } }; render() { /* eslint-disable @typescript-eslint/no-unused-vars */ const { className, children, context, onClick, component, role, isDisabled, isAriaDisabled, isPlainText, index, href, tooltip, tooltipProps, id, componentID, listItemClassName, additionalChild, customChild, enterTriggersArrowDown, icon, autoFocus, styleChildren, description, inoperableEvents, ...additionalProps } = this.props; /* eslint-enable @typescript-eslint/no-unused-vars */ let classes = css(icon && styles.modifiers.icon, isAriaDisabled && styles.modifiers.ariaDisabled, className); if (component === 'a') { additionalProps['aria-disabled'] = isDisabled || isAriaDisabled; } else if (component === 'button') { additionalProps['aria-disabled'] = isDisabled || isAriaDisabled; additionalProps.type = additionalProps.type || 'button'; } const renderWithTooltip = (childNode: React.ReactNode) => tooltip ? ( {childNode} ) : ( childNode ); const renderClonedComponent = (element: React.ReactElement) => React.cloneElement(element, { ...(styleChildren && { className: css(element.props.className, classes) }), ...(this.props.role !== 'separator' && { role, ref: this.componentRef }) }); const renderDefaultComponent = (tag: string) => { const Component = tag as any; const componentContent = description ? ( <>
  {icon && {icon}} {children}
  {description}
  ) : ( <> {icon && {icon}} {children} ); return ( {componentContent} ); }; return ( {({ onSelect, itemClass, disabledClass, plainTextClass }) => { if (this.props.role !== 'separator') { classes = css( classes, isDisabled && disabledClass, isPlainText && plainTextClass, itemClass, description && styles.modifiers.description ); } if (customChild) { return React.cloneElement(customChild as React.ReactElement, { ref: this.ref, onKeyDown: this.onKeyDown }); } return (
- { if (!isDisabled && !isAriaDisabled) { onClick?.(event); onSelect?.(event); } }} id={id} > {renderWithTooltip( React.isValidElement(component) ? renderClonedComponent(component) : renderDefaultComponent(component as string) )} {additionalChild && this.extendAdditionalChildRef()}
); }} ); } } export { InternalDropdownItem };