UNPKG

metadata-based-explorer1

Version:
257 lines (231 loc) 7.32 kB
/** * @flow * @file Item List Key bindings * @author Box */ import React, { PureComponent } from 'react'; import noop from 'lodash/noop'; import { isInputElement } from '../../utils/dom'; type Props = { children: Function, className: string, columnCount: number, id: string | void, items: BoxItem[], onDelete: Function, onDownload: Function, onOpen: Function, onRename: Function, onScrollToChange: Function, onSelect: Function, onShare: Function, rowCount: number, scrollToColumn: number, scrollToRow: number, }; type State = { focusOnRender: boolean, scrollToColumn: number, scrollToRow: number, }; class KeyBinder extends PureComponent<Props, State> { state: State; props: Props; columnStartIndex: number; columnStopIndex: number; rowStartIndex: number; rowStopIndex: number; static defaultProps = { scrollToColumn: 0, scrollToRow: 0, onRename: noop, onShare: noop, onDownload: noop, onOpen: noop, onSelect: noop, onDelete: noop, }; /** * [constructor] * * @private * @return {KeyBinder} */ constructor(props: Props) { super(props); this.state = { scrollToColumn: props.scrollToColumn, scrollToRow: props.scrollToRow, focusOnRender: false, }; this.columnStartIndex = 0; this.columnStopIndex = 0; this.rowStartIndex = 0; this.rowStopIndex = 0; } /** * Resets scroll states and sets new states if * needed specially when collection changes * * @private * @inheritdoc * @return {void} */ componentWillReceiveProps(nextProps: Props): void { const { id, scrollToColumn, scrollToRow }: Props = nextProps; const { id: prevId }: Props = this.props; const { scrollToColumn: prevScrollToColumn, scrollToRow: prevScrollToRow }: State = this.state; const newState = {}; if (id !== prevId) { // Only when the entire collection changes // like folder navigate, reset the scroll states newState.scrollToColumn = 0; newState.scrollToRow = 0; newState.focusOnRender = false; } else if (prevScrollToColumn !== scrollToColumn && prevScrollToRow !== scrollToRow) { newState.scrollToColumn = scrollToColumn; newState.scrollToRow = scrollToRow; } else if (prevScrollToColumn !== scrollToColumn) { newState.scrollToColumn = scrollToColumn; } else if (prevScrollToRow !== scrollToRow) { newState.scrollToRow = scrollToRow; } // Only update the state if there is something to set if (Object.keys(newState).length) { this.setState(newState); } } /** * Keyboard events * * @private * @inheritdoc * @return {void} */ onKeyDown = (event: SyntheticKeyboardEvent<HTMLElement>) => { if (isInputElement(event.target)) { return; } const { columnCount, rowCount, onSelect, onRename, onDownload, onShare, onDelete, onOpen, items, }: Props = this.props; const { scrollToColumn: scrollToColumnPrevious, scrollToRow: scrollToRowPrevious }: State = this.state; let { scrollToColumn, scrollToRow }: State = this.state; const currentItem: BoxItem = items[scrollToRow]; const ctrlMeta: boolean = event.metaKey || event.ctrlKey; // The above cases all prevent default event event behavior. // This is to keep the grid from scrolling after the snap-to update. switch (event.key) { case 'ArrowDown': scrollToRow = ctrlMeta ? rowCount - 1 : Math.min(scrollToRow + 1, rowCount - 1); event.stopPropagation(); // To prevent the arrow down capture of parent break; case 'ArrowLeft': scrollToColumn = ctrlMeta ? 0 : Math.max(scrollToColumn - 1, 0); break; case 'ArrowRight': scrollToColumn = ctrlMeta ? columnCount - 1 : Math.min(scrollToColumn + 1, columnCount - 1); break; case 'ArrowUp': scrollToRow = ctrlMeta ? 0 : Math.max(scrollToRow - 1, 0); break; case 'Enter': onOpen(currentItem); event.preventDefault(); break; case 'Delete': onDelete(currentItem); event.preventDefault(); break; case 'X': onSelect(currentItem); event.preventDefault(); break; case 'D': onDownload(currentItem); event.preventDefault(); break; case 'S': onShare(currentItem); event.preventDefault(); break; case 'R': onRename(currentItem); event.preventDefault(); break; default: return; } if (scrollToColumn !== scrollToColumnPrevious || scrollToRow !== scrollToRowPrevious) { event.preventDefault(); this.updateScrollState({ scrollToColumn, scrollToRow }); } }; /** * Callback for set of rows rendered * * @private * @inheritdoc * @return {void} */ onSectionRendered = ({ columnStartIndex, columnStopIndex, rowStartIndex, rowStopIndex, }: { columnStartIndex: number, columnStopIndex: number, rowStartIndex: number, rowStopIndex: number, }): void => { this.columnStartIndex = columnStartIndex; this.columnStopIndex = columnStopIndex; this.rowStartIndex = rowStartIndex; this.rowStopIndex = rowStopIndex; }; /** * Updates the scroll states * * @private * @inheritdoc * @return {void} */ updateScrollState({ scrollToColumn, scrollToRow }: { scrollToColumn: number, scrollToRow: number }): void { const { onScrollToChange } = this.props; onScrollToChange({ scrollToColumn, scrollToRow }); this.setState({ scrollToColumn, scrollToRow, focusOnRender: true }); } /** * Renders the HOC * * @private * @inheritdoc * @return {void} */ render() { const { className, children } = this.props; const { scrollToColumn, scrollToRow, focusOnRender }: State = this.state; /* eslint-disable jsx-a11y/no-static-element-interactions */ return ( <div className={className} onKeyDown={this.onKeyDown}> {children({ onSectionRendered: this.onSectionRendered, scrollToColumn, scrollToRow, focusOnRender, })} </div> ); /* eslint-enable jsx-a11y/no-static-element-interactions */ } } export default KeyBinder;