Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 243 additions & 39 deletions packages/react-core/src/components/DataList/DataList.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import * as React from 'react';

import { css } from '@patternfly/react-styles';
import styles from '@patternfly/react-styles/css/components/DataList/data-list';
import { PickOptional } from '../../helpers/typeUtils';

export enum DataListWrapModifier {
nowrap = 'nowrap',
truncate = 'truncate',
breakWord = 'breakWord'
}

export interface DataListProps extends React.HTMLProps<HTMLUListElement> {
export interface DataListProps extends Omit<React.HTMLProps<HTMLUListElement>, 'onDragStart'> {
/** Content rendered inside the DataList list */
children?: React.ReactNode;
/** Additional classes added to the DataList list */
Expand All @@ -18,61 +18,265 @@ export interface DataListProps extends React.HTMLProps<HTMLUListElement> {
'aria-label': string;
/** Optional callback to make DataList selectable, fired when DataListItem selected */
onSelectDataListItem?: (id: string) => void;
/** Optional callback to make DataList draggable, fired when dragging ends */
onDragFinish?: (newItemOrder: string[]) => void;
/** Optional informational callback for dragging, fired when dragging starts */
onDragStart?: (id: string) => void;
/** Optional informational callback for dragging, fired when an item moves */
onDragMove?: (oldIndex: number, newIndex: number) => void;
/** Optional informational callback for dragging, fired when dragging is cancelled */
onDragCancel?: () => void;
/** Id of DataList item currently selected */
selectedDataListItemId?: string;
/** Flag indicating if DataList should have compact styling */
isCompact?: boolean;
/** Determines which wrapping modifier to apply to the DataList */
wrapModifier?: DataListWrapModifier | 'nowrap' | 'truncate' | 'breakWord';
/** Order of items in a draggable DataList */
itemOrder?: string[];
}

interface DataListState {
draggedItemId: string;
draggingToItemIndex: number;
dragging: boolean;
tempItemOrder: string[];
}

interface DataListContextProps {
isSelectable: boolean;
selectedDataListItemId: string;
updateSelectedDataListItem: (id: string) => void;
isDraggable: boolean;
dragStart: (e: React.DragEvent) => void;
dragEnd: (e: React.DragEvent) => void;
dragKeyHandler: (e: React.KeyboardEvent) => void;
}

export const DataListContext = React.createContext<Partial<DataListContextProps>>({
isSelectable: false
});

export const DataList: React.FunctionComponent<DataListProps> = ({
children = null,
className = '',
'aria-label': ariaLabel,
selectedDataListItemId = '',
onSelectDataListItem,
isCompact = false,
wrapModifier = null,
...props
}: DataListProps) => {
const isSelectable = onSelectDataListItem !== undefined;

const updateSelectedDataListItem = (id: string) => {
onSelectDataListItem(id);
const moveItem = (arr: string[], i1: string, toIndex: number) => {
const fromIndex = arr.indexOf(i1);
if (fromIndex === toIndex) {
return arr;
}
const temp = arr.splice(fromIndex, 1);
arr.splice(toIndex, 0, temp[0]);

return arr;
};

export class DataList extends React.Component<DataListProps, DataListState> {
static displayName = 'DataList';
static defaultProps: PickOptional<DataListProps> = {
children: null,
className: '',
selectedDataListItemId: '',
isCompact: false,
wrapModifier: null
};
dragFinished: boolean = false;
arrayCopy: React.ReactElement[] = React.Children.toArray(this.props.children) as React.ReactElement[];
ref = React.createRef<HTMLUListElement>();

state: DataListState = {
tempItemOrder: [],
draggedItemId: null,
draggingToItemIndex: null,
dragging: false
};

componentDidUpdate(oldProps: DataListProps) {
if (this.dragFinished) {
this.dragFinished = false;

this.setState({
tempItemOrder: [...this.props.itemOrder],
draggedItemId: null,
dragging: false
});
}
if (oldProps.itemOrder !== this.props.itemOrder) {
this.move(this.props.itemOrder);
}
}

getIndex = (id: string) => Array.from(this.ref.current.children).findIndex(item => item.id === id);

move = (itemOrder: string[]) => {
const ulNode = this.ref.current;
const nodes = Array.from(ulNode.children);
if (nodes.map(node => node.id).every((id, i) => id === itemOrder[i])) {
return;
}
while (ulNode.firstChild) {
ulNode.removeChild(ulNode.lastChild);
}

itemOrder.forEach(id => {
ulNode.appendChild(nodes.find(n => n.id === id));
});
};

dragStart0 = (el: HTMLElement) => {
const { onDragStart } = this.props;
const draggedItemId = el.id;

el.classList.add(styles.modifiers.ghostRow);
el.setAttribute('aria-pressed', 'true');
this.setState({
draggedItemId,
dragging: true
});
onDragStart && onDragStart(draggedItemId);
};

dragStart = (evt: React.DragEvent) => {
evt.dataTransfer.effectAllowed = 'move';
evt.dataTransfer.setData('text/plain', evt.currentTarget.id);
this.dragStart0(evt.currentTarget as HTMLElement);
};

return (
<DataListContext.Provider
value={{
isSelectable,
selectedDataListItemId,
updateSelectedDataListItem
}}
>
<ul
className={css(
styles.dataList,
isCompact && styles.modifiers.compact,
className,
wrapModifier && styles.modifiers[wrapModifier]
)}
aria-label={ariaLabel}
{...props}
dragEnd0 = (el: HTMLElement) => {
el.classList.remove(styles.modifiers.ghostRow);
el.setAttribute('aria-pressed', 'false');
this.props.onDragFinish(this.state.tempItemOrder);
};

dragEnd = (evt: React.DragEvent) => {
this.dragEnd0(evt.currentTarget as HTMLElement);
};

dragOver0 = (id: string) => {
const draggingToItemIndex = Array.from(this.ref.current.children).findIndex(item => item.id === id);
if (draggingToItemIndex !== this.state.draggingToItemIndex) {
const tempItemOrder = moveItem([...this.props.itemOrder], this.state.draggedItemId, draggingToItemIndex);
this.move(tempItemOrder);

this.setState({
draggingToItemIndex,
tempItemOrder
});
}
};

dragOver = (evt: React.DragEvent) => {
evt.preventDefault();
const currListItem = (evt.target as Element).closest('li');
if (currListItem && currListItem.classList.contains(css(styles.modifiers.ghostRow))) {
return;
}
this.dragOver0(currListItem.id);
};

handleDragButtonKeys = (evt: React.KeyboardEvent) => {
const { dragging } = this.state;
const { onDragCancel } = this.props;
if (
evt.key !== ' ' &&
evt.key !== 'Escape' &&
evt.key !== 'Enter' &&
evt.key !== 'ArrowUp' &&
evt.key !== 'ArrowDown'
) {
if (dragging) {
evt.preventDefault();
}
return;
}
evt.preventDefault();

const dragItem = (evt.target as Element).closest('li');

if (evt.key === ' ' || (evt.key === 'Enter' && !dragging)) {
this.dragStart0(dragItem);
} else if (dragging) {
if (evt.key === 'Escape' || evt.key === 'Enter') {
this.setState({
dragging: false
});
this.dragFinished = true;
if (evt.key === 'Enter') {
this.dragEnd0(dragItem);
} else {
onDragCancel && onDragCancel();
}
} else if (evt.key === 'ArrowUp') {
const nextSelection = dragItem.previousSibling as HTMLElement;
if (nextSelection) {
this.dragOver0(nextSelection.id);
(dragItem.querySelector(`.${styles.dataListItemDraggableButton}`) as HTMLElement).focus();
}
} else if (evt.key === 'ArrowDown') {
const nextSelection = dragItem.nextSibling as HTMLElement;
if (nextSelection) {
this.dragOver0(nextSelection.id);
(dragItem.querySelector(`.${styles.dataListItemDraggableButton}`) as HTMLElement).focus();
}
}
}
};

render() {
const {
className,
children,
onSelectDataListItem,
selectedDataListItemId,
isCompact,
onDragStart,
onDragMove,
onDragCancel,
onDragFinish,
wrapModifier,
itemOrder,
...props
} = this.props;
const { dragging } = this.state;
const isSelectable = onSelectDataListItem !== undefined;
const isDraggable = onDragFinish !== undefined;

const updateSelectedDataListItem = (id: string) => {
onSelectDataListItem(id);
};

const dragProps = isDraggable && {
onDragOver: this.dragOver,
onDrop: this.dragOver
};

return (
<DataListContext.Provider
value={{
isSelectable,
selectedDataListItemId,
updateSelectedDataListItem,
isDraggable,
dragStart: this.dragStart,
dragEnd: this.dragEnd,
dragKeyHandler: this.handleDragButtonKeys
}}
>
{children}
</ul>
</DataListContext.Provider>
);
};
DataList.displayName = 'DataList';
<ul
className={css(
styles.dataList,
isCompact && styles.modifiers.compact,
wrapModifier && styles.modifiers[wrapModifier],
className
)}
style={{
...(dragging && { overflowAnchor: 'none' }),
...props.style
}}
{...props}
{...dragProps}
ref={this.ref}
>
{children}
</ul>
</DataListContext.Provider>
);
}
}
19 changes: 14 additions & 5 deletions packages/react-core/src/components/DataList/DataListCheck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface DataListCheckProps extends Omit<React.HTMLProps<HTMLInputElemen
onChange?: (checked: boolean, event: React.FormEvent<HTMLInputElement>) => void;
/** Aria-labelledby of the DataList checkbox */
'aria-labelledby': string;
/** Flag to indicate if other controls are used in the DataListItem */
otherControls?: boolean;
}

export const DataListCheck: React.FunctionComponent<DataListCheckProps> = ({
Expand All @@ -27,10 +29,11 @@ export const DataListCheck: React.FunctionComponent<DataListCheckProps> = ({
isDisabled = false,
isChecked = null,
checked = null,
otherControls = false,
...props
}: DataListCheckProps) => (
<div className={css(styles.dataListItemControl, className)}>
<div className={css('pf-c-data-list__check')}>
}: DataListCheckProps) => {
const check = (
<div className={css(styles.dataListCheck)}>
<input
{...props}
type="checkbox"
Expand All @@ -40,6 +43,12 @@ export const DataListCheck: React.FunctionComponent<DataListCheckProps> = ({
checked={isChecked || checked}
/>
</div>
</div>
);
);
return (
<React.Fragment>
{!otherControls && <div className={css(styles.dataListItemControl, className)}>{check}</div>}
{otherControls && check}
</React.Fragment>
);
};
DataListCheck.displayName = 'DataListCheck';
21 changes: 21 additions & 0 deletions packages/react-core/src/components/DataList/DataListControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as React from 'react';
import { css } from '@patternfly/react-styles';
import styles from '@patternfly/react-styles/css/components/DataList/data-list';

export interface DataListControlProps extends React.HTMLProps<HTMLDivElement> {
/** Children of the data list control */
children?: React.ReactNode;
/** Additional classes added to the DataList item control */
className?: string;
}

export const DataListControl: React.FunctionComponent<DataListControlProps> = ({
children,
className = '',
...props
}: DataListControlProps) => (
<div className={css(styles.dataListItemControl, className)} {...props}>
{children}
</div>
);
DataListControl.displayName = 'DataListControl';
Loading