diff --git a/common/changes/office-ui-fabric-react/laxmi-backport_2018-07-10-12-30.json b/common/changes/office-ui-fabric-react/laxmi-backport_2018-07-10-12-30.json new file mode 100644 index 0000000000000..4acc13895d47c --- /dev/null +++ b/common/changes/office-ui-fabric-react/laxmi-backport_2018-07-10-12-30.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": "minor" + } + ], + "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 0000000000000..9186d90a3eeac --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsColumn.tsx @@ -0,0 +1,258 @@ +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 9d2e4137ded60..3cc616353371c 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); @@ -169,7 +182,7 @@ $isPaddedMargin: 24px; overflow: hidden; max-width: 100%; @include focus-border($position: auto); - padding: 0 8px; + padding: 0 8px 0 12px; } .cellName { @@ -213,3 +226,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 57ccecef075c9..71b31f82d1c94 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); @@ -20,10 +24,10 @@ describe('DetailsHeader', () => { it('can render', () => { const component = renderer.create( ); expect(component.toJSON()).toMatchSnapshot(); @@ -41,12 +45,13 @@ describe('DetailsHeader', () => { const wrapper = mount( ); 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 25a76aa132e93..7b948f4d9641f 100644 --- a/packages/office-ui-fabric-react/src/components/DetailsList/DetailsHeader.tsx +++ b/packages/office-ui-fabric-react/src/components/DetailsList/DetailsHeader.tsx @@ -1,15 +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 { BaseComponent, css, getRTL, getId, KeyCodes, IRenderFunction, createRef } from '../../Utilities'; +import { IColumn, DetailsListLayoutMode, IColumnReorderOptions } from './DetailsList.types'; import { IFocusZone, FocusZone, FocusZoneDirection } from '../../FocusZone'; import { Icon } from '../../Icon'; import { Layer } from '../../Layer'; @@ -20,13 +12,15 @@ import { ITooltipHostProps } from '../../Tooltip'; import * as checkStylesModule from './DetailsRowCheck.scss'; 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; @@ -54,6 +48,8 @@ export interface IDetailsHeaderProps extends React.Props { ariaLabelForSelectAllCheckbox?: string; ariaLabelForSelectionColumn?: string; selectAllVisibility?: SelectAllVisibility; + columnReorderOptions?: IColumnReorderOptions | null; + minimumPixelsForDrag?: number; } export enum SelectAllVisibility { @@ -76,16 +72,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); @@ -97,20 +110,70 @@ export class DetailsHeader extends BaseComponent= 0 && this._onDropIndexInfo.targetIndex >= 0) { + if ( + prevProps.columns[this._onDropIndexInfo.sourceIndex].key === + this.props.columns[this._onDropIndexInfo.targetIndex].key + ) { + this._onDropIndexInfo = { + sourceIndex: Number.MIN_SAFE_INTEGER, + targetIndex: Number.MIN_SAFE_INTEGER + }; + } + } } public componentWillReceiveProps(newProps: IDetailsHeaderProps): void { @@ -121,15 +184,46 @@ 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 ( - { showCheckbox ? ( - [ + { showCheckbox + ? [
- { - onRenderColumnHeaderTooltip({ + { onRenderColumnHeaderTooltip( + { hostClassName: css(styles.checkTooltip), id: `${this._id}-checkTooltip`, setAriaDescribedBy: false, @@ -182,151 +277,276 @@ export class DetailsHeader extends BaseComponent ) - }, this._onRenderColumnHeaderTooltip) - } + }, + this._onRenderColumnHeaderTooltip + ) }
, ariaLabelForSelectAllCheckbox && !this.props.onRenderColumnHeaderTooltip ? ( -