diff --git a/common/changes/office-ui-fabric-react/laxmika-colreorder_2018-05-13-07-07.json b/common/changes/office-ui-fabric-react/laxmika-colreorder_2018-05-13-07-07.json new file mode 100644 index 00000000000000..b69e32754f899b --- /dev/null +++ b/common/changes/office-ui-fabric-react/laxmika-colreorder_2018-05-13-07-07.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "Added column reorder with DragAndDrop support in Details List. As part of this feature, a new component DetailsColumn has been added inside the header, for each column. An optional new prop(ColumnReorderOptions) has been added to DetailsList to handle the column reorder. Have added column level drag subscriptions to be able to drag the columns, and added one header level subscription, to handle the drops. All the drag drop events are being handled at header level.", + "type": "patch" + } + ], + "packageName": "office-ui-fabric-react", + "email": "laxmika@microsoft.com" +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsColumn.tsx b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsColumn.tsx new file mode 100644 index 00000000000000..7d375d9df58c64 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsColumn.tsx @@ -0,0 +1,254 @@ +import * as React from 'react'; +import * as stylesImport from './DetailsHeader.scss'; +const styles: any = stylesImport; +import { Icon } from '../../Icon'; +import { BaseComponent, css, IRenderFunction, createRef, IDisposable } from '../../Utilities'; +import { IColumn, ColumnActionsMode } from './DetailsList.types'; + +import { ITooltipHostProps } from '../../Tooltip'; +import { IDragDropHelper, IDragDropOptions } from './../../utilities/dragdrop/interfaces'; + +const INNER_PADDING = 16; // Account for padding around the cell. +const ISPADDED_WIDTH = 24; +const MOUSEDOWN_PRIMARY_BUTTON = 0; // for mouse down event we are using ev.button property, 0 means left button + +export interface IDetailsColumnProps extends React.Props { + componentRef?: () => void; + column: IColumn; + columnIndex: number; + parentId?: string; + onRenderColumnHeaderTooltip?: IRenderFunction; + onColumnClick?: (ev: React.MouseEvent, column: IColumn) => void; + onColumnContextMenu?: (column: IColumn, ev: React.MouseEvent) => void; + dragDropHelper?: IDragDropHelper | null; + isDraggable?: boolean; + setDraggedItemIndex?: (itemIndex: number) => void; + isDropped?: boolean; +} + +export class DetailsColumn extends BaseComponent { + private _root: any; + private _dragDropSubscription: IDisposable; + + constructor(props: IDetailsColumnProps) { + super(props); + + this._root = createRef(); + this._onDragStart = this._onDragStart.bind(this); + this._onDragEnd = this._onDragEnd.bind(this); + this._onRootMouseDown = this._onRootMouseDown.bind(this); + } + + public render() { + const { column, columnIndex, parentId, isDraggable } = this.props; + const { onRenderColumnHeaderTooltip = this._onRenderColumnHeaderTooltip } = this.props; + + return [ +
+ {isDraggable && } + {onRenderColumnHeaderTooltip( + { + hostClassName: css(styles.cellTooltip), + id: `${parentId}-${column.key}-tooltip`, + setAriaDescribedBy: false, + content: column.columnActionsMode !== ColumnActionsMode.disabled ? column.ariaLabel : '', + children: ( + + + {(column.iconName || column.iconClassName) && ( + + )} + + {!column.isIconOnly ? column.name : undefined} + + + {column.isFiltered && } + + {column.isSorted && ( + + )} + + {column.isGrouped && } + + {column.columnActionsMode === ColumnActionsMode.hasDropdown && + !column.isIconOnly && ( + + )} + + ) + }, + this._onRenderColumnHeaderTooltip + )} +
, + column.ariaLabel && !this.props.onRenderColumnHeaderTooltip ? ( + + ) : null + ]; + } + + public componentDidMount(): void { + if (this._dragDropSubscription) { + this._dragDropSubscription.dispose(); + delete this._dragDropSubscription; + } + + if (this.props.dragDropHelper && this.props.isDraggable!) { + this._dragDropSubscription = this.props.dragDropHelper.subscribe( + this._root.current as HTMLElement, + this._events, + this._getColumnDragDropOptions() + ); + + // We need to use native on this to avoid MarqueeSelection from handling the event before us. + this._events.on(this._root.current, 'mousedown', this._onRootMouseDown); + } + if (this.props.isDropped) { + if (this._root!.current!) { + this._root!.current!.classList!.add(styles.borderAfterDropping); + } + setTimeout(() => { + if (this._root!.current!) { + this._root!.current!.classList!.remove(styles.borderAfterDropping); + } + }, 1500); + } + } + + public componentWillUnmount(): void { + if (this._dragDropSubscription) { + this._dragDropSubscription.dispose(); + delete this._dragDropSubscription; + } + } + + public componentDidUpdate(): void { + if (!this._dragDropSubscription && this.props.dragDropHelper && this.props.isDraggable!) { + this._dragDropSubscription = this.props.dragDropHelper.subscribe( + this._root.value as HTMLElement, + this._events, + this._getColumnDragDropOptions() + ); + + // We need to use native on this to avoid MarqueeSelection from handling the event before us. + this._events.on(this._root.current, 'mousedown', this._onRootMouseDown); + } + if (this._dragDropSubscription && !this.props.isDraggable!) { + this._dragDropSubscription.dispose(); + this._events.off(this._root.current, 'mousedown'); + delete this._dragDropSubscription; + } + } + + private _onRenderColumnHeaderTooltip = ( + tooltipHostProps: ITooltipHostProps, + defaultRender?: IRenderFunction + ): JSX.Element => { + return {tooltipHostProps.children}; + }; + + private _onColumnClick(column: IColumn, ev: React.MouseEvent): void { + const { onColumnClick } = this.props; + if (column.onColumnClick) { + column.onColumnClick(ev, column); + } + if (onColumnClick) { + onColumnClick(ev, column); + } + } + + private _getColumnDragDropOptions(): IDragDropOptions { + const { columnIndex } = this.props; + const options = { + selectionIndex: columnIndex, + context: { data: columnIndex, index: columnIndex }, + canDrag: () => this.props.isDraggable!, + canDrop: () => false, + onDragStart: this._onDragStart, + updateDropState: () => undefined, + onDrop: () => undefined, + onDragEnd: this._onDragEnd + }; + return options; + } + + private _onDragStart(item?: any, itemIndex?: number, selectedItems?: any[], event?: MouseEvent): void { + if (itemIndex && this.props.setDraggedItemIndex) { + this.props.setDraggedItemIndex(itemIndex); + this._root.current.classList.add(styles.borderWhileDragging); + } + } + + private _onDragEnd(item?: any, event?: MouseEvent): void { + if (this.props.setDraggedItemIndex) { + this.props.setDraggedItemIndex(-1); + this._root.current.classList.remove(styles.borderWhileDragging); + } + } + + private _onColumnContextMenu(column: IColumn, ev: React.MouseEvent): void { + const { onColumnContextMenu } = this.props; + if (column.onColumnContextMenu) { + column.onColumnContextMenu(column, ev); + ev.preventDefault(); + } + if (onColumnContextMenu) { + onColumnContextMenu(column, ev); + ev.preventDefault(); + } + } + + private _onRootMouseDown = (ev: MouseEvent): void => { + const { isDraggable } = this.props; + // Ignore anything except the primary button. + if (isDraggable && ev.button === MOUSEDOWN_PRIMARY_BUTTON) { + ev.stopPropagation(); + } + }; +} diff --git a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsHeader.scss b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsHeader.scss index dcb76f313241e4..7380da94b658bc 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsHeader.scss +++ b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsHeader.scss @@ -75,6 +75,18 @@ $isPaddedMargin: 24px; &.cellIsEmpty { text-overflow: clip; } + + &:hover .gripperBarVerticalStyle { + display: block; + } +} + +.gripperBarVerticalStyle { + display: none; + position: absolute; + @include ms-text-align(left); + color: $ms-color-neutralTertiary; + @include ms-left(1px); } .cellSizer { @@ -123,6 +135,7 @@ $isPaddedMargin: 24px; @include margin-left(-16px); } + .collapseButton { text-align: center; transform: rotate(-180deg); @@ -168,7 +181,7 @@ $isPaddedMargin: 24px; box-sizing: border-box; overflow: hidden; @include focus-border($position: auto); - padding: 0 8px; + padding: 0 8px 0 12px; } .cellName { @@ -212,3 +225,66 @@ $isPaddedMargin: 24px; .accessibleLabel { @include ms-screenReaderOnly; } + +.borderWhileDragging { + border-style : solid; + border-width: 1px; + border-color:$ms-color-themePrimary; + -webkit-animation: fadeOut 0.2s forwards; + animation: fadeOut 0.2s forwards; +} + +.dropHintCircleStyle { + display:inline-block; + visibility:hidden; + position: absolute; + bottom: 0; + top: -27px; + height: 9px; + width: 9px; + border-radius: 50%; + @include ms-margin-left(-5px); + top:34px; + overflow: visible; + z-index: 10; + border:1px solid $ms-color-themePrimary; + background: $ms-color-white; +} + +.dropHintLineStyle{ + display: inline-block; + visibility:hidden; + position: absolute; + bottom: 0; + top: -3px; + overflow: hidden; + height: 37px; + width: 1px; + background: $ms-color-themePrimary; + z-index:10; +} + +.dropHintStyle{ + display: inline-block; + position: absolute; +} + +.borderAfterDropping { + border-style : solid; + border-width: 1px; + border-color:$ms-color-themePrimary; + -webkit-animation: fadeOut 1.5s forwards; + animation: fadeOut 1.5s forwards; + @include ms-left(-1px); + line-height: 31px; +} + + @-webkit-keyframes fadeOut { + from {border-color:$ms-color-themePrimary; } + to {border-color:white } + } + + @keyframes fadeOut { + from {border-color:$ms-color-themePrimary; } + to {border-color:white } + } diff --git a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsHeader.test.tsx b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsHeader.test.tsx index edce4732b27fd9..32274815514d7e 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsHeader.test.tsx +++ b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsHeader.test.tsx @@ -12,6 +12,10 @@ const _columns: IColumn[] = [ { key: 'a', name: 'a', fieldName: 'a', minWidth: 200, maxWidth: 400, calculatedWidth: 200, isResizable: true }, { key: 'b', name: 'b', fieldName: 'a', minWidth: 200, maxWidth: 400, calculatedWidth: 200, isResizable: true } ]; +const _columnReorderOptions = { + frozenColumnCountFromStart: 1, + handleColumnReorder: this._dummyFunction +}; _selection.setItems(_items); @@ -43,6 +47,7 @@ describe('DetailsHeader', () => { layoutMode={DetailsListLayoutMode.fixedColumns} columns={_columns} onColumnResized={onColumnResized} + columnReorderOptions={_columnReorderOptions} /> ); diff --git a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsHeader.tsx b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsHeader.tsx index ccf2ef0e730b1c..1d47a3f144ec90 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsHeader.tsx +++ b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsHeader.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { findDOMNode } from 'react-dom'; import { BaseComponent, css, getRTL, getId, KeyCodes, IRenderFunction, createRef } from '../../Utilities'; -import { IColumn, DetailsListLayoutMode, ColumnActionsMode } from './DetailsList.types'; +import { IColumn, DetailsListLayoutMode, IColumnReorderOptions } from './DetailsList.types'; import { IFocusZone, FocusZone, FocusZoneDirection } from '../../FocusZone'; import { Icon } from '../../Icon'; import { Layer } from '../../Layer'; @@ -13,13 +13,15 @@ import * as checkStylesModule from './DetailsRowCheck.scss'; import { IViewport } from '../../utilities/decorators/withViewport'; import { ISelection, SelectionMode, SELECTION_CHANGE } from '../../utilities/selection/interfaces'; import * as stylesImport from './DetailsHeader.scss'; +import { IDragDropOptions } from './../../utilities/dragdrop/interfaces'; +import { DragDropHelper } from './../../utilities/dragdrop'; +import { DetailsColumn } from './../../components/DetailsList/DetailsColumn'; + const styles: any = stylesImport; const checkStyles: any = checkStylesModule; const MOUSEDOWN_PRIMARY_BUTTON = 0; // for mouse down event we are using ev.button property, 0 means left button const MOUSEMOVE_PRIMARY_BUTTON = 1; // for mouse move event we are using ev.buttons property, 1 means left button -const INNER_PADDING = 16; -const ISPADDED_WIDTH = 24; export interface IDetailsHeader { focus: () => boolean; @@ -49,6 +51,8 @@ export interface IDetailsHeaderProps extends React.Props { ariaLabelForSelectionColumn?: string; selectAllVisibility?: SelectAllVisibility; viewport?: IViewport; + columnReorderOptions?: IColumnReorderOptions | null; + minimumPixelsForDrag?: number; } export enum SelectAllVisibility { @@ -71,16 +75,33 @@ export interface IColumnResizeDetails { columnMinWidth: number; } +export interface IDropHintDetails { + originX: number; // X index of dropHint Element relative to header + startX: number; // start index of the range for the current drophint + endX: number; // end index of the range for the current drophint + dropHintElementRef: HTMLElement; // Reference for drophint to change the style when needed +} + export class DetailsHeader extends BaseComponent implements IDetailsHeader { public static defaultProps = { selectAllVisibility: SelectAllVisibility.visible, collapseAllVisibility: CollapseAllVisibility.visible }; - - private _root = createRef(); - + private _rootElement: HTMLElement | undefined; + private _rootComponent = createRef(); private _id: string; - + private _draggedColumnIndex = -1; + private _dropHintDetails: { [key: number]: IDropHintDetails } = {}; + private _dragDropHelper: DragDropHelper | null; + private _currentDropHintIndex: number; + private _subscriptionObject: { + key: string; + dispose(): void; + }; + private _onDropIndexInfo: { + sourceIndex: number; + targetIndex: number; + }; constructor(props: IDetailsHeaderProps) { super(props); @@ -92,20 +113,70 @@ export class DetailsHeader extends BaseComponent= 0 && this._onDropIndexInfo.targetIndex >= 0) { + if ( + prevProps.columns[this._onDropIndexInfo.sourceIndex].key === + this.props.columns[this._onDropIndexInfo.targetIndex - 1].key + ) { + this._onDropIndexInfo = { + sourceIndex: Number.MIN_SAFE_INTEGER, + targetIndex: Number.MIN_SAFE_INTEGER + }; + } + } } public componentWillReceiveProps(newProps: IDetailsHeaderProps): void { @@ -116,6 +187,13 @@ export class DetailsHeader extends BaseComponent { + return; + } + } as ISelection, + minimumPixelsForDrag: this.props.minimumPixelsForDrag + }); + } + const frozenColumnCountFromStart = + columnReorderOptions && columnReorderOptions!.frozenColumnCountFromStart + ? columnReorderOptions!.frozenColumnCountFromStart! + : 0; + const frozenColumnCountFromEnd = + columnReorderOptions && columnReorderOptions!.frozenColumnCountFromEnd + ? columnReorderOptions!.frozenColumnCountFromEnd! + : 0; return ( {columns.map((column: IColumn, columnIndex: number) => { + const _isDraggable = columnReorderOptions + ? columnIndex >= frozenColumnCountFromStart && columnIndex < columns.length - frozenColumnCountFromEnd + : false; return [ -
- {onRenderColumnHeaderTooltip( - { - hostClassName: css(styles.cellTooltip), - id: `${this._id}-${column.key}-tooltip`, - setAriaDescribedBy: false, - content: column.columnActionsMode !== ColumnActionsMode.disabled ? column.ariaLabel : '', - children: ( - - - {(column.iconName || column.iconClassName) && ( - - )} - - {!column.isIconOnly ? column.name : undefined} - - - {column.isFiltered && ( - - )} - - {column.isSorted && ( - - )} - - {column.isGrouped && ( - - )} - - {column.columnActionsMode === ColumnActionsMode.hasDropdown && - !column.isIconOnly && ( - - )} - - ) - }, - this._onRenderColumnHeaderTooltip - )} -
, - column.ariaLabel && !this.props.onRenderColumnHeaderTooltip ? ( - - ) : null, + columnIndex={(showCheckbox ? 2 : 1) + columnIndex} + parentId={this._id} + isDraggable={_isDraggable} + setDraggedItemIndex={this._setDraggedItemIndex} + dragDropHelper={this._dragDropHelper} + onColumnClick={onColumnClick} + onColumnContextMenu={onColumnContextMenu} + isDropped={this._onDropIndexInfo.targetIndex === columnIndex + 1} + />, column.isResizable && this._renderColumnSizer(columnIndex) ]; })} + {columnReorderOptions && frozenColumnCountFromEnd === 0 && this._renderDropHint(columns.length)} {isSizing && (
false, + canDrop: () => true, + onDragStart: () => undefined, + updateDropState: this._updateDroppingState, + onDrop: this._onDrop, + onDragEnd: () => undefined, + onDragOver: this._onDragOver + }; + return options; + } + + private _updateDroppingState(newValue: boolean, event: DragEvent): void { + if (this._draggedColumnIndex >= 0 && event.type !== 'drop') { + if (!newValue) { + this._resetDropHints(); + } + } + } + + private _isValidCurrentDropHintIndex() { + return this._currentDropHintIndex! >= 0; + } + + private _onDragOver(item: any, event: DragEvent): void { + if (this._draggedColumnIndex >= 0) { + event.stopPropagation(); + this._computeDropHintToBeShown(event.clientX); + } + } + + private _onDrop(item?: any, event?: DragEvent): void { + const draggedColumnIndex = this._draggedColumnIndex; + const dropIndex = this._currentDropHintIndex!; + let isValidDrop = false; + if (this._draggedColumnIndex >= 0 && event! instanceof DragEvent) { + event!.stopPropagation(); + if (this._isValidCurrentDropHintIndex()) { + isValidDrop = true; + this._onDropIndexInfo.sourceIndex = draggedColumnIndex; + // Target index will not get changed if draggeditem is before target item. + this._onDropIndexInfo.targetIndex = dropIndex + (draggedColumnIndex > dropIndex! ? 1 : 0); + } + this._resetDropHints(); + this._dropHintDetails = {}; + this._draggedColumnIndex = -1; + if (isValidDrop) { + this.props.columnReorderOptions!.handleColumnReorder(draggedColumnIndex, dropIndex); + } + } } + private _setDraggedItemIndex(itemIndex: number) { + if (itemIndex >= 0) { + // Column index is set based on the checkbox + this._draggedColumnIndex = this.props.selectionMode !== SelectionMode.none ? itemIndex - 2 : itemIndex - 1; + this._getDropHintPositions(); + } else { + this._resetDropHints(); + this._draggedColumnIndex = -1; + this._dropHintDetails = {}; + } + } + + private _resetDropHints(): void { + if (this._currentDropHintIndex >= 0) { + this._updateDropHintElement(this._dropHintDetails[this._currentDropHintIndex].dropHintElementRef, 'hidden'); + this._currentDropHintIndex = Number.MIN_SAFE_INTEGER; + } + } + + private _updateDropHintElement(element: HTMLElement, property: string) { + (element.childNodes[1] as HTMLElement).style.visibility = property; + (element.childNodes[0] as HTMLElement).style.visibility = property; + } + + private _getDropHintPositions = (): void => { + const { columnReorderOptions, columns } = this.props; + let prevX = 0; + let prevMid = 0; + let prevRef: HTMLElement; + const frozenColumnCountFromStart = + columnReorderOptions && columnReorderOptions!.frozenColumnCountFromStart + ? columnReorderOptions!.frozenColumnCountFromStart + : 0; + const frozenColumnCountFromEnd = + columnReorderOptions && columnReorderOptions!.frozenColumnCountFromEnd + ? columnReorderOptions!.frozenColumnCountFromEnd + : 0; + for (let i = frozenColumnCountFromStart!; i < columns.length - frozenColumnCountFromEnd! + 1; i++) { + const dropHintElement = this._rootElement!.querySelectorAll('#columnDropHint_' + i)[0] as HTMLElement; + if (dropHintElement) { + if (i === frozenColumnCountFromStart!) { + prevX = dropHintElement!.offsetLeft; + prevMid = dropHintElement!.offsetLeft; + prevRef = dropHintElement; + } else { + const newMid = (dropHintElement!.offsetLeft + prevX!) / 2; + this._dropHintDetails[i - 1] = { + originX: prevX, + startX: prevMid!, + endX: newMid, + dropHintElementRef: prevRef! + }; + prevMid = newMid; + prevRef = dropHintElement; + prevX = dropHintElement!.offsetLeft; + if (i === columns.length - frozenColumnCountFromEnd!) { + this._dropHintDetails[i] = { + originX: prevX, + startX: prevMid!, + endX: dropHintElement!.offsetLeft, + dropHintElementRef: prevRef + }; + } + } + } + } + }; + + /** + * Based on the given cursor position, finds the nearest drop hint and updates the state to make it visible + * + */ + private _computeDropHintToBeShown = (clientX: number): void => { + const clientRect = this._rootElement!.getBoundingClientRect(); + const headerOriginX = clientRect.left; + const eventXRelativePosition = clientX - headerOriginX; + const currentDropHintIndex = this._currentDropHintIndex!; + if (this._isValidCurrentDropHintIndex()) { + if ( + eventXRelativePosition >= this._dropHintDetails[currentDropHintIndex!].startX && + eventXRelativePosition <= this._dropHintDetails[currentDropHintIndex!].endX + ) { + return; + } + } + const { columnReorderOptions, columns } = this.props; + const frozenColumnCountFromStart = + columnReorderOptions && columnReorderOptions!.frozenColumnCountFromStart + ? columnReorderOptions!.frozenColumnCountFromStart + : 0; + const frozenColumnCountFromEnd = + columnReorderOptions && columnReorderOptions!.frozenColumnCountFromEnd + ? columnReorderOptions!.frozenColumnCountFromEnd + : 0; + const currentIndex: number = frozenColumnCountFromStart!; + const lastValidColumn = columns.length - frozenColumnCountFromEnd!; + let indexToUpdate = -1; + if (eventXRelativePosition <= this._dropHintDetails[currentIndex].endX) { + indexToUpdate = currentIndex; + } else if (eventXRelativePosition >= this._dropHintDetails[lastValidColumn]!.startX) { + indexToUpdate = lastValidColumn; + } else if (this._isValidCurrentDropHintIndex()) { + if ( + this._dropHintDetails[currentDropHintIndex! + 1] && + eventXRelativePosition >= this._dropHintDetails[currentDropHintIndex! + 1].startX && + eventXRelativePosition <= this._dropHintDetails[currentDropHintIndex! + 1].endX + ) { + indexToUpdate = currentDropHintIndex! + 1; + } else if ( + this._dropHintDetails[currentDropHintIndex! - 1] && + eventXRelativePosition >= this._dropHintDetails[currentDropHintIndex! - 1].startX && + eventXRelativePosition <= this._dropHintDetails[currentDropHintIndex! - 1].endX + ) { + indexToUpdate = currentDropHintIndex! - 1; + } + } + if (indexToUpdate === -1) { + let startIndex = frozenColumnCountFromStart!; + let endIndex = lastValidColumn; + while (startIndex < endIndex) { + const middleIndex = Math.ceil((endIndex + startIndex!) / 2); + if ( + eventXRelativePosition >= this._dropHintDetails[middleIndex].startX && + eventXRelativePosition <= this._dropHintDetails[middleIndex].endX + ) { + indexToUpdate = middleIndex; + break; + } else if (eventXRelativePosition < this._dropHintDetails[middleIndex]!.originX) { + endIndex = middleIndex; + } else if (eventXRelativePosition > this._dropHintDetails[middleIndex]!.originX) { + startIndex = middleIndex; + } + } + } + + if (indexToUpdate === this._draggedColumnIndex || indexToUpdate === this._draggedColumnIndex + 1) { + if (this._isValidCurrentDropHintIndex()) { + this._resetDropHints(); + } + } else if (currentDropHintIndex !== indexToUpdate && indexToUpdate >= 0) { + this._resetDropHints(); + this._updateDropHintElement(this._dropHintDetails[indexToUpdate].dropHintElementRef, 'visible'); + this._currentDropHintIndex = indexToUpdate; + } + }; + private _renderColumnSizer(columnIndex: number): JSX.Element { const { columns } = this.props; const column = this.props.columns[columnIndex]; @@ -362,6 +583,26 @@ export class DetailsHeader extends BaseComponent +
+
+
+ ); + } private _onRenderColumnHeaderTooltip = ( tooltipHostProps: ITooltipHostProps, defaultRender?: IRenderFunction @@ -423,6 +664,15 @@ export class DetailsHeader extends BaseComponent { + if (focusZone) { + // Need to resolve the actual DOM node, not the component. The element itself will be used for drag/drop and focusing. + this._rootElement = findDOMNode(focusZone) as HTMLElement; + } else { + this._rootElement = undefined; + } + }; + private _onRootKeyDown = (ev: KeyboardEvent): void => { const { columnResizeDetails, isSizing } = this.state; const { columns, onColumnResized } = this.props; @@ -571,34 +821,6 @@ export class DetailsHeader extends BaseComponent): void { - const { onColumnClick } = this.props; - - if (column.onColumnClick) { - column.onColumnClick(ev, column); - } - - if (onColumnClick) { - onColumnClick(ev, column); - } - } - - private _onColumnContextMenu(column: IColumn, ev: React.MouseEvent): void { - const { onColumnContextMenu } = this.props; - - if (column.onColumnContextMenu) { - column.onColumnContextMenu(column, ev); - - ev.preventDefault(); - } - - if (onColumnContextMenu) { - onColumnContextMenu(column, ev); - - ev.preventDefault(); - } - } - private _onToggleCollapseAll(): void { const { onToggleCollapseAll } = this.props; const newCollapsed = !this.state.isAllCollapsed; diff --git a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.tsx b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.tsx index b1530a8d3135c8..a972204b1e3583 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.tsx +++ b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.tsx @@ -124,9 +124,9 @@ export class DetailsList extends BaseComponent ) : ( - - )} + + )}
diff --git a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.types.ts b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.types.ts index 817b23abe3a8e0..612ef9bd1b1a65 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.types.ts +++ b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsList.types.ts @@ -148,7 +148,7 @@ export interface IDetailsListProps extends React.Props, IWithViewpo */ onRenderItemColumn?: (item?: any, index?: number, column?: IColumn) => any; - /** Map of callback functions related to drag and drop functionality. */ + /** Map of callback functions related to row drag and drop functionality. */ dragDropEvents?: IDragDropEvents; /** Callback for what to render when the item is missing. */ @@ -241,6 +241,12 @@ export interface IDetailsListProps extends React.Props, IWithViewpo * On horizontal scroll event listener */ onScroll?: (e?: Event) => void; + + /** + * Options for column re-order using drag and drop + * + */ + columnReorderOptions?: IColumnReorderOptions; } export interface IColumn { @@ -436,6 +442,27 @@ export enum ConstrainMode { horizontalConstrained = 1 } +export interface IColumnReorderOptions { + /** + * Specifies the number fixed columns from left(0th index) + * @default 0 + */ + frozenColumnCountFromStart?: number; + + /** + * Specifies the number fixed columns from right + * @default 0 + */ + frozenColumnCountFromEnd?: number; + + /** + * Callback to handle the column reorder + * draggedIndex is the source column index, that need to be placed in targetIndex + */ + handleColumnReorder: (draggedIndex: number, targetIndex: number) => void; + +} + export enum DetailsListLayoutMode { /** * Lets the user resize columns and makes not attempt to fit them. diff --git a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsRow.scss b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsRow.scss index 4b2dcf541f7d2d..9ac3af0666d7d1 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsRow.scss +++ b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsRow.scss @@ -51,7 +51,7 @@ $detailsList-item-focus-meta-text-color: $ms-color-neutralDark; .cell { min-height: $compactRowHeight; - padding: $compactRowVerticalPadding $rowHorizontalPadding; + padding: $compactRowVerticalPadding $rowHorizontalPadding $compactRowVerticalPadding 12px; // Masking the running shimmer background with borders &.shimmer { @@ -214,7 +214,7 @@ $detailsList-item-focus-meta-text-color: $ms-color-neutralDark; display: inline-block; position: relative; box-sizing: border-box; - padding: $rowVerticalPadding $rowHorizontalPadding; + padding: $rowVerticalPadding $rowHorizontalPadding $rowVerticalPadding 12px; min-height: $rowHeight; vertical-align: top; white-space: nowrap; diff --git a/packages/office-ui-fabric-react/src/components/DetailsList/__snapshots__/DetailsHeader.test.tsx.snap b/packages/office-ui-fabric-react/src/components/DetailsList/__snapshots__/DetailsHeader.test.tsx.snap index 019979c9007522..946a2b39980015 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/__snapshots__/DetailsHeader.test.tsx.snap +++ b/packages/office-ui-fabric-react/src/components/DetailsList/__snapshots__/DetailsHeader.test.tsx.snap @@ -140,7 +140,9 @@ exports[`DetailsHeader can render 1`] = ` aria-sort="none" className="ms-DetailsHeader-cell is-actionable undefined" data-automationid="ColumnsHeaderColumn" + data-is-draggable={false} data-item-key="a" + draggable={false} role="columnheader" style={ Object { @@ -188,7 +190,9 @@ exports[`DetailsHeader can render 1`] = ` aria-sort="none" className="ms-DetailsHeader-cell is-actionable undefined" data-automationid="ColumnsHeaderColumn" + data-is-draggable={false} data-item-key="b" + draggable={false} role="columnheader" style={ Object { diff --git a/packages/office-ui-fabric-react/src/components/DetailsList/__snapshots__/DetailsList.test.tsx.snap b/packages/office-ui-fabric-react/src/components/DetailsList/__snapshots__/DetailsList.test.tsx.snap index cb1a6316b696e9..9b449c4d79ecaa 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/__snapshots__/DetailsList.test.tsx.snap +++ b/packages/office-ui-fabric-react/src/components/DetailsList/__snapshots__/DetailsList.test.tsx.snap @@ -166,7 +166,9 @@ exports[`DetailsList renders List correctly 1`] = ` aria-sort="none" className="ms-DetailsHeader-cell is-actionable undefined" data-automationid="ColumnsHeaderColumn" + data-is-draggable={false} data-item-key="key" + draggable={false} role="columnheader" style={ Object { diff --git a/packages/office-ui-fabric-react/src/components/DetailsList/examples/DetailsList.DragDrop.Example.tsx b/packages/office-ui-fabric-react/src/components/DetailsList/examples/DetailsList.DragDrop.Example.tsx index d0d4b5a9e8eefd..87891eca14674a 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/examples/DetailsList.DragDrop.Example.tsx +++ b/packages/office-ui-fabric-react/src/components/DetailsList/examples/DetailsList.DragDrop.Example.tsx @@ -1,20 +1,28 @@ import * as React from 'react'; import { Link } from 'office-ui-fabric-react/lib/Link'; import { DetailsList, Selection } from 'office-ui-fabric-react/lib/DetailsList'; -import { IColumn } from 'office-ui-fabric-react/lib/DetailsList'; import { MarqueeSelection } from 'office-ui-fabric-react/lib/MarqueeSelection'; +import { IColumn, buildColumns } from 'office-ui-fabric-react/lib/DetailsList'; import { IDragDropEvents, IDragDropContext } from 'office-ui-fabric-react/lib/utilities/dragdrop/interfaces'; -import { createListItems } from '../../../utilities/exampleData'; import './DetailsList.DragDrop.Example.scss'; +import { IColumnReorderOptions } from 'office-ui-fabric-react/lib/components/DetailsList'; +import { createListItems } from 'office-ui-fabric-react/lib/utilities/exampleData'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import { Toggle } from 'office-ui-fabric-react/lib/Toggle'; let _draggedItem: any = null; let _draggedIndex = -1; - +let _items: any[]; +let _columns: IColumn[]; export class DetailsListDragDropExample extends React.Component< {}, { items: {}[]; selectionDetails?: string; + columns: IColumn[]; + isColumnReorderEnabled: boolean; + frozenColumnCountFromStart: string; + frozenColumnCountFromEnd: string; } > { private _selection: Selection; @@ -23,35 +31,116 @@ export class DetailsListDragDropExample extends React.Component< super(props); this._onRenderItemColumn = this._onRenderItemColumn.bind(this); + this._handleColumnReorder = this._handleColumnReorder.bind(this); + this._getColumnReorderOptions = this._getColumnReorderOptions.bind(this); + this._onChangeColumnReorderEnabled = this._onChangeColumnReorderEnabled.bind(this); + this._onChangeStartCountText = this._onChangeStartCountText.bind(this); + this._onChangeEndCountText = this._onChangeEndCountText.bind(this); this._selection = new Selection(); + _items = _items || createListItems(10, 0); + _columns = buildColumns(_items, true); + this.state = { - items: createListItems(10) + items: createListItems(10), + columns: _columns, + isColumnReorderEnabled: false, + frozenColumnCountFromStart: '1', + frozenColumnCountFromEnd: '0' }; } public render(): JSX.Element { - const { items, selectionDetails } = this.state; + const { + items, + selectionDetails, + columns, + isColumnReorderEnabled, + frozenColumnCountFromStart, + frozenColumnCountFromEnd + } = this.state; return ( -
+
+ + +
{selectionDetails}
); } + private _handleColumnReorder = (draggedIndex: number, targetIndex: number) => { + const draggedItems = this.state.columns[draggedIndex]; + const newColumns: IColumn[] = [...this.state.columns]; + + // insert before the dropped item + newColumns.splice(draggedIndex, 1); + if (draggedIndex < targetIndex) { + targetIndex--; + } + newColumns.splice(targetIndex, 0, draggedItems); + this.setState({ columns: newColumns }); + }; + + private _getColumnReorderOptions(): IColumnReorderOptions { + return { + frozenColumnCountFromStart: !isNaN(Number(this.state.frozenColumnCountFromStart)) + ? parseInt(this.state.frozenColumnCountFromStart, 10) + : undefined, + frozenColumnCountFromEnd: !isNaN(Number(this.state.frozenColumnCountFromEnd)) + ? parseInt(this.state.frozenColumnCountFromEnd, 10) + : undefined, + handleColumnReorder: this._handleColumnReorder + }; + } + + private _validateNumber(value: string): string { + return isNaN(Number(value)) ? `The value should be a number, actual is ${value}.` : ''; + } + + private _onChangeStartCountText = (text: any): void => { + this.setState({ frozenColumnCountFromStart: text }); + }; + + private _onChangeEndCountText = (text: any): void => { + this.setState({ frozenColumnCountFromEnd: text }); + }; + + private _onChangeColumnReorderEnabled = (checked: boolean): void => { + this.setState({ isColumnReorderEnabled: checked }); + }; + private _getDragDropEvents(): IDragDropEvents { return { canDrop: (dropContext?: IDragDropContext, dragContext?: IDragDropContext) => { diff --git a/packages/office-ui-fabric-react/src/components/__snapshots__/DetailsList.DragDrop.Example.tsx.shot b/packages/office-ui-fabric-react/src/components/__snapshots__/DetailsList.DragDrop.Example.tsx.shot index 399f46321204d1..fb54309e69ebc5 100644 --- a/packages/office-ui-fabric-react/src/components/__snapshots__/DetailsList.DragDrop.Example.tsx.shot +++ b/packages/office-ui-fabric-react/src/components/__snapshots__/DetailsList.DragDrop.Example.tsx.shot @@ -4,6 +4,260 @@ exports[`Component Examples renders DetailsList.DragDrop.Example.tsx correctly 1
+
+ +
+ + +
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
{ event.preventDefault(); + if (dragDropOptions.onDragOver) { + dragDropOptions.onDragOver(dragDropOptions.context.data, event); + } }; this._dragEnterCounts[key] = 0; diff --git a/packages/office-ui-fabric-react/src/utilities/dragdrop/interfaces.ts b/packages/office-ui-fabric-react/src/utilities/dragdrop/interfaces.ts index 4a88c5bee2f3e2..77e287814eac21 100644 --- a/packages/office-ui-fabric-react/src/utilities/dragdrop/interfaces.ts +++ b/packages/office-ui-fabric-react/src/utilities/dragdrop/interfaces.ts @@ -47,6 +47,7 @@ export interface IDragDropOptions { onDragStart?: (item?: any, itemIndex?: number, selectedItems?: any[], event?: MouseEvent) => void; onDrop?: (item?: any, event?: DragEvent) => void; onDragEnd?: (item?: any, event?: DragEvent) => void; + onDragOver?: (item?: any, event?: DragEvent) => void; } export interface IDragDropEvent {