Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
@@ -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"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

patch [](start = 15, length = 5)

This should be 'minor'

}
],
"packageName": "office-ui-fabric-react",
"email": "laxmika@microsoft.com"
}
Original file line number Diff line number Diff line change
@@ -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<DetailsColumn> {
componentRef?: () => void;
column: IColumn;
columnIndex: number;
parentId?: string;
onRenderColumnHeaderTooltip?: IRenderFunction<ITooltipHostProps>;
onColumnClick?: (ev: React.MouseEvent<HTMLElement>, column: IColumn) => void;
onColumnContextMenu?: (column: IColumn, ev: React.MouseEvent<HTMLElement>) => void;
dragDropHelper?: IDragDropHelper | null;
isDraggable?: boolean;
setDraggedItemIndex?: (itemIndex: number) => void;
Copy link
Copy Markdown
Member

@shre-verse shre-verse May 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setDraggedItemIndex?: (itemIndex: number) => void; [](start = 2, length = 50)

Rather than this, the notification should be simple - like 'onColumnDragStart' with no params. The params (index) can be handled inside the DetailsHeader, where this callback is binded to a function. #Closed

isDropped?: boolean;
}

export class DetailsColumn extends BaseComponent<IDetailsColumnProps> {
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 [
<div
key={column.key}
ref={this._root}
role={'columnheader'}
aria-sort={column.isSorted ? (column.isSortedDescending ? 'descending' : 'ascending') : 'none'}
aria-disabled={column.columnActionsMode === ColumnActionsMode.disabled}
aria-colindex={columnIndex}
className={css(
'ms-DetailsHeader-cell',
styles.cell,
column.headerClassName,
column.columnActionsMode !== ColumnActionsMode.disabled && 'is-actionable ' + styles.cellIsActionable,
!column.name && 'is-empty ' + styles.cellIsEmpty,
(column.isSorted || column.isGrouped || column.isFiltered) && 'is-icon-visible',
column.isPadded && styles.cellWrapperPadded
)}
data-is-draggable={isDraggable}
draggable={isDraggable}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need both data-is-draggable and draggable attributes?

Copy link
Copy Markdown
Contributor Author

@laxmikankanala laxmikankanala Jun 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have followed DetailsRow, might have been kept both for legacy reasons,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yiminwu @ThomasMichon - do any of you recall why we need both these attributes? Seems sort of redundant for the new component.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't remember.


In reply to: 196860743 [](ancestors = 196860743)

style={{ width: column.calculatedWidth! + INNER_PADDING + (column.isPadded ? ISPADDED_WIDTH : 0) }}
data-automationid={'ColumnsHeaderColumn'}
data-item-key={column.key}
>
{isDraggable && <Icon iconName={'GripperBarVertical'} className={css(styles.GripperBarVerticalStyle)} />}
{onRenderColumnHeaderTooltip(
{
hostClassName: css(styles.cellTooltip),
id: `${parentId}-${column.key}-tooltip`,
setAriaDescribedBy: false,
content: column.columnActionsMode !== ColumnActionsMode.disabled ? column.ariaLabel : '',
children: (
<span
id={`${parentId}-${column.key}`}
aria-label={column.isIconOnly ? column.name : undefined}
aria-labelledby={column.isIconOnly ? undefined : `${parentId}-${column.key}-name `}
className={css('ms-DetailsHeader-cellTitle', styles.cellTitle)}
data-is-focusable={column.columnActionsMode !== ColumnActionsMode.disabled}
role={column.columnActionsMode !== ColumnActionsMode.disabled ? 'button' : undefined}
aria-describedby={
this.props.onRenderColumnHeaderTooltip ? `${parentId}-${column.key}-tooltip` : undefined
}
onContextMenu={this._onColumnContextMenu.bind(this, column)}
onClick={this._onColumnClick.bind(this, column)}
aria-haspopup={column.columnActionsMode === ColumnActionsMode.hasDropdown}
>
<span
id={`${parentId}-${column.key}-name`}
className={css('ms-DetailsHeader-cellName', styles.cellName, {
[styles.iconOnlyHeader]: column.isIconOnly
})}
>
{(column.iconName || column.iconClassName) && (
<Icon className={css(styles.nearIcon, column.iconClassName)} iconName={column.iconName} />
)}

{!column.isIconOnly ? column.name : undefined}
</span>

{column.isFiltered && <Icon className={styles.nearIcon} iconName={'Filter'} />}

{column.isSorted && (
<Icon
className={css(styles.nearIcon, styles.sortIcon)}
iconName={column.isSortedDescending ? 'SortDown' : 'SortUp'}
/>
)}

{column.isGrouped && <Icon className={styles.nearIcon} iconName={'GroupedDescending'} />}

{column.columnActionsMode === ColumnActionsMode.hasDropdown &&
!column.isIconOnly && (
<Icon
className={css('ms-DetailsHeader-filterChevron', styles.filterChevron)}
iconName={'ChevronDown'}
/>
)}
</span>
)
},
this._onRenderColumnHeaderTooltip
)}
</div>,
column.ariaLabel && !this.props.onRenderColumnHeaderTooltip ? (
<label key={`${column.key}_label`} id={`${parentId}-${column.key}-tooltip`} className={styles.accessibleLabel}>
{column.ariaLabel}
</label>
) : 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<ITooltipHostProps>
): JSX.Element => {
return <span className={tooltipHostProps.hostClassName}>{tooltipHostProps.children}</span>;
};

private _onColumnClick(column: IColumn, ev: React.MouseEvent<HTMLElement>): 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<HTMLElement>): void {
const { onColumnContextMenu } = this.props;
if (column.onColumnContextMenu) {
column.onColumnContextMenu(column, ev);
Copy link
Copy Markdown
Contributor

@aditima aditima Jun 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, why we need both column.onColumnContextMenu and props.onColumnContextMenu. We'll end up executing both if they are provided, not sure if that is intentional too. #Resolved

Copy link
Copy Markdown
Contributor Author

@laxmikankanala laxmikankanala Jun 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an existing function which was there in ColumnHeader, i just moved it as is. #Resolved

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see... thanks!


In reply to: 196748992 [](ancestors = 1967489)

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();
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -123,6 +135,7 @@ $isPaddedMargin: 24px;
@include margin-left(-16px);
}


.collapseButton {
text-align: center;
transform: rotate(-180deg);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -43,6 +47,7 @@ describe('DetailsHeader', () => {
layoutMode={DetailsListLayoutMode.fixedColumns}
columns={_columns}
onColumnResized={onColumnResized}
columnReorderOptions={_columnReorderOptions}
/>
);

Expand Down
Loading