terra-list
Version:
The Terra List is a structural component to arrange content within list/listitems.
272 lines (249 loc) • 8.7 kB
JSX
import React, {
useRef, useContext, useState, useEffect,
} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import classNamesBind from 'classnames/bind';
import ThemeContext from 'terra-theme-context';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import { injectIntl } from 'react-intl';
import * as KeyCode from 'keycode-js';
import SubsectionHeader from './ListSubsectionHeader';
import styles from './List.module.scss';
const cx = classNamesBind.bind(styles);
const propTypes = {
/**
* The children list items passed to the component.
*/
children: PropTypes.node,
/**
* Whether or not the subsection is collapsed.
*/
isCollapsed: PropTypes.bool,
/**
* Whether or not the subsection can be collapsed.
*/
isCollapsible: PropTypes.bool,
/**
* Optionally sets the heading level. One of `2`, `3`, `4`, `5`, `6`.
*/
level: PropTypes.oneOf([2, 3, 4, 5, 6]),
/**
* The associated metaData to be provided in the onSelect callback.
*/
// eslint-disable-next-line react/forbid-prop-types
metaData: PropTypes.object,
/**
* Function callback for when the appropriate click or key action is performed.
* Callback contains the javascript evnt and prop metadata, e.g. onSelect(event, metaData)
*/
onSelect: PropTypes.func,
/**
* Function callback passthrough for the ref of the section li.
*/
refCallback: PropTypes.func,
/**
* Title text to be placed within the subsection header.
*/
title: PropTypes.string.isRequired,
/**
* Whether or not the list item is draggable. List Item is draggable only when it is selectable.
*/
isDraggable: PropTypes.bool,
/**
* Function callback when the Item is dropped. Callback contains the DropResult of result object and provided object, e.g. onDragEnd(result, provided).
*/
onDragEnd: PropTypes.func,
/**
* @private
* The intl object to be injected for translations.
*/
intl: PropTypes.shape({ formatMessage: PropTypes.func }),
/**
* z-index value for the list item (li element). Defaults to 6001 which is greater value than terra-modal-manager z-index value.
*/
zIndex: PropTypes.number,
};
const defaultProps = {
children: [],
isCollapsed: false,
isCollapsible: false,
level: 2,
zIndex: 6001,
};
const ListSubsection = ({
children,
isCollapsed,
isCollapsible,
isDraggable,
onDragEnd,
intl,
zIndex,
...customProps
}) => {
const [listItemNodes, setlistItemNodes] = useState(children);
let listSubSectionItemNode = useRef();
const isListItemDropped = useRef();
const draggedItemindex = useRef();
/* eslint-disable-next-line no-param-reassign */
delete customProps?.isTabFocusDisabled;
useEffect(() => {
if (!isCollapsible || !isCollapsed) {
if (Array.isArray(children)) {
setlistItemNodes(children);
} else if (children) {
setlistItemNodes([children]);
}
} else {
setlistItemNodes([]);
}
}, [children, isCollapsible, isCollapsed]);
useEffect(() => {
if (isListItemDropped.current) {
const listItems = listSubSectionItemNode && listSubSectionItemNode.querySelectorAll('[data-item-show-focus]');
if (listItems[draggedItemindex.current]) {
listItems[draggedItemindex.current].focus();
}
isListItemDropped.current = false;
}
}, [listItemNodes]);
const theme = useContext(ThemeContext);
const listClassNames = classNames(
cx('list', 'list-fill', theme.className),
);
const handleListItemsRef = (nodes) => {
listSubSectionItemNode = nodes;
};
const handleKeyDown = event => {
const listItems = listSubSectionItemNode.querySelectorAll('[data-item-show-focus]');
const currentIndex = Array.from(listItems).indexOf(event.target);
const lastIndex = listItems.length - 1;
switch (event.nativeEvent.keyCode) {
case KeyCode.KEY_END:
event.preventDefault();
listItems[listItems.length - 1].focus();
break;
case KeyCode.KEY_HOME:
event.preventDefault();
listItems[0].focus();
break;
case KeyCode.KEY_UP: {
event.preventDefault();
const previousIndex = currentIndex > 0 ? currentIndex - 1 : lastIndex;
listItems[previousIndex].focus();
break;
}
case KeyCode.KEY_DOWN: {
event.preventDefault();
const nextIndex = currentIndex < lastIndex ? currentIndex + 1 : 0;
listItems[nextIndex].focus();
break;
}
default:
break;
}
event.stopPropagation();
};
const reorderListItems = (list, startIndex, endIndex) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
const handleDragEnd = (result, provided) => {
// dropped outside the list
if (!result.destination) {
provided.announce(intl.formatMessage({ id: 'Terra.list.cancelDrag' }, { startPosition: (result.source.index + 1) }));
return;
}
const items = reorderListItems(
listItemNodes,
result.source.index,
result.destination.index,
);
setlistItemNodes(items);
draggedItemindex.current = result.destination.index;
provided.announce(intl.formatMessage({ id: 'Terra.list.drop' }, { startPosition: (result.source.index + 1), endPosition: (result.destination.index + 1) }));
if (onDragEnd) {
onDragEnd(result, provided);
}
};
const handleDragStart = (start, provided) => {
isListItemDropped.current = true;
provided.announce(intl.formatMessage({ id: 'Terra.list.lift' }, { startPosition: (start.source.index + 1) }));
};
const handleDragUpdate = (update, provided) => {
if (update.destination) {
provided.announce(intl.formatMessage({ id: 'Terra.list.drag' }, { startPosition: (update.source.index + 1), endPosition: (update.destination.index + 1) }));
}
};
const getStyleforDrag = (ListItem, snapshot, provider) => {
const styleProperties = provider.draggableProps.style;
if (styleProperties && snapshot && snapshot.isDragging) {
styleProperties.zIndex = zIndex;
}
return styleProperties;
};
const cloneListItem = (ListItem, provider, snapshot) => React.cloneElement(ListItem, {
isDraggable: ListItem?.props?.isSelectable,
refCallback: (refobj) => {
provider.innerRef(refobj);
},
...provider.draggableProps,
...provider.dragHandleProps,
style: getStyleforDrag(ListItem, snapshot, provider),
});
const renderSubSectionListItemsDom = () => (
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
<>
<SubsectionHeader {...customProps} isCollapsible={isCollapsible} isCollapsed={isCollapsed} />
{listItemNodes && (
<li className={cx('list-item')}>
<ul className={listClassNames} ref={(refobj) => handleListItemsRef(refobj)} onKeyDown={handleKeyDown}>
{listItemNodes}
</ul>
</li>
)}
</>
);
window['__react-beautiful-dnd-disable-dev-warnings'] = true;
const renderDraggableListDom = () => (
<DragDropContext onDragEnd={handleDragEnd} onDragStart={handleDragStart} onDragUpdate={handleDragUpdate}>
<Droppable
droppableId="listSubSection"
renderClone={(provided, snapshot, rubric) => (
cloneListItem(listItemNodes[rubric.source.index], provided, snapshot)
)}
>
{(provided) => (
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
<div ref={provided.innerRef}>
<SubsectionHeader {...customProps} isCollapsible={isCollapsible} isCollapsed={isCollapsed} />
<li className={cx('list-item')}>
<ul
className={listClassNames}
onKeyDown={handleKeyDown}
ref={(refobj) => handleListItemsRef(refobj)}
>
{listItemNodes.map((item, index) => (
<Draggable isDragDisabled={!(item?.props?.isSelectable)} key={item.key} draggableId={item.key} index={index}>
{(provider, snapshot) => (
cloneListItem(item, provider, snapshot)
)}
</Draggable>
))}
</ul>
</li>
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
return (
(isDraggable) ? renderDraggableListDom() : renderSubSectionListItemsDom()
);
};
ListSubsection.propTypes = propTypes;
ListSubsection.defaultProps = defaultProps;
export default injectIntl(ListSubsection);